1import re 2 3 4def _prefer_non_zero(*args): 5 for arg in args: 6 if arg != 0: 7 return arg 8 return 0.0 9 10 11def _ntos(n): 12 # %f likes to add unnecessary 0's, %g isn't consistent about # decimals 13 return ("%.3f" % n).rstrip("0").rstrip(".") 14 15 16def _strip_xml_ns(tag): 17 # ElementTree API doesn't provide a way to ignore XML namespaces in tags 18 # so we here strip them ourselves: cf. https://bugs.python.org/issue18304 19 return tag.split("}", 1)[1] if "}" in tag else tag 20 21 22def _transform(raw_value): 23 # TODO assumes a 'matrix' transform. 24 # No other transform functions are supported at the moment. 25 # https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform 26 # start simple: if you aren't exactly matrix(...) then no love 27 match = re.match(r"matrix\((.*)\)", raw_value) 28 if not match: 29 raise NotImplementedError 30 matrix = tuple(float(p) for p in re.split(r"\s+|,", match.group(1))) 31 if len(matrix) != 6: 32 raise ValueError("wrong # of terms in %s" % raw_value) 33 return matrix 34 35 36class PathBuilder(object): 37 def __init__(self): 38 self.paths = [] 39 self.transforms = [] 40 41 def _start_path(self, initial_path=""): 42 self.paths.append(initial_path) 43 self.transforms.append(None) 44 45 def _end_path(self): 46 self._add("z") 47 48 def _add(self, path_snippet): 49 path = self.paths[-1] 50 if path: 51 path += " " + path_snippet 52 else: 53 path = path_snippet 54 self.paths[-1] = path 55 56 def _move(self, c, x, y): 57 self._add("%s%s,%s" % (c, _ntos(x), _ntos(y))) 58 59 def M(self, x, y): 60 self._move("M", x, y) 61 62 def m(self, x, y): 63 self._move("m", x, y) 64 65 def _arc(self, c, rx, ry, x, y, large_arc): 66 self._add( 67 "%s%s,%s 0 %d 1 %s,%s" 68 % (c, _ntos(rx), _ntos(ry), large_arc, _ntos(x), _ntos(y)) 69 ) 70 71 def A(self, rx, ry, x, y, large_arc=0): 72 self._arc("A", rx, ry, x, y, large_arc) 73 74 def a(self, rx, ry, x, y, large_arc=0): 75 self._arc("a", rx, ry, x, y, large_arc) 76 77 def _vhline(self, c, x): 78 self._add("%s%s" % (c, _ntos(x))) 79 80 def H(self, x): 81 self._vhline("H", x) 82 83 def h(self, x): 84 self._vhline("h", x) 85 86 def V(self, y): 87 self._vhline("V", y) 88 89 def v(self, y): 90 self._vhline("v", y) 91 92 def _line(self, c, x, y): 93 self._add("%s%s,%s" % (c, _ntos(x), _ntos(y))) 94 95 def L(self, x, y): 96 self._line("L", x, y) 97 98 def l(self, x, y): 99 self._line("l", x, y) 100 101 def _parse_line(self, line): 102 x1 = float(line.attrib.get("x1", 0)) 103 y1 = float(line.attrib.get("y1", 0)) 104 x2 = float(line.attrib.get("x2", 0)) 105 y2 = float(line.attrib.get("y2", 0)) 106 107 self._start_path() 108 self.M(x1, y1) 109 self.L(x2, y2) 110 111 def _parse_rect(self, rect): 112 x = float(rect.attrib.get("x", 0)) 113 y = float(rect.attrib.get("y", 0)) 114 w = float(rect.attrib.get("width")) 115 h = float(rect.attrib.get("height")) 116 rx = float(rect.attrib.get("rx", 0)) 117 ry = float(rect.attrib.get("ry", 0)) 118 119 rx = _prefer_non_zero(rx, ry) 120 ry = _prefer_non_zero(ry, rx) 121 # TODO there are more rules for adjusting rx, ry 122 123 self._start_path() 124 self.M(x + rx, y) 125 self.H(x + w - rx) 126 if rx > 0: 127 self.A(rx, ry, x + w, y + ry) 128 self.V(y + h - ry) 129 if rx > 0: 130 self.A(rx, ry, x + w - rx, y + h) 131 self.H(x + rx) 132 if rx > 0: 133 self.A(rx, ry, x, y + h - ry) 134 self.V(y + ry) 135 if rx > 0: 136 self.A(rx, ry, x + rx, y) 137 self._end_path() 138 139 def _parse_path(self, path): 140 if "d" in path.attrib: 141 self._start_path(initial_path=path.attrib["d"]) 142 143 def _parse_polygon(self, poly): 144 if "points" in poly.attrib: 145 self._start_path("M" + poly.attrib["points"]) 146 self._end_path() 147 148 def _parse_polyline(self, poly): 149 if "points" in poly.attrib: 150 self._start_path("M" + poly.attrib["points"]) 151 152 def _parse_circle(self, circle): 153 cx = float(circle.attrib.get("cx", 0)) 154 cy = float(circle.attrib.get("cy", 0)) 155 r = float(circle.attrib.get("r")) 156 157 # arc doesn't seem to like being a complete shape, draw two halves 158 self._start_path() 159 self.M(cx - r, cy) 160 self.A(r, r, cx + r, cy, large_arc=1) 161 self.A(r, r, cx - r, cy, large_arc=1) 162 163 def _parse_ellipse(self, ellipse): 164 cx = float(ellipse.attrib.get("cx", 0)) 165 cy = float(ellipse.attrib.get("cy", 0)) 166 rx = float(ellipse.attrib.get("rx")) 167 ry = float(ellipse.attrib.get("ry")) 168 169 # arc doesn't seem to like being a complete shape, draw two halves 170 self._start_path() 171 self.M(cx - rx, cy) 172 self.A(rx, ry, cx + rx, cy, large_arc=1) 173 self.A(rx, ry, cx - rx, cy, large_arc=1) 174 175 def add_path_from_element(self, el): 176 tag = _strip_xml_ns(el.tag) 177 parse_fn = getattr(self, "_parse_%s" % tag.lower(), None) 178 if not callable(parse_fn): 179 return False 180 parse_fn(el) 181 if "transform" in el.attrib: 182 self.transforms[-1] = _transform(el.attrib["transform"]) 183 return True 184