#!/usr/bin/env python # -------------------------------------------------------------------- # convert SVG to Ipe format # -------------------------------------------------------------------- # # Copyright (C) 2009-2014 Otfried Cheong # # svgtoipe is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # svgtoipe is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with svgtoipe; if not, you can find it at # "http://www.gnu.org/copyleft/gpl.html", or write to the Free # Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # # -------------------------------------------------------------------- svgtoipe_version = "20091018" import sys import xml.dom.minidom as xml from xml.dom.minidom import Node import re import math import base64 import cStringIO try: from PIL import Image have_pil = True except: have_pil = False # -------------------------------------------------------------------- color_keywords = { "black" : "rgb(0, 0, 0)", "green" :"rgb(0, 128, 0)", "silver" :"rgb(192, 192, 192)", "lime" :"rgb(0, 255, 0)", "gray" :"rgb(128, 128, 128)", "olive" :"rgb(128, 128, 0)", "white" :"rgb(255, 255, 255)", "yellow" :"rgb(255, 255, 0)", "maroon" :"rgb(128, 0, 0)", "navy" :"rgb(0, 0, 128)", "red" :"rgb(255, 0, 0)", "blue" :"rgb(0, 0, 255)", "purple" :"rgb(128, 0, 128)", "teal" :"rgb(0, 128, 128)", "fuchsia" :"rgb(255, 0, 255)", "aqua" :"rgb(0, 255, 255)", } attribute_names = [ "stroke", "fill", "stroke-opacity", "fill-opacity", "stroke-width", "fill-rule", "stroke-linecap", "stroke-linejoin", "stroke-dasharray", "stroke-dashoffset", "stroke-miterlimit", "opacity", "font-size" ] def printAttributes(n): a = n.attributes for i in range(a.length): name = a.item(i).name if name[:9] != "sodipodi:" and name[:9] != "inkscape:": print " ", name, n.getAttribute(name) def parse_float(txt): if not txt: return None if txt.endswith('px') or txt.endswith('pt'): return float(txt[:-2]) elif txt.endswith('pc'): return 12 * float(txt[:-2]) elif txt.endswith('mm'): return 72.0 * float(txt[:-2]) / 25.4 elif txt.endswith('cm'): return 72.0 * float(txt[:-2]) / 2.54 elif txt.endswith('in'): return 72.0 * float(txt[:-2]) else: return float(txt) def parse_opacity(txt): if not txt: return None m = int(10 * (float(txt) + 0.05)) if m == 0: m = 1 return 10 * m def parse_list(string): return re.findall("([A-Za-z]|-?[0-9]+\.?[0-9]*(?:e-?[0-9]*)?)", string) def parse_style(string): sdict = {} for item in string.split(';'): if ':' in item: key, value = item.split(':') sdict[key.strip()] = value.strip() return sdict def parse_color_component(txt): if txt.endswith("%"): return float(txt[:-1]) / 100.0 else: return int(txt) / 255.0 def parse_color(c): if not c or c == 'none': return None if c in color_keywords: c = color_keywords[c] m = re.match(r"rgb\(([0-9\.]+%?),\s*([0-9\.]+%?),\s*([0-9\.]+%?)\s*\)", c) if m: r = parse_color_component(m.group(1)) g = parse_color_component(m.group(2)) b = parse_color_component(m.group(3)) return (r, g, b) m = re.match(r"#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])$", c) if m: r = int(m.group(1), 16) / 15.0 g = int(m.group(2), 16) / 15.0 b = int(m.group(3), 16) / 15.0 return (r, g, b) m = re.match(r"#([0-9a-fA-F][0-9a-fA-F])([0-9a-fA-F][0-9a-fA-F])" + r"([0-9a-fA-F][0-9a-fA-F])$", c) if m: r = int(m.group(1), 16) / 255.0 g = int(m.group(2), 16) / 255.0 b = int(m.group(3), 16) / 255.0 return (r, g, b) sys.stderr.write("Unknown color: %s\n" % c) return None def pnext(d, n): l = [] while n > 0: l.append(float(d.pop(0))) n -= 1 return tuple(l) def parse_path(out, d): d = re.findall("([A-Za-z]|-?[0-9]+\.?[0-9]*(?:e-?[0-9]*)?)", d) x, y = 0.0, 0.0 xs, ys = 0.0, 0.0 while d: if not d[0][0] in "01234567890.-": opcode = d.pop(0) if opcode == 'M': x, y = pnext(d, 2) out.write("%g %g m\n" % (x, y)) opcode = 'L' elif opcode == 'm': x1, y1 = pnext(d, 2) x += x1 y += y1 out.write("%g %g m\n" % (x, y)) opcode = 'l' elif opcode == 'L': x, y = pnext(d, 2) out.write("%g %g l\n" % (x, y)) elif opcode == 'l': x1, y1 = pnext(d, 2) x += x1 y += y1 out.write("%g %g l\n" % (x, y)) elif opcode == 'H': x = pnext(d, 1)[0] out.write("%g %g l\n" % (x, y)) elif opcode == 'h': x += pnext(d, 1)[0] out.write("%g %g l\n" % (x, y)) elif opcode == 'V': y = pnext(d, 1)[0] out.write("%g %g l\n" % (x, y)) elif opcode == 'v': y += pnext(d, 1)[0] out.write("%g %g l\n" % (x, y)) elif opcode == 'C': x1, y1, xs, ys, x, y = pnext(d, 6) out.write("%g %g %g %g %g %g c\n" % (x1, y1, xs, ys, x, y)) elif opcode == 'c': x1, y1, xs, ys, xf, yf = pnext(d, 6) x1 += x; y1 += y xs += x; ys += y x += xf; y += yf out.write("%g %g %g %g %g %g c\n" % (x1, y1, xs, ys, x, y)) elif opcode == 'S' or opcode == 's': x2, y2, xf, yf = pnext(d, 4) if opcode == 's': x2 += x; y2 += y xf += x; yf += y x1 = x + (x - xs); y1 = y + (y - ys) out.write("%g %g %g %g %g %g c\n" % (x1, y1, x2, y2, xf, yf)) xs, ys = x2, y2 x, y = xf, yf elif opcode == 'Q': xs, ys, x, y = pnext(d, 4) out.write("%g %g %g %g q\n" % (xs, ys, x, y)) elif opcode == 'q': xs, ys, xf, yf = pnext(d, 4) xs += x; ys += y x += xf; y += yf out.write("%g %g %g %g q\n" % (xs, ys, x, y)) elif opcode == 'T' or opcode == 't': xf, yf = pnext(d, 2) if opcode == 't': xf += x; yf += y x1 = x + (x - xs); y1 = y + (y - ys) out.write("%g %g %g %g q\n" % (x1, y1, xf, yf)) xs, ys = x1, y1 x, y = xf, yf elif opcode == 'A' or opcode == 'a': rx, ry, phi, large_arc, sweep, x2, y2 = pnext(d, 7) if opcode == 'a': x2 += x; y2 += y draw_arc(out, x, y, rx, ry, phi, large_arc, sweep, x2, y2) x, y = x2, y2 elif opcode in 'zZ': out.write("h\n") else: sys.stderr.write("Unrecognised opcode: %s\n" % opcode) def parse_transformation(txt): d = re.findall("[a-zA-Z]+\([^)]*\)", txt) m = Matrix() while d: m1 = Matrix(d.pop(0)) m = m * m1 return m def get_gradientTransform(n): if n.hasAttribute("gradientTransform"): return parse_transformation(n.getAttribute("gradientTransform")) return Matrix() def parse_transform(n): if n.hasAttribute("transform"): return parse_transformation(n.getAttribute("transform")) return None # Convert from endpoint to center parameterization # www.w3.org/TR/2003/REC-SVG11-20030114/implnote.html#ArcImplementationNotes def draw_arc(out, x1, y1, rx, ry, phi, large_arc, sweep, x2, y2): phi = math.pi * phi / 180.0 cp = math.cos(phi); sp = math.sin(phi) dx = .5 * (x1 - x2); dy = .5 * (y1 - y2) x1p = cp * dx + sp * dy; y1p = -sp * dx + cp * dy r2 = (((rx * ry)**2 - (rx * y1p)**2 - (ry * x1p)**2)/ ((rx * y1p)**2 + (ry * x1p)**2)) if r2 < 0: r2 = 0 r = math.sqrt(r2) if large_arc == sweep: r = -r cxp = r * rx * y1p / ry; cyp = -r * ry * x1p / rx cx = cp * cxp - sp * cyp + .5 * (x1 + x2) cy = sp * cxp + cp * cyp + .5 * (y1 + y2) m = Matrix([rx, 0, 0, ry, 0, 0]) m = Matrix([cp, sp, -sp, cp, cx, cy]) * m if sweep == 0: m = m * Matrix([1, 0, 0, -1, 0, 0]) out.write("%s %g %g a\n" % (str(m), x2, y2)) # -------------------------------------------------------------------- class Matrix(object): # Default is identity matrix def __init__(self, string = None): self.values = [1, 0, 0, 1, 0, 0] if not string or string == "": return if isinstance(string, list): self.values = string return mat = re.match(r"([a-zA-Z]+)\(([^)]*)\)$", string) if not mat: sys.stderr.write("Unknown transform: %s\n" % string) op = mat.group(1) d = [float(x) for x in parse_list(mat.group(2))] if op == "matrix": self.values = d elif op == "translate": if len(d) == 1: d.append(0.0) self.values = [1, 0, 0, 1, d[0], d[1]] elif op == "scale": if len(d) == 1: d.append(d[0]) sx, sy = d self.values = [sx, 0, 0, sy, 0, 0] elif op == "rotate": phi = math.pi * d[0] / 180.0 self.values = [math.cos(phi), math.sin(phi), -math.sin(phi), math.cos(phi), 0, 0] elif op == "skewX": tphi = math.tan(math.pi * d[0] / 180.0) self.values = [1, 0, tphi, 1, 0, 0] elif op == "skewY": tphi = math.tan(math.pi * d[0] / 180.0) self.values = [1, tphi, 0, 1, 0, 0] else: sys.stderr.write("Unknown transform: %s\n" % string) def __call__(self, other): return (self.values[0]*other[0] + self.values[2]*other[1] + self.values[4], self.values[1]*other[0] + self.values[3]*other[1] + self.values[5]) def inverse(self): d = float(self.values[0]*self.values[3] - self.values[1]*self.values[2]) return Matrix([self.values[3]/d, -self.values[1]/d, -self.values[2]/d, self.values[0]/d, (self.values[2]*self.values[5] - self.values[3]*self.values[4])/d, (self.values[1]*self.values[4] - self.values[0]*self.values[5])/d]) def __mul__(self, other): a, b, c, d, e, f = self.values u, v, w, x, y, z = other.values return Matrix([a*u + c*v, b*u + d*v, a*w + c*x, b*w + d*x, a*y + c*z + e, b*y + d*z + f]) def __str__(self): a, b, c, d, e, f = self.values return "%g %g %g %g %g %g" % (a, b, c, d, e, f) # -------------------------------------------------------------------- class Svg(): def __init__(self, fname): self.dom = xml.parse(fname) attr = { } for a in attribute_names: attr[a] = None self.attributes = [ attr ] self.defs = { } for n in self.dom.childNodes: if n.nodeType == Node.ELEMENT_NODE and n.tagName == "svg": if n.hasAttribute("viewBox"): x, y, w, h = [float(x) for x in parse_list(n.getAttribute("viewBox"))] self.width = w self.height = h self.origin = (x, y) else: self.width = parse_float(n.getAttribute("width")) self.height = parse_float(n.getAttribute("height")) self.origin = (0, 0) self.root = n return # -------------------------------------------------------------------- def parse_svg(self, outname): self.out = open(outname, "w") self.out.write('\n') self.out.write('\n') self.out.write('\n' % svgtoipe_version) self.out.write('\n') self.out.write(('\n') % (self.width, self.height, self.width, self.height)) for t in range(10, 100, 10): self.out.write('\n' % (t, t)) # set SVG defaults self.out.write('\n') self.out.write('\n') # collect definitions for n in self.root.childNodes: if n.nodeType != Node.ELEMENT_NODE: continue if hasattr(self, "def_" + n.tagName): getattr(self, "def_" + n.tagName)(n) # write definitions into stylesheet if len(self.defs) > 0: self.out.write('\n') for k in self.defs: if self.defs[k][0] == "linearGradient": self.write_linear_gradient(k) elif self.defs[k][0] == "radialGradient": self.write_radial_gradient(k) self.out.write('\n') # start real data self.out.write('\n') m = Matrix([1, 0, 0, 1, 0, self.height / 2.0]) m = m * Matrix([1, 0, 0, -1, 0, 0]) m = m * Matrix([1, 0, 0, 1, -self.origin[0], -(self.origin[1] + self.height / 2.0)]) self.out.write('\n' % str(m)) for n in self.root.childNodes: if n.nodeType != Node.ELEMENT_NODE: continue if hasattr(self, "node_" + n.tagName): getattr(self, "node_" + n.tagName)(n) else: sys.stderr.write("Unhandled node: %s\n" % n.tagName) self.out.write('\n') self.out.write('\n') self.out.write('\n') self.out.close() # -------------------------------------------------------------------- def write_linear_gradient(self, k): typ, x1, x2, y1, y2, stops, matrix = self.defs[k] self.out.write('\n' % (x1, y1, x2, y2)) for s in stops: offset, color = s self.out.write(' \n' % (offset, color[0], color[1], color[2])) self.out.write('\n') def write_radial_gradient(self, k): typ, cx, cy, r, fx, fy, stops, matrix = self.defs[k] self.out.write('\n' % (fx, fy, 0, cx, cy, r)) for s in stops: offset, color = s self.out.write(' \n' % (offset, color[0], color[1], color[2])) self.out.write('\n') def get_stops(self, n): stops = [] for m in n.childNodes: if m.nodeType != Node.ELEMENT_NODE: continue if m.tagName != "stop": continue # should not happen offs = m.getAttribute("offset") if offs.endswith("%"): offs = float(offs[:-1]) / 100.0 else: offs = float(offs) color = parse_color(m.getAttribute("stop-color")) if m.hasAttribute("style"): sdict = parse_style(m.getAttribute("style")) if "stop-color" in sdict: color = parse_color(sdict["stop-color"]) stops.append((offs, color)) if len(stops) == 0: if n.hasAttribute("xlink:href"): ref = n.getAttribute("xlink:href") if ref.startswith("#") and ref[1:] in self.defs: stops = self.defs[ref[1:]][5] return stops def def_linearGradient(self, n): #printAttributes(n) kid = n.getAttribute("id") x1 = 0; y1 = 0 x2 = self.width; y2 = self.height if n.hasAttribute("x1"): s = n.getAttribute("x1") if s.endswith("%"): x1 = self.width * float(s[:-1]) / 100.0 else: x1 = parse_float(s) if n.hasAttribute("x2"): s = n.getAttribute("x2") if s.endswith("%"): x2 = self.width * float(s[:-1]) / 100.0 else: x2 = parse_float(s) if n.hasAttribute("y1"): s = n.getAttribute("y1") if s.endswith("%"): y1 = self.width * float(s[:-1]) / 100.0 else: y1 = parse_float(s) if n.hasAttribute("y2"): s = n.getAttribute("y2") if s.endswith("%"): y2 = self.width * float(s[:-1]) / 100.0 else: y2 = parse_float(s) matrix = get_gradientTransform(n) stops = self.get_stops(n) self.defs[kid] = ("linearGradient", x1, x2, y1, y2, stops, matrix) def def_radialGradient(self, n): #printAttributes(n) kid = n.getAttribute("id") cx = "50%"; cy = "50%"; r = "50%" if n.hasAttribute("cx"): cx = n.getAttribute("cx") if cx.endswith("%"): cx = self.width * float(cx[:-1]) / 100.0 else: cx = parse_float(cx) if n.hasAttribute("cy"): cy = n.getAttribute("cy") if cy.endswith("%"): cy = self.width * float(cy[:-1]) / 100.0 else: cy = parse_float(cy) if n.hasAttribute("r"): r = n.getAttribute("r") if r.endswith("%"): r = self.width * float(r[:-1]) / 100.0 else: r = parse_float(r) if n.hasAttribute("fx"): s = n.getAttribute("fx") if s.endswith("%"): fx = self.width * float(s[:-1]) / 100.0 else: fx = parse_float(s) else: fx = cx if n.hasAttribute("fy"): s = n.getAttribute("fy") if s.endswith("%"): fy = self.width * float(s[:-1]) / 100.0 else: fy = parse_float(s) else: fy = cy matrix = get_gradientTransform(n) stops = self.get_stops(n) self.defs[kid] = ("radialGradient", cx, cy, r, fx, fy, stops, matrix) def def_clipPath(self, node): kid = node.getAttribute("id") # only a single path is implemented for n in node.childNodes: if n.nodeType != Node.ELEMENT_NODE or n.tagName != "path": continue m = parse_transform(n) d = n.getAttribute("d") output = cStringIO.StringIO() parse_path(output, d) path = output.getvalue() output.close() self.defs[kid] = ("clipPath", m, path) return def def_g(self, group): for n in group.childNodes: if n.nodeType != Node.ELEMENT_NODE: continue if hasattr(self, "def_" + n.tagName): getattr(self, "def_" + n.tagName)(n) def def_defs(self, node): self.def_g(node) # -------------------------------------------------------------------- def parse_attributes(self, n): pattr = self.attributes[-1] attr = { } for a in attribute_names: if n.hasAttribute(a): attr[a] = n.getAttribute(a) else: attr[a] = pattr[a] if n.hasAttribute("style"): sdict = parse_style(n.getAttribute("style")) for a in attribute_names: if a in sdict: attr[a] = sdict[a] return attr def write_pathattributes(self, a): stroke = parse_color(a["stroke"]) if stroke: self.out.write(' stroke="%g %g %g"' % stroke) fill = a["fill"] if fill and fill.startswith("url("): mat = re.match("url\(#([^)]+)\).*", fill) if mat: grad = mat.group(1) if grad in self.defs and (self.defs[grad][0] == "linearGradient" or self.defs[grad][0] == "radialGradient"): self.out.write(' fill="1" gradient="g%s"' % grad) else: fill = parse_color(a["fill"]) if fill: self.out.write(' fill="%g %g %g"' % fill) opacity = parse_opacity(a["opacity"]) fill_opacity = parse_opacity(a["fill-opacity"]) stroke_opacity = parse_opacity(a["stroke-opacity"]) if fill and fill_opacity: opacity = fill_opacity if not fill and stroke and stroke_opacity: opacity = stroke_opacity if opacity and opacity != 100: self.out.write(' opacity="%d%%"' % opacity) stroke_width = parse_float(a["stroke-width"]) if a["stroke-width"]: self.out.write(' pen="%g"' % stroke_width) if a["fill-rule"] == "nonzero": self.out.write(' fillrule="wind"') k = {"butt" : 0, "round" : 1, "square" : 2 } if a["stroke-linecap"] in k: self.out.write(' cap="%d"' % k[a["stroke-linecap"]]) k = {"miter" : 0, "round" : 1, "bevel" : 2 } if a["stroke-linejoin"] in k: self.out.write(' join="%d"' % k[a["stroke-linejoin"]]) dasharray = a["stroke-dasharray"] dashoffset = a["stroke-dashoffset"] if dasharray and dashoffset and dasharray != "none": d = parse_list(dasharray) off = parse_float(dashoffset) self.out.write(' dash="[%s] %g"' % (" ".join(d), off)) # -------------------------------------------------------------------- def node_g(self, group): # printAttributes(group) attr = self.parse_attributes(group) self.attributes.append(attr) self.out.write('\n') for n in group.childNodes: if n.nodeType != Node.ELEMENT_NODE: continue if hasattr(self, "node_" + n.tagName): getattr(self, "node_" + n.tagName)(n) else: sys.stderr.write("Unhandled node: %s\n" % n.tagName) self.out.write('\n') self.attributes.pop() def collect_text(self, root): for n in root.childNodes: if n.nodeType == Node.TEXT_NODE: self.text += n.data if n.nodeType != Node.ELEMENT_NODE: continue if n.tagName == "tspan": # recurse self.collect_text(n) def node_text(self, t): if not t.hasAttribute("x") or not t.hasAttribute("y"): sys.stderr.write("Text without coordinates ignored\n") return x = float(t.getAttribute("x")) y = float(t.getAttribute("y")) attr = self.parse_attributes(t) self.out.write('%s\n' % self.text.encode("UTF-8")) def node_image(self, node): if not have_pil: sys.stderr.write("No Python image library, ignored\n") return href = node.getAttribute("xlink:href") if not href.startswith("data:image/png;base64,"): sys.stderr.write("Image ignored, href = %s...\n" % href[:40]) return x = float(node.getAttribute("x")) y = float(node.getAttribute("y")) w = float(node.getAttribute("width")) h = float(node.getAttribute("height")) clipped = False if node.hasAttribute("clip-path"): mat = re.match("url\(#([^)]+)\).*", node.getAttribute("clip-path")) if mat: cp = mat.group(1) if cp in self.defs and self.defs[cp][0] == "clipPath": cp, m, path = self.defs[cp] clipped = True self.out.write('\n' % (str(m), path)) self.out.write('\n' % str(m.inverse())) self.out.write(' \n') if True: data = cStringIO.StringIO() for pixel in image.getdata(): data.write("%c%c%c" % pixel[:3]) self.out.write(base64.b64encode(data.getvalue())) data.close() else: count = 0 for pixel in image.getdata(): self.out.write("%02x%02x%02x" % pixel[:3]) count += 1 if count == 10: self.out.write("\n") count = 0 fin.close() self.out.write('\n') if clipped: self.out.write('\n\n') # handled in def pass def node_linearGradient(self, n): pass def node_radialGradient(self, n): pass def node_rect(self, n): attr = self.parse_attributes(n) self.out.write('\n') x = float(n.getAttribute("x")) y = float(n.getAttribute("y")) w = float(n.getAttribute("width")) h = float(n.getAttribute("height")) self.out.write("%g %g m %g %g l %g %g l %g %g l h\n" % (x, y, x + w, y, x + w, y + h, x, y + h)) self.out.write('\n') def node_circle(self, n): self.out.write('\n') cx = float(n.getAttribute("cx")) cy = float(n.getAttribute("cy")) r = float(n.getAttribute("r")) self.out.write("%g 0 0 %g %g %g e\n" % (r, r, cx, cy)) self.out.write('\n') def node_ellipse(self, n): self.out.write('\n') cx = 0 cy = 0 if n.hasAttribute("cx"): cx = float(n.getAttribute("cx")) if n.hasAttribute("cy"): cy = float(n.getAttribute("cy")) rx = float(n.getAttribute("rx")) ry = float(n.getAttribute("ry")) self.out.write("%g 0 0 %g %g %g e\n" % (rx, ry, cx, cy)) self.out.write('\n') def node_line(self, n): self.out.write('\n') x1 = 0; y1 = 0; x2 = 0; y2 = 0 if n.hasAttribute("x1"): x1 = float(n.getAttribute("x1")) if n.hasAttribute("y1"): y1 = float(n.getAttribute("y1")) if n.hasAttribute("x2"): x2 = float(n.getAttribute("x2")) if n.hasAttribute("y2"): y2 = float(n.getAttribute("y2")) self.out.write("%g %g m %g %g l\n" % (x1, y1, x2, y2)) self.out.write('\n') def node_polyline(self, n): self.polygon(n, closed=False) def node_polygon(self, n): self.polygon(n, closed=True) def polygon(self, n, closed): self.out.write('\n') d = parse_list(n.getAttribute("points")) op = "m" while d: x = float(d.pop(0)) y = float(d.pop(0)) self.out.write("%g %g %s\n" % (x, y, op)) op = "l" if closed: self.out.write("h\n") self.out.write('\n') def node_path(self, n): self.out.write('\n') d = n.getAttribute("d") parse_path(self.out, d) self.out.write('\n') # -------------------------------------------------------------------- def main(): if len(sys.argv) != 2 and len(sys.argv) != 3: sys.stderr.write("Usage: svgtoipe [ ]\n") return fname = sys.argv[1] if len(sys.argv) > 2: outname = sys.argv[2] else: if fname[-4:].lower() == ".svg": outname = fname[:-4] + ".ipe" else: outname = fname + ".ipe" svg = Svg(fname) svg.parse_svg(outname) if __name__ == '__main__': main() # --------------------------------------------------------------------