prevent double call of _cleanup, which harms usefiles (and is a bad idea in general)
[PyX.git] / pyx / svgfile.py
blob7c0f50c719ced3d14773a6c867cf245ccd608710
1 # -*- encoding: utf-8 -*-
4 # Copyright (C) 2015 André Wobst <wobsta@users.sourceforge.net>
6 # This file is part of PyX (http://pyx.sourceforge.net/).
8 # PyX is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 2 of the License, or
11 # (at your option) any later version.
13 # PyX is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with PyX; if not, write to the Free Software
20 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
23 import xml.sax, re, math, logging
24 from . import baseclasses, bbox, canvas, path, trafo, deco, style, color, unit
26 logger = logging.getLogger("pyx")
29 def endpointarc(x1, y1, x2, y2, fA, fS, rx, ry, phi):
30 # Note: all lengths are _pt, but has been skipped to prevent clumsy notation
31 # See http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
33 # F.6.6 step 1
34 if rx == 0 or ry == 0:
35 return path.line_pt(x1, y1, x2, y2)
37 # F.6.6 step 2
38 if rx < 0: rx = -rx
39 if ry < 0: ry = -ry
41 # F.6.5 step 1
42 cos_phi = math.cos(math.radians(phi))
43 sin_phi = math.sin(math.radians(phi))
44 dx = (x1 - x2) / 2
45 dy = (y1 - y2) / 2
46 x1prim = cos_phi * dx + sin_phi * dy
47 y1prim = -sin_phi * dx + cos_phi * dy
49 # F.6.6 step 3
50 Lambda = (x1prim/rx)**2 + (y1prim/ry)**2
51 if Lambda > 1:
52 Lambda_sqrt = math.sqrt(Lambda)
53 rx *= Lambda_sqrt
54 ry *= Lambda_sqrt
56 # F.6.5 step 2
57 c_sq = ((rx*ry)**2 - (rx*y1prim)**2 - (ry*x1prim)**2) / ((rx*y1prim)**2 + (ry*x1prim)**2)
58 c = math.sqrt(c_sq) if c_sq > 0 else 0
59 if fA == fS:
60 c = -c
61 cxprim = c * rx * y1prim / ry
62 cyprim = -c * ry * x1prim / rx
64 # F.6.5 step 3
65 cx = cos_phi * cxprim - sin_phi * cyprim + dx
66 cy = sin_phi * cxprim + cos_phi * cyprim + dy
68 # F.6.5 step 4
69 theta1 = math.atan2((y1prim - cyprim)/ry, (x1prim - cxprim)/rx)
70 theta2 = math.atan2((-y1prim - cyprim)/ry, (-x1prim - cxprim)/rx)
72 if fS:
73 # clockwise and counterclockwise are exchanged due to negative y axis direction
74 arc = path.path(path.arc_pt(0, 0, 1, theta1*180/math.pi, theta2*180/math.pi))
75 else:
76 arc = path.path(path.arcn_pt(0, 0, 1, theta1*180/math.pi, theta2*180/math.pi))
77 arc = arc.transformed(trafo.scale(rx, ry).rotated(phi))
78 x1p, y1p = arc.atbegin_pt()
79 return arc.transformed(trafo.translate_pt(x1-x1p, y1-y1p))
82 class svgValueError(ValueError): pass
84 class _marker: pass
86 _svgFloatPattern = re.compile("(?P<value>[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)(?P<unit>(px|pt|pc|mm|cm|in|%)?)\s*,?\s*")
87 _svgBoolPattern = re.compile("(?P<bool>[01])\s*,?\s*")
88 _svgPathPattern = re.compile("(?P<cmd>[mlhvcsqtaz])\s*(?P<args>(([^mlhvcsqtaz]|pt|pc|mm|cm)*))", re.IGNORECASE)
89 _svgColorAbsPattern = re.compile("rgb\(\s*(?P<red>[0-9]+)\s*,\s*(?P<green>[0-9]+)\s*,\s*(?P<blue>[0-9]+)\s*\)$", re.IGNORECASE)
90 _svgColorRelPattern = re.compile("rgb\(\s*(?P<red>[0-9]+)%\s*,\s*(?P<green>[0-9]+)%\s*,\s*(?P<blue>[0-9]+)%\s*\)$", re.IGNORECASE)
93 class svgBaseHandler(xml.sax.ContentHandler):
95 def __init__(self, resolution):
96 self.resolution = resolution
97 self.units = {"": 72/self.resolution,
98 "px": 72/self.resolution,
99 "pt": 1,
100 "pc": 12,
101 "mm": 72/25.4,
102 "cm": 72/2.54,
103 "in": 72}
105 def toFloat(self, arg, relative=None, single=False, units=True):
106 match = _svgFloatPattern.match(arg)
107 if not match:
108 raise svgValueError("could not match float for '%s'" % arg)
109 if match.group("unit") and not units:
110 raise svgValueError("no units allowed for '%s'" % arg)
111 value = float(match.group("value"))
112 if match.group("unit") == "%":
113 if relative is not None:
114 value *= 0.01 * relative
115 else:
116 raise svgValueError("missing support for relative coordinates")
117 elif units:
118 value *= self.units[match.group("unit")]
119 if single:
120 if match.end() < len(arg):
121 raise svgValueError("could not match single float for '%s'" % arg)
122 return value
123 return value, arg[match.end():]
125 def toFloats(self, args, units=True):
126 while args:
127 float, args = self.toFloat(args, units=units)
128 yield float
131 class svgHandler(svgBaseHandler):
133 def __init__(self, resolution):
134 super().__init__(resolution)
135 self.stack = []
136 self.stroke = None
137 self.fill = color.grey.black
139 def toBool(self, arg):
140 match = _svgBoolPattern.match(arg)
141 if not match:
142 raise svgValueError("could not match boolean for '%s'" % arg)
143 return match.group("bool") == "1", arg[match.end():]
145 def toPath(self, svgPath):
146 # Note: all lengths are _pt, but _pt has been skipped to prevent clumsy notation
147 p = path.path()
148 for match in _svgPathPattern.finditer(svgPath):
149 cmd = match.group("cmd")
150 args = match.group("args")
151 try:
152 if cmd not in "aA":
153 args = self.toFloats(args)
154 if cmd in "MmLl":
155 first = True
156 while args:
157 x, y, *args = args
158 if cmd in "Ll" or not first:
159 p.append(path.lineto_pt(x, y) if cmd.isupper() else
160 path.rlineto_pt(x, y))
161 else:
162 p.append(path.moveto_pt(x, y) if cmd.isupper() or not p else
163 path.rmoveto_pt(x, y))
164 first = False
165 elif cmd in "HhVv":
166 x, y = p.atend_pt() if cmd.isupper() else (0, 0)
167 for arg in args:
168 if cmd in "Hh":
169 x = arg
170 else:
171 y = arg
172 p.append(path.lineto_pt(x, y) if cmd.isupper() else
173 path.rlineto_pt(x, y))
174 elif cmd in "CcSs":
175 while args:
176 if cmd in "Cc":
177 x1, y1, x2, y2, x3, y3, *args = args
178 else:
179 x2, y2, x3, y3, *args = args
180 if isinstance(p[-1], path.curveto_pt):
181 x1 = p[-1].x3_pt - p[-1].x2_pt
182 y1 = p[-1].y3_pt - p[-1].y2_pt
183 elif isinstance(p[-1], path.rcurveto_pt):
184 x1 = p[-1].dx3_pt - p[-1].dx2_pt
185 y1 = p[-1].dy3_pt - p[-1].dy2_pt
186 else:
187 x1, y1 = 0, 0
188 if cmd == "S":
189 x0, y0 = p.atend_pt()
190 x1 += x0
191 y1 += y0
192 p.append(path.curveto_pt(x1, y1, x2, y2, x3, y3) if cmd.isupper() else
193 path.rcurveto_pt(x1, y1, x2, y2, x3, y3))
194 elif cmd in "QqTt":
195 while args:
196 x0, y0 = p.atend_pt()
197 if cmd in "Qq":
198 xq, yq, x3, y3, *args = args
199 if cmd == "q":
200 xq += x0
201 yq += y0
202 x3 += x0
203 y3 += y0
204 else:
205 x3, y3, *args = args
206 if cmd == "t":
207 x3 += x0
208 y3 += y0
209 if isinstance(p[-1], path.curveto_pt):
210 xq = x0 + 3/2 * (p[-1].x3_pt - p[-1].x2_pt)
211 yq = y0 + 3/2 * (p[-1].y3_pt - p[-1].y2_pt)
212 elif isinstance(p[-1], path.rcurveto_pt):
213 xq = x0 + 3/2 * (p[-1].dx3_pt - p[-1].dx2_pt)
214 yq = y0 + 3/2 * (p[-1].dy3_pt - p[-1].dy2_pt)
215 else:
216 xq, yq = p.atend_pt()
217 x1 = x0 + 2/3 * (xq - x0)
218 y1 = y0 + 2/3 * (yq - y0)
219 x2 = x3 + 2/3 * (xq - x3)
220 y2 = y3 + 2/3 * (yq - y3)
221 p.append(path.curveto_pt(x1, y1, x2, y2, x3, y3))
222 elif cmd in "aA":
223 while args:
224 rx, args = self.toFloat(args)
225 ry, args = self.toFloat(args)
226 phi, args = self.toFloat(args)
227 fA, args = self.toBool(args)
228 fS, args = self.toBool(args)
229 x2, args = self.toFloat(args)
230 y2, args = self.toFloat(args)
231 x1, y1 = p.atend_pt()
232 if cmd == "a":
233 x2 += x1
234 y2 += y1
235 p.join(endpointarc(x1, y1, x2, y2, fA, fS, rx, ry, phi))
236 else:
237 assert cmd in "zZ"
238 p.append(path.closepath())
239 except svgValueError:
240 pass
241 return p
243 def toTrafo(self, svgTrafo):
244 t = trafo.identity
245 for match in reversed(list(re.finditer("(?P<cmd>matrix|translate|scale|rotate|skewX|skewY)\((?P<args>[^)]*)\)", svgTrafo))):
246 cmd = match.group("cmd")
247 args = match.group("args")
248 if cmd == "matrix":
249 a, args = self.toFloat(args, units=False)
250 b, args = self.toFloat(args, units=False)
251 c, args = self.toFloat(args, units=False)
252 d, args = self.toFloat(args, units=False)
253 e, args = self.toFloat(args)
254 f = self.toFloat(args, single=True)
255 t = t * trafo.trafo_pt(((a, b), (c, d)), (e, f))
256 elif cmd == "translate":
257 args = list(self.toFloats(args))
258 if len(args) == 1:
259 args.append(0)
260 assert len(args) == 2
261 t = t.translated_pt(args[0], args[1])
262 elif cmd == "scale":
263 args = list(self.toFloats(args, units=False))
264 if len(args) == 1:
265 args.append(args[0])
266 assert len(args) == 2
267 t = t.scaled(args[0], args[1])
268 elif cmd == "rotate":
269 a, args = self.toFloat(args, units=False)
270 if args:
271 b, args = self.toFloat(args)
272 c = self.toFloat(args, single=True)
273 else:
274 b, c = 0, 0
275 t = t.rotated_pt(a, b, c)
276 elif cmd == "skewX":
277 t = t * trafo.trafo_pt(((1, math.tan(self.toFloat(args, units=False, single=True)*math.pi/180)), (0, 1)))
278 else:
279 assert cmd == "skewY"
280 t = t * trafo.trafo_pt(((1, 0), (math.tan(self.toFloat(args, units=False, single=True)*math.pi/180), 1)))
281 return t
283 def toColor(self, name, inherit):
284 if name == "currentColor":
285 return None # TODO
286 if name == "inherit":
287 return inherit
288 if name == "none":
289 return None
290 names = {"aliceblue": "rgb(240, 248, 255)", "antiquewhite": "rgb(250, 235, 215)", "aqua": "rgb( 0, 255, 255)",
291 "aquamarine": "rgb(127, 255, 212)", "azure": "rgb(240, 255, 255)", "beige": "rgb(245, 245, 220)",
292 "bisque": "rgb(255, 228, 196)", "black": "rgb( 0, 0, 0)", "blanchedalmond": "rgb(255, 235, 205)",
293 "blue": "rgb( 0, 0, 255)", "blueviolet": "rgb(138, 43, 226)", "brown": "rgb(165, 42, 42)",
294 "burlywood": "rgb(222, 184, 135)", "cadetblue": "rgb( 95, 158, 160)", "chartreuse": "rgb(127, 255, 0)",
295 "chocolate": "rgb(210, 105, 30)", "coral": "rgb(255, 127, 80)", "cornflowerblue": "rgb(100, 149, 237)",
296 "cornsilk": "rgb(255, 248, 220)", "crimson": "rgb(220, 20, 60)", "cyan": "rgb( 0, 255, 255)",
297 "darkblue": "rgb( 0, 0, 139)", "darkcyan": "rgb( 0, 139, 139)", "darkgoldenrod": "rgb(184, 134, 11)",
298 "darkgray": "rgb(169, 169, 169)", "darkgreen": "rgb( 0, 100, 0)", "darkgrey": "rgb(169, 169, 169)",
299 "darkkhaki": "rgb(189, 183, 107)", "darkmagenta": "rgb(139, 0, 139)", "darkolivegreen": "rgb( 85, 107, 47)",
300 "darkorange": "rgb(255, 140, 0)", "darkorchid": "rgb(153, 50, 204)", "darkred": "rgb(139, 0, 0)",
301 "darksalmon": "rgb(233, 150, 122)", "darkseagreen": "rgb(143, 188, 143)", "darkslateblue": "rgb( 72, 61, 139)",
302 "darkslategray": "rgb( 47, 79, 79)", "darkslategrey": "rgb( 47, 79, 79)", "darkturquoise": "rgb( 0, 206, 209)",
303 "darkviolet": "rgb(148, 0, 211)", "deeppink": "rgb(255, 20, 147)", "deepskyblue": "rgb( 0, 191, 255)",
304 "dimgray": "rgb(105, 105, 105)", "dimgrey": "rgb(105, 105, 105)", "dodgerblue": "rgb( 30, 144, 255)",
305 "firebrick": "rgb(178, 34, 34)", "floralwhite": "rgb(255, 250, 240)", "forestgreen": "rgb( 34, 139, 34)",
306 "fuchsia": "rgb(255, 0, 255)", "gainsboro": "rgb(220, 220, 220)", "ghostwhite": "rgb(248, 248, 255)",
307 "gold": "rgb(255, 215, 0)", "goldenrod": "rgb(218, 165, 32)", "gray": "rgb(128, 128, 128)",
308 "grey": "rgb(128, 128, 128)", "green": "rgb( 0, 128, 0)", "greenyellow": "rgb(173, 255, 47)",
309 "honeydew": "rgb(240, 255, 240)", "hotpink": "rgb(255, 105, 180)", "indianred": "rgb(205, 92, 92)",
310 "indigo": "rgb( 75, 0, 130)", "ivory": "rgb(255, 255, 240)", "khaki": "rgb(240, 230, 140)",
311 "lavender": "rgb(230, 230, 250)", "lavenderblush": "rgb(255, 240, 245)", "lawngreen": "rgb(124, 252, 0)",
312 "lemonchiffon": "rgb(255, 250, 205)", "lightblue": "rgb(173, 216, 230)", "lightcoral": "rgb(240, 128, 128)",
313 "lightcyan": "rgb(224, 255, 255)", "lightgoldenrodyellow": "rgb(250, 250, 210)", "lightgray": "rgb(211, 211, 211)",
314 "lightgreen": "rgb(144, 238, 144)", "lightgrey": "rgb(211, 211, 211)", "lightpink": "rgb(255, 182, 193)",
315 "lightsalmon": "rgb(255, 160, 122)", "lightseagreen": "rgb( 32, 178, 170)", "lightskyblue": "rgb(135, 206, 250)",
316 "lightslategray": "rgb(119, 136, 153)", "lightslategrey": "rgb(119, 136, 153)", "lightsteelblue": "rgb(176, 196, 222)",
317 "lightyellow": "rgb(255, 255, 224)", "lime": "rgb( 0, 255, 0)", "limegreen": "rgb( 50, 205, 50)",
318 "linen": "rgb(250, 240, 230)", "magenta": "rgb(255, 0, 255)", "maroon": "rgb(128, 0, 0)",
319 "mediumaquamarine": "rgb(102, 205, 170)", "mediumblue": "rgb( 0, 0, 205)", "mediumorchid": "rgb(186, 85, 211)",
320 "mediumpurple": "rgb(147, 112, 219)", "mediumseagreen": "rgb( 60, 179, 113)", "mediumslateblue": "rgb(123, 104, 238)",
321 "mediumspringgreen": "rgb( 0, 250, 154)", "mediumturquoise": "rgb( 72, 209, 204)", "mediumvioletred": "rgb(199, 21, 133)",
322 "midnightblue": "rgb( 25, 25, 112)", "mintcream": "rgb(245, 255, 250)", "mistyrose": "rgb(255, 228, 225)",
323 "moccasin": "rgb(255, 228, 181)", "navajowhite": "rgb(255, 222, 173)", "navy": "rgb( 0, 0, 128)",
324 "oldlace": "rgb(253, 245, 230)", "olive": "rgb(128, 128, 0)", "olivedrab": "rgb(107, 142, 35)",
325 "orange": "rgb(255, 165, 0)", "orangered": "rgb(255, 69, 0)", "orchid": "rgb(218, 112, 214)",
326 "palegoldenrod": "rgb(238, 232, 170)", "palegreen": "rgb(152, 251, 152)", "paleturquoise": "rgb(175, 238, 238)",
327 "palevioletred": "rgb(219, 112, 147)", "papayawhip": "rgb(255, 239, 213)", "peachpuff": "rgb(255, 218, 185)",
328 "peru": "rgb(205, 133, 63)", "pink": "rgb(255, 192, 203)", "plum": "rgb(221, 160, 221)",
329 "powderblue": "rgb(176, 224, 230)", "purple": "rgb(128, 0, 128)", "red": "rgb(255, 0, 0)",
330 "rosybrown": "rgb(188, 143, 143)", "royalblue": "rgb( 65, 105, 225)", "saddlebrown": "rgb(139, 69, 19)",
331 "salmon": "rgb(250, 128, 114)", "sandybrown": "rgb(244, 164, 96)", "seagreen": "rgb( 46, 139, 87)",
332 "seashell": "rgb(255, 245, 238)", "sienna": "rgb(160, 82, 45)", "silver": "rgb(192, 192, 192)",
333 "skyblue": "rgb(135, 206, 235)", "slateblue": "rgb(106, 90, 205)", "slategray": "rgb(112, 128, 144)",
334 "slategrey": "rgb(112, 128, 144)", "snow": "rgb(255, 250, 250)", "springgreen": "rgb( 0, 255, 127)",
335 "steelblue": "rgb( 70, 130, 180)", "tan": "rgb(210, 180, 140)", "teal": "rgb( 0, 128, 128)",
336 "thistle": "rgb(216, 191, 216)", "tomato": "rgb(255, 99, 71)", "turquoise": "rgb( 64, 224, 208)",
337 "violet": "rgb(238, 130, 238)", "wheat": "rgb(245, 222, 179)", "white": "rgb(255, 255, 255)",
338 "whitesmoke": "rgb(245, 245, 245)", "yellow": "rgb(255, 255, 0)", "yellowgreen": "rgb(154, 205, 50)"}
339 name = names.get(name, name)
340 match = _svgColorAbsPattern.match(name.strip())
341 if match:
342 return color.rgb(int(match.group("red"))/255, int(match.group("green"))/255, int(match.group("blue"))/255)
343 match = _svgColorRelPattern.match(name.strip())
344 if match:
345 return color.rgb(int(match.group("red"))/100, int(match.group("green"))/100, int(match.group("blue"))/100)
346 return color.rgbfromhexstring(name)
348 def startElementNS(self, name, qname, attributes):
350 def floatAttr(localname, default=_marker):
351 if default is _marker:
352 return self.toFloat(attributes[None, localname])[0]
353 else:
354 try:
355 return self.toFloat(attributes[None, localname])[0]
356 except KeyError:
357 return default
359 def pathAttrs(default=_marker):
360 if default is not _marker:
361 attrs = default
362 else:
363 attrs = []
364 if (None, "transform") in attributes:
365 attrs.append(self.toTrafo(attributes[None, "transform"]))
366 if (None, "stroke-dasharray") in attributes:
367 attrs.append(style.dash(self.toFloats(attributes[None, "stroke-dasharray"]),
368 offset=floatAttr("stroke-dashoffset", 0),
369 rellengths=False))
370 if (None, "stroke-linecap") in attributes:
371 attrs.append({"butt": style.linecap.butt,
372 "round": style.linecap.round,
373 "square": style.linecap.square}[attributes[None, "stroke-linecap"]])
374 if (None, "stroke-linejoin") in attributes:
375 attrs.append({"miter": style.linejoin.miter,
376 "round": style.linejoin.round,
377 "bevel": style.linejoin.bevel}[attributes[None, "stroke-linejoin"]])
378 if (None, "stroke-miterlimit") in attributes:
379 attrs.append(style.miterlimit(floatAttr("stroke-miterlimit")))
380 if (None, "stroke-width") in attributes:
381 attrs.append(style.linewidth(floatAttr("stroke-width")*unit.t_pt))
382 if (None, "fill-rule") in attributes:
383 attrs.append({"nonzero": style.fillrule.nonzero_winding,
384 "evenodd": style.fillrule.even_odd}[attributes[None, "fill-rule"]])
385 return attrs
387 namespace, localname = name
388 if namespace == "http://www.w3.org/2000/svg":
389 if localname == "svg":
390 attrs = pathAttrs([style.linewidth(1*unit.t_pt), style.miterlimit(4)])
391 outer_x = self.toFloat(attributes.get((None, "x"), "0"), single=True)
392 outer_y = self.toFloat(attributes.get((None, "y"), "0"), single=True)
393 if (None, "viewBox") in attributes:
394 inner_x, inner_y, inner_width, inner_height = self.toFloats(attributes[None, "viewBox"])
395 if attributes.get((None, "clip"), "auto") == "auto":
396 attrs.append(canvas.clip(path.rect(inner_x, inner_y, inner_width, inner_height)))
397 outer_width = self.toFloat(attributes.get((None, "width"), "100%"), single=True, relative=inner_width)
398 outer_height = self.toFloat(attributes.get((None, "height"), "100%"), single=True, relative=inner_height)
399 self.bbox = bbox.bbox_pt(outer_x, -outer_y, outer_x+outer_width, -outer_y+outer_height)
400 attrs.append(trafo.translate_pt(-inner_x, -inner_y))
401 attrs.append(trafo.scale(outer_width/inner_width, outer_height/inner_height))
402 attrs.append(trafo.translate_pt(outer_x, outer_y))
403 attrs.append(trafo.translate_pt(0, -outer_height))
404 elif (None, "width") in attributes and (None, "height") in attributes:
405 outer_width = self.toFloat(attributes.get((None, "width"), "100%"), single=True)
406 outer_height = self.toFloat(attributes.get((None, "height"), "100%"), single=True)
407 self.bbox = bbox.bbox_pt(outer_x, -outer_y, outer_x+outer_width, -outer_y+outer_height)
408 attrs.append(trafo.translate_pt(outer_x, outer_y))
409 attrs.append(trafo.translate_pt(0, -outer_height))
410 else:
411 self.bbox = None
412 raise ValueError("SVG viewbox or width and height missing, we continue by aligning by SVG coordinates (top-left) instead of PyX-like (bottom-left) and calculate the bbox from the SVG content")
413 attrs.append(trafo.mirror(0))
414 self.canvas = canvas.canvas(attrs)
415 elif localname == "g":
416 self.stack.append((self.canvas, self.stroke, self.fill))
417 self.canvas = self.canvas.insert(canvas.canvas(pathAttrs()))
418 self.fill = self.toColor(attributes.get((None, "fill"), "inherit"), self.fill)
419 self.stroke = self.toColor(attributes.get((None, "stroke"), "inherit"), self.stroke)
420 elif localname in ["rect", "circle", "ellipse", "line", "polyline", "polygon", "path"]:
421 if localname == "line":
422 p = path.line_pt(floatAttr("x1"), floatAttr("y1"), floatAttr("x2"), floatAttr("y2"))
423 elif localname == "rect":
424 x, y = floatAttr("x", 0), floatAttr("y", 0)
425 width, height = floatAttr("width"), floatAttr("height")
426 if width == 0 or height == 0:
427 p = None
428 else:
429 rx, ry = floatAttr("rx", None), floatAttr("ry", None)
430 if ((rx is None or rx < 1e-10) and (ry is None or ry < 1e-10)):
431 p = path.rect_pt(x, y, width, height)
432 else:
433 if rx is None: rx = ry
434 elif ry is None: ry = rx
435 if 2*rx > width: rx = 0.5*width
436 if 2*ry > height: ry = 0.5*height
437 c = path.circle_pt(0, 0, 1).transformed(trafo.scale(rx, ry))
438 c1, c2, c3, c4 = c.split_pt([i*c.arclen_pt()/4 for i in range(4)])
439 p = c1.transformed(trafo.translate_pt(x+width-rx, y+ry))
440 p.join(c2.transformed(trafo.translate_pt(x+width-rx, y+height-ry)))
441 p.join(c3.transformed(trafo.translate_pt(x+rx, y+height-ry)))
442 p.join(c4.transformed(trafo.translate_pt(x+rx, y+ry)))
443 p.append(path.closepath())
444 elif localname == "circle":
445 if floatAttr("r") != 0:
446 p = path.circle_pt(floatAttr("cx", 0), floatAttr("cy", 0), floatAttr("r"))
447 else:
448 p = None
449 elif localname == "ellipse":
450 if floatAttr("rx") != 0 and floatAttr("ry") != 0:
451 p = path.ellipse_pt(floatAttr("cx", 0), floatAttr("cy", 0), floatAttr("rx"), floatAttr("ry"), angle=0)
452 else:
453 p = None
454 elif localname == "polyline" or localname == "polygon":
455 x, y, *args = self.toFloats(attributes[None, "points"])
456 p = path.path(path.moveto_pt(x, y))
457 while len(args) >= 2:
458 x, y, *args = args
459 p.append(path.lineto_pt(x, y))
460 if localname == "polygon":
461 p.append(path.closepath())
462 else:
463 assert localname == "path"
464 p = self.toPath(attributes[None, "d"])
465 if p is not None:
466 attrs = pathAttrs()
467 fill = self.toColor(attributes.get((None, "fill"), "inherit"), self.fill)
468 if fill:
469 attrs.append(deco.filled([fill]))
470 stroke = self.toColor(attributes.get((None, "stroke"), "inherit"), self.stroke)
471 if stroke:
472 attrs.append(deco.stroked([stroke]))
473 if stroke or fill:
474 self.canvas.draw(p, attrs)
476 def endElementNS(self, name, qname):
477 namespace, localname = name
478 if namespace == "http://www.w3.org/2000/svg":
479 if localname == "g":
480 self.canvas, self.stroke, self.fill = self.stack.pop()
482 class svgBboxDoneException(Exception): pass
484 class svgBboxHandler(svgBaseHandler):
486 def startElementNS(self, name, qname, attributes):
487 if name != ("http://www.w3.org/2000/svg", "svg"):
488 raise ValueError("not an SVG file")
489 if (None, "width") not in attributes or (None, "height") not in attributes:
490 raise ValueError("SVG width and height missing, which is required for unparsed SVG inclusion")
491 outer_x = self.toFloat(attributes.get((None, "x"), "0"), single=True)
492 outer_y = self.toFloat(attributes.get((None, "y"), "0"), single=True)
493 try:
494 outer_width = self.toFloat(attributes.get((None, "width")), single=True)
495 outer_height = self.toFloat(attributes.get((None, "height")), single=True)
496 self.trafo = trafo.translate_pt(0, outer_height) * trafo.scale(72/self.resolution)
497 except svgValueError:
498 inner_x, inner_y, inner_width, inner_height = self.toFloats(attributes[None, "viewBox"])
499 outer_width = self.toFloat(attributes.get((None, "width")), relative=inner_width, single=True)
500 outer_height = self.toFloat(attributes.get((None, "height")), relative=inner_height, single=True)
501 self.trafo = trafo.translate_pt(-0.5*outer_width, outer_height)
502 self.bbox = bbox.bbox_pt(outer_x, -outer_y, outer_x+outer_width, -outer_y+outer_height)
503 raise svgBboxDoneException()
506 class svgfile_pt(baseclasses.canvasitem):
508 def __init__(self, x_pt, y_pt, filename, width_pt=None, height_pt=None, ratio=None, parsed=False, resolution=96):
509 self.filename = filename
510 self.parsed = parsed
511 self.resolution = resolution
513 if parsed:
514 self.svg = svgHandler(resolution)
515 else:
516 self.svg = svgBboxHandler(resolution)
517 parser = xml.sax.make_parser()
518 parser.setContentHandler(self.svg)
519 parser.setFeature(xml.sax.handler.feature_namespaces, True)
520 parser.setFeature(xml.sax.handler.feature_external_ges, False)
521 parser.setFeature(xml.sax.handler.feature_external_pes, False)
522 if parsed:
523 with open(filename, "rb") as f:
524 parser.parse(f)
525 else:
526 try:
527 with open(filename, "rb") as f:
528 parser.parse(f)
529 except svgBboxDoneException:
530 pass
531 else:
532 raise ValueError("no XML found")
534 if not self.svg.bbox:
535 # fallback for parsed svg without viewbox
536 self.svg.bbox = self.svg.canvas.bbox()
538 self.trafo = trafo.translate_pt(x_pt, y_pt)
540 if width_pt is not None or height_pt is not None:
541 svgwidth_pt = self.svg.bbox.width_pt()
542 svgheight_pt = self.svg.bbox.height_pt()
543 if width_pt is None:
544 if ratio is None:
545 width_pt = height_pt * svgwidth_pt / svgheight_pt
546 else:
547 width_pt = ratio * height_pt
548 elif height_pt is None:
549 if ratio is None:
550 height_pt = width_pt * svgheight_pt / svgwidth_pt
551 else:
552 height_pt = (1.0/ratio) * width_pt
553 elif ratio is not None:
554 raise ValueError("can't specify a ratio when setting width_pt and height_pt")
555 self.trafo *= trafo.scale_pt(width_pt/svgwidth_pt, height_pt/svgheight_pt)
556 else:
557 if ratio is not None:
558 raise ValueError("must specify width_pt or height_pt to set a ratio")
560 self.trafo *= trafo.translate_pt(-self.svg.bbox.llx_pt, -self.svg.bbox.lly_pt)
562 self._bbox = self.svg.bbox.transformed(self.trafo)
563 if self.parsed:
564 self.canvas = canvas.canvas([self.trafo])
565 self.canvas.insert(self.svg.canvas)
567 def bbox(self):
568 return self._bbox
570 def processPS(self, file, writer, context, registry, bbox):
571 if not self.parsed:
572 raise ValueError("cannot output unparsed SVG to PostScript")
573 self.canvas.processPS(file, writer, context, registry, bbox)
575 def processPDF(self, file, writer, context, registry, bbox):
576 if not self.parsed:
577 raise ValueError("cannot output unparsed SVG to PDF")
578 self.canvas.processPDF(file, writer, context, registry, bbox)
580 def processSVG(self, svg, writer, context, registry, bbox):
581 if self.parsed:
582 self.canvas.processSVG(svg, writer, context, registry, bbox)
583 else:
584 t = self.trafo * self.svg.trafo
585 attrs = {"fill": "black"}
586 t.processSVGattrs(attrs, writer, context, registry)
587 svg.startSVGElement("g", attrs)
588 parser = xml.sax.make_parser()
589 parser.setContentHandler(svg)
590 parser.setFeature(xml.sax.handler.feature_namespaces, True)
591 parser.setFeature(xml.sax.handler.feature_external_ges, False)
592 parser.setFeature(xml.sax.handler.feature_external_pes, False)
593 svg.passthrough = True
594 with open(self.filename, "rb") as f:
595 parser.parse(f)
596 svg.passthrough = False
597 svg.endSVGElement("g")
600 class svgfile(svgfile_pt):
602 def __init__(self, x, y, filename, width=None, height=None, *args, **kwargs):
603 x_pt = unit.topt(x)
604 y_pt = unit.topt(y)
605 if width is not None:
606 width_pt = unit.topt(width)
607 else:
608 width_pt = None
609 if height is not None:
610 height_pt = unit.topt(height)
611 else:
612 height_pt = None
613 super().__init__(x_pt, y_pt, filename, width_pt, height_pt, *args, **kwargs)