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
34 if rx
== 0 or ry
== 0:
35 return path
.line_pt(x1
, y1
, x2
, y2
)
42 cos_phi
= math
.cos(math
.radians(phi
))
43 sin_phi
= math
.sin(math
.radians(phi
))
46 x1prim
= cos_phi
* dx
+ sin_phi
* dy
47 y1prim
= -sin_phi
* dx
+ cos_phi
* dy
50 Lambda
= (x1prim
/rx
)**2 + (y1prim
/ry
)**2
52 Lambda_sqrt
= math
.sqrt(Lambda
)
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
61 cxprim
= c
* rx
* y1prim
/ ry
62 cyprim
= -c
* ry
* x1prim
/ rx
65 cx
= cos_phi
* cxprim
- sin_phi
* cyprim
+ dx
66 cy
= sin_phi
* cxprim
+ cos_phi
* cyprim
+ dy
69 theta1
= math
.atan2((y1prim
- cyprim
)/ry
, (x1prim
- cxprim
)/rx
)
70 theta2
= math
.atan2((-y1prim
- cyprim
)/ry
, (-x1prim
- cxprim
)/rx
)
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
))
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
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
,
105 def toFloat(self
, arg
, relative
=None, single
=False, units
=True):
106 match
= _svgFloatPattern
.match(arg
)
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
116 raise svgValueError("missing support for relative coordinates")
118 value
*= self
.units
[match
.group("unit")]
120 if match
.end() < len(arg
):
121 raise svgValueError("could not match single float for '%s'" % arg
)
123 return value
, arg
[match
.end():]
125 def toFloats(self
, args
, units
=True):
127 float, args
= self
.toFloat(args
, units
=units
)
131 class svgHandler(svgBaseHandler
):
133 def __init__(self
, resolution
):
134 super().__init
__(resolution
)
137 self
.fill
= color
.grey
.black
139 def toBool(self
, arg
):
140 match
= _svgBoolPattern
.match(arg
)
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
148 for match
in _svgPathPattern
.finditer(svgPath
):
149 cmd
= match
.group("cmd")
150 args
= match
.group("args")
153 args
= self
.toFloats(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
))
162 p
.append(path
.moveto_pt(x
, y
) if cmd
.isupper() or not p
else
163 path
.rmoveto_pt(x
, y
))
166 x
, y
= p
.atend_pt() if cmd
.isupper() else (0, 0)
172 p
.append(path
.lineto_pt(x
, y
) if cmd
.isupper() else
173 path
.rlineto_pt(x
, y
))
177 x1
, y1
, x2
, y2
, x3
, y3
, *args
= args
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
189 x0
, y0
= p
.atend_pt()
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
))
196 x0
, y0
= p
.atend_pt()
198 xq
, yq
, x3
, y3
, *args
= args
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
)
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
))
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()
235 p
.join(endpointarc(x1
, y1
, x2
, y2
, fA
, fS
, rx
, ry
, phi
))
238 p
.append(path
.closepath())
239 except svgValueError
:
243 def toTrafo(self
, svgTrafo
):
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")
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
))
260 assert len(args
) == 2
261 t
= t
.translated_pt(args
[0], args
[1])
263 args
= list(self
.toFloats(args
, units
=False))
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)
271 b
, args
= self
.toFloat(args
)
272 c
= self
.toFloat(args
, single
=True)
275 t
= t
.rotated_pt(a
, b
, c
)
277 t
= t
* trafo
.trafo_pt(((1, math
.tan(self
.toFloat(args
, units
=False, single
=True)*math
.pi
/180)), (0, 1)))
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)))
283 def toColor(self
, name
, inherit
):
284 if name
== "currentColor":
286 if name
== "inherit":
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())
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())
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]
355 return self
.toFloat(attributes
[None, localname
])[0]
359 def pathAttrs(default
=_marker
):
360 if default
is not _marker
:
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),
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"]])
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
))
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:
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
)
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"))
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)
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:
459 p
.append(path
.lineto_pt(x
, y
))
460 if localname
== "polygon":
461 p
.append(path
.closepath())
463 assert localname
== "path"
464 p
= self
.toPath(attributes
[None, "d"])
467 fill
= self
.toColor(attributes
.get((None, "fill"), "inherit"), self
.fill
)
469 attrs
.append(deco
.filled([fill
]))
470 stroke
= self
.toColor(attributes
.get((None, "stroke"), "inherit"), self
.stroke
)
472 attrs
.append(deco
.stroked([stroke
]))
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":
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)
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
511 self
.resolution
= resolution
514 self
.svg
= svgHandler(resolution
)
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)
523 with
open(filename
, "rb") as f
:
527 with
open(filename
, "rb") as f
:
529 except svgBboxDoneException
:
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()
545 width_pt
= height_pt
* svgwidth_pt
/ svgheight_pt
547 width_pt
= ratio
* height_pt
548 elif height_pt
is None:
550 height_pt
= width_pt
* svgheight_pt
/ svgwidth_pt
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
)
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
)
564 self
.canvas
= canvas
.canvas([self
.trafo
])
565 self
.canvas
.insert(self
.svg
.canvas
)
570 def processPS(self
, file, writer
, context
, registry
, bbox
):
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
):
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
):
582 self
.canvas
.processSVG(svg
, writer
, context
, registry
, bbox
)
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
:
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
):
605 if width
is not None:
606 width_pt
= unit
.topt(width
)
609 if height
is not None:
610 height_pt
= unit
.topt(height
)
613 super().__init
__(x_pt
, y_pt
, filename
, width_pt
, height_pt
, *args
, **kwargs
)