1import re 2 3 4def _prefer_non_zero(*args): 5 for arg in args: 6 if arg != 0: 7 return arg 8 return 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('%s%s,%s 0 %d 1 %s,%s' % (c, _ntos(rx), _ntos(ry), large_arc, 67 _ntos(x), _ntos(y))) 68 69 def A(self, rx, ry, x, y, large_arc=0): 70 self._arc('A', rx, ry, x, y, large_arc) 71 72 def a(self, rx, ry, x, y, large_arc=0): 73 self._arc('a', rx, ry, x, y, large_arc) 74 75 def _vhline(self, c, x): 76 self._add('%s%s' % (c, _ntos(x))) 77 78 def H(self, x): 79 self._vhline('H', x) 80 81 def h(self, x): 82 self._vhline('h', x) 83 84 def V(self, y): 85 self._vhline('V', y) 86 87 def v(self, y): 88 self._vhline('v', y) 89 90 def _line(self, c, x, y): 91 self._add('%s%s,%s' % (c, _ntos(x), _ntos(y))) 92 93 def L(self, x, y): 94 self._line('L', x, y) 95 96 def l(self, x, y): 97 self._line('l', x, y) 98 99 def _parse_line(self, line): 100 x1 = float(line.attrib.get('x1', 0)) 101 y1 = float(line.attrib.get('y1', 0)) 102 x2 = float(line.attrib.get('x2', 0)) 103 y2 = float(line.attrib.get('y2', 0)) 104 105 self._start_path() 106 self.M(x1, y1) 107 self.L(x2, y2) 108 109 def _parse_rect(self, rect): 110 x = float(rect.attrib.get('x', 0)) 111 y = float(rect.attrib.get('y', 0)) 112 w = float(rect.attrib.get('width')) 113 h = float(rect.attrib.get('height')) 114 rx = float(rect.attrib.get('rx', 0)) 115 ry = float(rect.attrib.get('ry', 0)) 116 117 rx = _prefer_non_zero(rx, ry) 118 ry = _prefer_non_zero(ry, rx) 119 # TODO there are more rules for adjusting rx, ry 120 121 self._start_path() 122 self.M(x + rx, y) 123 self.H(x + w - rx) 124 if rx > 0: 125 self.A(rx, ry, x + w, y + ry) 126 self.V(y + h - ry) 127 if rx > 0: 128 self.A(rx, ry, x + w - rx, y + h) 129 self.H(x + rx) 130 if rx > 0: 131 self.A(rx, ry, x, y + h - ry) 132 self.V(y + ry) 133 if rx > 0: 134 self.A(rx, ry, x + rx, y) 135 self._end_path() 136 137 def _parse_path(self, path): 138 if 'd' in path.attrib: 139 self._start_path(initial_path=path.attrib['d']) 140 141 def _parse_polygon(self, poly): 142 if 'points' in poly.attrib: 143 self._start_path('M' + poly.attrib['points']) 144 self._end_path() 145 146 def _parse_polyline(self, poly): 147 if 'points' in poly.attrib: 148 self._start_path('M' + poly.attrib['points']) 149 150 def _parse_circle(self, circle): 151 cx = float(circle.attrib.get('cx', 0)) 152 cy = float(circle.attrib.get('cy', 0)) 153 r = float(circle.attrib.get('r')) 154 155 # arc doesn't seem to like being a complete shape, draw two halves 156 self._start_path() 157 self.M(cx - r, cy) 158 self.A(r, r, cx + r, cy, large_arc=1) 159 self.A(r, r, cx - r, cy, large_arc=1) 160 161 def _parse_ellipse(self, ellipse): 162 cx = float(ellipse.attrib.get('cx', 0)) 163 cy = float(ellipse.attrib.get('cy', 0)) 164 rx = float(ellipse.attrib.get('rx')) 165 ry = float(ellipse.attrib.get('ry')) 166 167 # arc doesn't seem to like being a complete shape, draw two halves 168 self._start_path() 169 self.M(cx - rx, cy) 170 self.A(rx, ry, cx + rx, cy, large_arc=1) 171 self.A(rx, ry, cx - rx, cy, large_arc=1) 172 173 def add_path_from_element(self, el): 174 tag = _strip_xml_ns(el.tag) 175 parse_fn = getattr(self, '_parse_%s' % tag.lower(), None) 176 if not callable(parse_fn): 177 return False 178 parse_fn(el) 179 if 'transform' in el.attrib: 180 self.transforms[-1] = _transform(el.attrib['transform']) 181 return True 182