Coverage for lib/lottie/parsers/svg/importer.py: 50%
978 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-20 16:17 +0100
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-20 16:17 +0100
1import re
2import math
3import colorsys
4from xml.etree import ElementTree
5from ... import objects
6from ...nvector import NVector
7from .svgdata import color_table, css_atrrs
8from .handler import SvgHandler, NameMode
9from ...utils.ellipse import Ellipse
10from ...utils.transform import TransformMatrix
11from ...utils.color import Color
13try:
14 from ...utils import font
15 has_font = True
16except ImportError:
17 has_font = False
19nocolor = {"none"}
22class SvgGradientCoord:
23 def __init__(self, name, comp, value, percent):
24 self.name = name
25 self.comp = comp
26 self.value = value
27 self.percent = percent
29 def to_value(self, bbox, default=None):
30 if self.value is None:
31 return default
33 if not self.percent:
34 return self.value
36 if self.comp == "w":
37 return (bbox.x2 - bbox.x1) * self.value
39 if self.comp == "x":
40 return bbox.x1 + (bbox.x2 - bbox.x1) * self.value
42 return bbox.y1 + (bbox.y2 - bbox.y1) * self.value
44 def parse(self, attr, default_percent):
45 if attr is None:
46 return
47 if attr.endswith("%"):
48 self.percent = True
49 self.value = float(attr[:-1])/100
50 else:
51 self.percent = default_percent
52 self.value = float(attr)
55class SvgGradient:
56 def __init__(self):
57 self.colors = []
58 self.coords = []
59 self.matrix = TransformMatrix()
61 def add_color(self, offset, color):
62 self.colors.append((offset, color[:4]))
64 def to_lottie(self, gradient_shape, shape, time=0):
65 """!
66 @param gradient_shape Should be a GradientFill or GradientStroke
67 @param shape ShapeElement to apply the gradient to
68 @param time Time to fetch properties from @p shape
70 """
71 for off, col in self.colors:
72 gradient_shape.colors.add_color(off, col)
74 def add_coord(self, value):
75 setattr(self, value.name, value)
76 self.coords.append(value)
78 def parse_attrs(self, attrib):
79 relunits = attrib.get("gradientUnits", "") != "userSpaceOnUse"
80 for c in self.coords:
81 c.parse(attrib.get(c.name, None), relunits)
84class SvgLinearGradient(SvgGradient):
85 def __init__(self):
86 super().__init__()
87 self.add_coord(SvgGradientCoord("x1", "x", 0, True))
88 self.add_coord(SvgGradientCoord("y1", "y", 0, True))
89 self.add_coord(SvgGradientCoord("x2", "x", 1, True))
90 self.add_coord(SvgGradientCoord("y2", "y", 0, True))
92 def to_lottie(self, gradient_shape, shape, time=0):
93 bbox = shape.bounding_box(time)
94 gradient_shape.start_point.value = self.matrix.apply(NVector(
95 self.x1.to_value(bbox),
96 self.y1.to_value(bbox),
97 ))
98 gradient_shape.end_point.value = self.matrix.apply(NVector(
99 self.x2.to_value(bbox),
100 self.y2.to_value(bbox),
101 ))
102 gradient_shape.gradient_type = objects.GradientType.Linear
104 super().to_lottie(gradient_shape, shape, time)
107class SvgRadialGradient(SvgGradient):
108 def __init__(self):
109 super().__init__()
110 self.add_coord(SvgGradientCoord("cx", "x", 0.5, True))
111 self.add_coord(SvgGradientCoord("cy", "y", 0.5, True))
112 self.add_coord(SvgGradientCoord("fx", "x", None, True))
113 self.add_coord(SvgGradientCoord("fy", "y", None, True))
114 self.add_coord(SvgGradientCoord("r", "w", 0.5, True))
116 def to_lottie(self, gradient_shape, shape, time=0):
117 bbox = shape.bounding_box(time)
118 cx = self.cx.to_value(bbox)
119 cy = self.cy.to_value(bbox)
120 gradient_shape.start_point.value = self.matrix.apply(NVector(cx, cy))
121 r = self.r.to_value(bbox)
122 gradient_shape.end_point.value = self.matrix.apply(NVector(cx+r, cy))
124 fx = self.fx.to_value(bbox, cx) - cx
125 fy = self.fy.to_value(bbox, cy) - cy
126 gradient_shape.highlight_angle.value = math.atan2(fy, fx) * 180 / math.pi
127 gradient_shape.highlight_length.value = math.hypot(fx, fy)
129 gradient_shape.gradient_type = objects.GradientType.Radial
131 super().to_lottie(gradient_shape, shape, time)
134def parse_color(color, current_color=Color(0, 0, 0, 1)):
135 """!
136 Parses CSS colors
137 @see https://www.w3.org/wiki/CSS/Properties/color
138 """
139 # #112233
140 if re.match(r"^#[0-9a-fA-F]{6}$", color):
141 return Color(int(color[1:3], 16) / 0xff, int(color[3:5], 16) / 0xff, int(color[5:7], 16) / 0xff, 1)
142 # #fff
143 if re.match(r"^#[0-9a-fA-F]{3}$", color):
144 return Color(int(color[1], 16) / 0xf, int(color[2], 16) / 0xf, int(color[3], 16) / 0xf, 1)
145 # #112233aa
146 if re.match(r"^#[0-9a-fA-F]{8}$", color): 146 ↛ 147line 146 didn't jump to line 147, because the condition on line 146 was never true
147 return Color(
148 int(color[1:3], 16) / 0xff,
149 int(color[3:5], 16) / 0xff,
150 int(color[5:7], 16) / 0xff,
151 int(color[7:9], 16) / 0xff
152 )
153 # #fffa
154 if re.match(r"^#[0-9a-fA-F]{4}$", color): 154 ↛ 155line 154 didn't jump to line 155, because the condition on line 154 was never true
155 return Color(int(color[1], 16) / 0xf, int(color[2], 16) / 0xf, int(color[3], 16) / 0xf, int(color[4], 16) / 0xf)
156 # rgba(123, 123, 123, 0.7)
157 match = re.match(r"^rgba\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9.eE]+)\s*\)$", color)
158 if match:
159 return Color(int(match[1])/255, int(match[2])/255, int(match[3])/255, float(match[4]))
160 # rgb(123, 123, 123)
161 match = re.match(r"^rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)$", color)
162 if match:
163 return Color(int(match[1])/255, int(match[2])/255, int(match[3])/255, 1)
164 # rgb(60%, 30%, 20%)
165 match = re.match(r"^rgb\s*\(\s*([0-9.eE]+)%\s*,\s*([0-9.eE]+)%\s*,\s*([0-9.eE]+)%\s*\)$", color)
166 if match:
167 return Color(float(match[1])/100, float(match[2])/100, float(match[3])/100, 1)
168 # rgba(60%, 30%, 20%, 0.7)
169 match = re.match(r"^rgba\s*\(\s*([0-9.eE]+)%\s*,\s*([0-9.eE]+)%\s*,\s*([0-9.eE]+)%\s*,\s*([0-9.eE]+)\s*\)$", color)
170 if match:
171 return Color(float(match[1])/100, float(match[2])/100, float(match[3])/100, float(match[4]))
172 # transparent
173 if color == "transparent":
174 return Color(0, 0, 0, 0)
175 # hsl(60, 30%, 20%)
176 match = re.match(r"^hsl\s*\(\s*([0-9.eE]+)\s*,\s*([0-9.eE]+)%\s*,\s*([0-9.eE]+)%\s*\)$", color)
177 if match:
178 return Color(*(colorsys.hls_to_rgb(float(match[1])/360, float(match[3])/100, float(match[2])/100) + (1,)))
179 # hsla(60, 30%, 20%, 0.7)
180 match = re.match(r"^hsla\s*\(\s*([0-9.eE]+)\s*,\s*([0-9.eE]+)%\s*,\s*([0-9.eE]+)%\s*,\s*([0-9.eE]+)\s*\)$", color)
181 if match:
182 return Color(*(colorsys.hls_to_rgb(float(match[1])/360, float(match[3])/100, float(match[2])/100) + (float(match[4]),)))
183 # currentColor
184 if color in {"currentColor", "inherit"}:
185 return current_color.clone()
186 # red
187 return Color(*color_table[color])
190class SvgDefsParent:
191 def __init__(self):
192 self.items = {}
194 def insert(self, dummy, shape):
195 self.items[shape.name] = shape
197 def __getitem__(self, key):
198 return self.items[key]
200 def __setitem__(self, key, value):
201 self.items[key] = value
203 def __contains__(self, key):
204 return key in self.items
206 @property
207 def shapes(self):
208 return self
211class SvgParser(SvgHandler):
212 def __init__(self, name_mode=NameMode.Inkscape):
213 self.init_etree()
214 self.name_mode = name_mode
215 self.current_color = Color(0, 0, 0, 1)
216 self.gradients = {}
217 self.max_time = 0
218 self.defs = SvgDefsParent()
219 self.dpi = 96
221 def _get_name(self, element, inkscapequal):
222 if self.name_mode == NameMode.Inkscape: 222 ↛ 224line 222 didn't jump to line 224, because the condition on line 222 was never false
223 return element.attrib.get(inkscapequal, element.attrib.get("id"))
224 return self._get_id(element)
226 def _get_id(self, element):
227 if self.name_mode != NameMode.NoName:
228 return element.attrib.get("id")
229 return None
231 def _parse_viewbox(self, attrib):
232 return map(float, attrib.replace(",", " ").split())
234 def parse_etree(self, etree, layer_frames=0, *args, **kwargs):
235 animation = objects.Animation(*args, **kwargs)
236 self.animation = animation
237 self.max_time = 0
238 self.document = etree
240 svg = etree.getroot()
242 self._get_dpi(svg)
244 if "width" in svg.attrib and "height" in svg.attrib: 244 ↛ 245line 244 didn't jump to line 245, because the condition on line 244 was never true
245 animation.width = int(round(self._parse_unit(svg.attrib["width"])))
246 animation.height = int(round(self._parse_unit(svg.attrib["height"])))
247 else:
248 _, _, animation.width, animation.height = self._parse_viewbox(svg.attrib["viewBox"])
249 animation.name = self._get_name(svg, self.qualified("sodipodi", "docname"))
251 if layer_frames: 251 ↛ 252line 251 didn't jump to line 252, because the condition on line 251 was never true
252 for frame in svg:
253 if self.unqualified(frame.tag) == "g":
254 layer = objects.ShapeLayer()
255 layer.in_point = self.max_time
256 animation.add_layer(layer)
257 self._parseshape_g(frame, layer, {})
258 self.max_time += layer_frames
259 layer.out_point = self.max_time
260 else:
261 self._svg_to_layer(animation, svg)
263 if self.max_time: 263 ↛ 264line 263 didn't jump to line 264, because the condition on line 263 was never true
264 animation.out_point = self.max_time
266 self._fix_viewbox(svg, (layer for layer in animation.layers if not layer.parent_index)) 266 ↛ exitline 266 didn't run the generator expression on line 266
268 return animation
270 def etree_to_layer(self, animation, etree):
271 svg = etree.getroot()
272 self._get_dpi(svg)
273 layer = self._svg_to_layer(animation, svg)
274 self._fix_viewbox(svg, [layer])
275 return layer
277 def _get_dpi(self, svg):
278 self.dpi = float(svg.attrib.get(self.qualified("inkscape", "export-xdpi"), self.dpi))
280 def _svg_to_layer(self, animation, svg):
281 self.animation = animation
282 layer = objects.ShapeLayer()
283 animation.add_layer(layer)
284 self.parse_children(svg, layer, self.parse_style(svg, {}))
285 if self.max_time: 285 ↛ 286line 285 didn't jump to line 286, because the condition on line 285 was never true
286 for sublayer in layer.find_all(objects.Layer):
287 sublayer.out_point = self.max_time
288 return layer
290 def _fix_viewbox(self, svg, layers):
291 if "viewBox" in svg.attrib: 291 ↛ exitline 291 didn't return from function '_fix_viewbox', because the condition on line 291 was never false
292 vbx, vby, vbw, vbh = self._parse_viewbox(svg.attrib["viewBox"])
293 if vbx != 0 or vby != 0 or vbw != self.animation.width or vbh != self.animation.height: 293 ↛ 294line 293 didn't jump to line 294, because the condition on line 293 was never true
294 for layer in layers:
295 layer.transform.position.value = -NVector(vbx, vby)
296 layer.transform.scale.value = NVector(self.animation.width / vbw, self.animation.height / vbh) * 100
298 def _parse_unit(self, value):
299 if not isinstance(value, str):
300 return value
302 mult = 1
303 cmin = 2.54
304 if value.endswith("px"):
305 value = value[:-2]
306 elif value.endswith("vw"):
307 value = value[:-2]
308 mult = self.animation.width * 0.01
309 elif value.endswith("vh"):
310 value = value[:-2]
311 mult = self.animation.height * 0.01
312 elif value.endswith("vmin"):
313 value = value[:-4]
314 mult = min(self.animation.width, self.animation.height) * 0.01
315 elif value.endswith("vmax"):
316 value = value[:-4]
317 mult = max(self.animation.width, self.animation.height) * 0.01
318 elif value.endswith("in"):
319 value = value[:-2]
320 mult = self.dpi
321 elif value.endswith("pc"):
322 value = value[:-2]
323 mult = self.dpi / 6
324 elif value.endswith("pt"):
325 value = value[:-2]
326 mult = self.dpi / 72
327 elif value.endswith("cm"):
328 value = value[:-2]
329 mult = self.dpi / cmin
330 elif value.endswith("mm"):
331 value = value[:-2]
332 mult = self.dpi / cmin / 10
333 elif value.endswith("Q"):
334 value = value[:-1]
335 mult = self.dpi / cmin / 40
337 return float(value) * mult
339 def parse_color(self, color):
340 return parse_color(color, self.current_color)
342 def parse_transform(self, element, group, dest_trans):
343 bb = group.bounding_box()
344 if not bb.isnull(): 344 ↛ 359line 344 didn't jump to line 359, because the condition on line 344 was never false
345 itcx = self.qualified("inkscape", "transform-center-x")
346 if itcx in element.attrib:
347 cx = float(element.attrib[itcx])
348 cy = float(element.attrib[self.qualified("inkscape", "transform-center-y")])
349 bbx, bby = bb.center()
350 cx += bbx
351 cy = bby - cy
352 dest_trans.anchor_point.value = NVector(cx, cy)
353 dest_trans.position.value = NVector(cx, cy)
354 #else:
355 #c = bb.center()
356 #dest_trans.anchor_point.value = c
357 #dest_trans.position.value = c.clone()
359 if "transform" not in element.attrib:
360 return
362 matrix = TransformMatrix()
363 read_matrix = False
365 for t in re.finditer(r"([a-zA-Z]+)\s*\(([^\)]*)\)", element.attrib["transform"]):
366 name = t[1]
367 params = list(map(float, t[2].strip().replace(",", " ").split()))
368 if name == "translate":
369 dest_trans.position.value += NVector(
370 params[0],
371 (params[1] if len(params) > 1 else 0),
372 )
373 elif name == "scale":
374 xfac = params[0]
375 dest_trans.scale.value[0] = (dest_trans.scale.value[0] / 100 * xfac) * 100
376 yfac = params[1] if len(params) > 1 else xfac
377 dest_trans.scale.value[1] = (dest_trans.scale.value[1] / 100 * yfac) * 100
378 elif name == "rotate":
379 ang = params[0]
380 x = y = 0
381 if len(params) > 2:
382 x = params[1]
383 y = params[2]
384 ap = NVector(x, y)
385 dap = ap - dest_trans.position.value
386 dest_trans.position.value += dap
387 dest_trans.anchor_point.value += dap
388 dest_trans.rotation.value = ang
389 else:
390 read_matrix = True
391 self._apply_transform_element_to_matrix(matrix, t)
393 if read_matrix:
394 dest_trans.position.value -= dest_trans.anchor_point.value
395 dest_trans.anchor_point.value = NVector(0, 0)
396 trans = matrix.extract_transform()
397 dest_trans.skew_axis.value = math.degrees(trans["skew_axis"])
398 dest_trans.skew.value = -math.degrees(trans["skew_angle"])
399 dest_trans.position.value += trans["translation"]
400 dest_trans.rotation.value -= math.degrees(trans["angle"])
401 dest_trans.scale.value *= trans["scale"]
403 def parse_style(self, element, parent_style):
404 style = parent_style.copy()
405 for att in css_atrrs & set(element.attrib.keys()):
406 if att in element.attrib: 406 ↛ 405line 406 didn't jump to line 405, because the condition on line 406 was never false
407 style[att] = element.attrib[att]
408 if "style" in element.attrib:
409 style.update(**dict(map(
410 lambda x: map(lambda y: y.strip(), x.split(":")),
411 filter(bool, element.attrib["style"].split(";"))
412 )))
413 return style
415 def apply_common_style(self, style, transform):
416 opacity = float(style.get("opacity", 1))
417 transform.opacity.value = opacity * 100
419 def apply_visibility(self, style, object):
420 if style.get("display", "inline") == "none" or style.get("visibility", "visible") == "hidden":
421 object.hidden = True
423 def add_shapes(self, element, shapes, shape_parent, parent_style):
424 style = self.parse_style(element, parent_style)
426 group = objects.Group()
427 self.apply_common_style(style, group.transform)
428 self.apply_visibility(style, group)
429 group.name = self._get_name(element, self.qualified("inkscape", "label"))
431 shape_parent.shapes.insert(0, group)
432 for shape in shapes:
433 group.add_shape(shape)
435 self._add_style_shapes(style, group)
437 self.parse_transform(element, group, group.transform)
439 return group
441 def _add_style_shapes(self, style, group):
442 stroke_color = style.get("stroke", "none")
443 if stroke_color not in nocolor: 443 ↛ 487line 443 didn't jump to line 487, because the condition on line 443 was never false
444 if stroke_color.startswith("url"): 444 ↛ 445line 444 didn't jump to line 445, because the condition on line 444 was never true
445 stroke = self.get_color_url(stroke_color, objects.GradientStroke, group)
446 opacity = 1
447 else:
448 stroke = objects.Stroke()
449 color = self.parse_color(stroke_color)
450 stroke.color.value = color
451 opacity = color[3]
452 group.add_shape(stroke)
454 stroke.opacity.value = opacity * float(style.get("stroke-opacity", 1)) * 100
456 stroke.width.value = self._parse_unit(style.get("stroke-width", 1))
458 linecap = style.get("stroke-linecap")
459 if linecap == "round": 459 ↛ 460line 459 didn't jump to line 460, because the condition on line 459 was never true
460 stroke.line_cap = objects.shapes.LineCap.Round
461 elif linecap == "butt": 461 ↛ 462line 461 didn't jump to line 462, because the condition on line 461 was never true
462 stroke.line_cap = objects.shapes.LineCap.Butt
463 elif linecap == "square": 463 ↛ 464line 463 didn't jump to line 464, because the condition on line 463 was never true
464 stroke.line_cap = objects.shapes.LineCap.Square
466 linejoin = style.get("stroke-linejoin")
467 if linejoin == "round": 467 ↛ 468line 467 didn't jump to line 468, because the condition on line 467 was never true
468 stroke.line_join = objects.shapes.LineJoin.Round
469 elif linejoin == "bevel": 469 ↛ 470line 469 didn't jump to line 470, because the condition on line 469 was never true
470 stroke.line_join = objects.shapes.LineJoin.Bevel
471 elif linejoin in {"miter", "arcs", "miter-clip"}: 471 ↛ 472line 471 didn't jump to line 472, because the condition on line 471 was never true
472 stroke.line_join = objects.shapes.LineJoin.Miter
474 stroke.miter_limit = self._parse_unit(style.get("stroke-miterlimit", 0))
476 dash_array = style.get("stroke-dasharray")
477 if dash_array and dash_array != "none": 477 ↛ 478line 477 didn't jump to line 478, because the condition on line 477 was never true
478 values = list(map(self._parse_unit, dash_array.replace(",", " ").split()))
479 if len(values) % 2:
480 values += values
482 stroke.dashes = []
483 for i in range(0, len(values), 2):
484 stroke.dashes.append(objects.StrokeDash(values[i], objects.StrokeDashType.Dash))
485 stroke.dashes.append(objects.StrokeDash(values[i+1], objects.StrokeDashType.Gap))
487 fill_color = style.get("fill", "inherit")
488 if fill_color not in nocolor: 488 ↛ 489line 488 didn't jump to line 489, because the condition on line 488 was never true
489 if fill_color.startswith("url"):
490 fill = self.get_color_url(fill_color, objects.GradientFill, group)
491 opacity = 1
492 else:
493 color = self.parse_color(fill_color)
494 fill = objects.Fill(color)
495 opacity = color[3]
496 opacity *= float(style.get("fill-opacity", 1))
497 fill.opacity.value = opacity * 100
499 if style.get("fill-rule", "") == "evenodd":
500 fill.fill_rule = objects.FillRule.EvenOdd
502 group.add_shape(fill)
504 def _parseshape_use(self, element, shape_parent, parent_style):
505 link = element.attrib.get(self.qualified("xlink", "href"))
506 if link and link.startswith("#"):
507 id = link[1:]
508 base_element = self.document.find(".//*[@id='%s']" % id)
509 use_style = self.parse_style(element, parent_style)
510 used = objects.Group()
511 shape_parent.add_shape(used)
512 used.name = "use"
513 used.transform.position.value.x = float(element.attrib.get("x", 0))
514 used.transform.position.value.y = float(element.attrib.get("y", 0))
515 self.parse_transform(element, used, used.transform)
516 self.parse_shape(base_element, used, use_style)
517 return used
519 def _parseshape_g(self, element, shape_parent, parent_style):
520 group = objects.Group()
521 shape_parent.shapes.insert(0, group)
522 style = self.parse_style(element, parent_style)
523 self.apply_common_style(style, group.transform)
524 self.apply_visibility(style, group)
525 group.name = self._get_name(element, self.qualified("inkscape", "label"))
526 self.parse_children(element, group, style)
527 self.parse_transform(element, group, group.transform)
528 if group.hidden: # Lottie web doesn't seem to support .hd
529 group.transform.opacity.value = 0
530 return group
532 def _parseshape_ellipse(self, element, shape_parent, parent_style):
533 ellipse = objects.Ellipse()
534 ellipse.position.value = NVector(
535 self._parse_unit(element.attrib["cx"]),
536 self._parse_unit(element.attrib["cy"])
537 )
538 ellipse.size.value = NVector(
539 self._parse_unit(element.attrib["rx"]) * 2,
540 self._parse_unit(element.attrib["ry"]) * 2
541 )
542 self.add_shapes(element, [ellipse], shape_parent, parent_style)
543 return ellipse
545 def _parseshape_anim_ellipse(self, ellipse, element, animations):
546 self._merge_animations(element, animations, "cx", "cy", "position")
547 self._merge_animations(element, animations, "rx", "ry", "size", lambda x, y: NVector(x, y) * 2)
548 self._apply_animations(ellipse.position, "position", animations)
549 self._apply_animations(ellipse.size, "size", animations)
551 def _parseshape_circle(self, element, shape_parent, parent_style):
552 ellipse = objects.Ellipse()
553 ellipse.position.value = NVector(
554 self._parse_unit(element.attrib["cx"]),
555 self._parse_unit(element.attrib["cy"])
556 )
557 r = self._parse_unit(element.attrib["r"]) * 2
558 ellipse.size.value = NVector(r, r)
559 self.add_shapes(element, [ellipse], shape_parent, parent_style)
560 return ellipse
562 def _parseshape_anim_circle(self, ellipse, element, animations):
563 self._merge_animations(element, animations, "cx", "cy", "position")
564 self._apply_animations(ellipse.position, "position", animations)
565 self._apply_animations(ellipse.size, "r", animations, lambda r: NVector(r, r) * 2)
567 def _parseshape_rect(self, element, shape_parent, parent_style):
568 rect = objects.Rect()
569 w = self._parse_unit(element.attrib.get("width", 0))
570 h = self._parse_unit(element.attrib.get("height", 0))
571 rect.position.value = NVector(
572 self._parse_unit(element.attrib.get("x", 0)) + w / 2,
573 self._parse_unit(element.attrib.get("y", 0)) + h / 2
574 )
575 rect.size.value = NVector(w, h)
576 rx = self._parse_unit(element.attrib.get("rx", 0))
577 ry = self._parse_unit(element.attrib.get("ry", 0))
578 rect.rounded.value = (rx + ry) / 2
579 self.add_shapes(element, [rect], shape_parent, parent_style)
580 return rect
582 def _parseshape_anim_rect(self, rect, element, animations):
583 self._merge_animations(element, animations, "width", "height", "size", lambda x, y: NVector(x, y))
584 self._apply_animations(rect.size, "size", animations)
585 self._merge_animations(element, animations, "x", "y", "position")
586 self._merge_animations(element, animations, "position", "size", "position", lambda p, s: p + s / 2)
587 self._apply_animations(rect.position, "position", animations)
588 self._merge_animations(element, animations, "rx", "ry", "rounded", lambda x, y: (x + y) / 2)
589 self._apply_animations(rect.rounded, "rounded", animations)
591 def _parseshape_line(self, element, shape_parent, parent_style):
592 line = objects.Path()
593 line.shape.value.add_point(NVector(
594 self._parse_unit(element.attrib["x1"]),
595 self._parse_unit(element.attrib["y1"])
596 ))
597 line.shape.value.add_point(NVector(
598 self._parse_unit(element.attrib["x2"]),
599 self._parse_unit(element.attrib["y2"])
600 ))
601 return self.add_shapes(element, [line], shape_parent, parent_style)
603 def _parseshape_anim_line(self, group, element, animations):
604 line = group.shapes[0]
606 self._merge_animations(element, animations, "x1", "y1", "p1")
607 self._merge_animations(element, animations, "x2", "y2", "p2")
609 def combine(p1, p2):
610 shape = objects.Bezier()
611 shape.add_point(p1)
612 shape.add_point(p2)
613 return shape
615 self._merge_animations(element, animations, "p1", "p2", "points", combine)
616 self._apply_animations(line.shape, "points", animations)
618 def _handle_poly(self, element):
619 line = objects.Path()
620 coords = list(map(float, element.attrib["points"].replace(",", " ").split()))
621 for i in range(0, len(coords), 2):
622 line.shape.value.add_point(NVector(*coords[i:i+2]))
623 return line
625 def _parseshape_polyline(self, element, shape_parent, parent_style):
626 line = self._handle_poly(element)
627 return self.add_shapes(element, [line], shape_parent, parent_style)
629 def _parseshape_polygon(self, element, shape_parent, parent_style):
630 line = self._handle_poly(element)
631 line.shape.value.close()
632 return self.add_shapes(element, [line], shape_parent, parent_style)
634 def _parseshape_path(self, element, shape_parent, parent_style):
635 d_parser = PathDParser(element.attrib.get("d", ""))
636 d_parser.parse()
637 paths = []
638 for path in d_parser.paths:
639 p = objects.Path()
640 p.shape.value = path
641 paths.append(p)
642 #if len(d_parser.paths) > 1:
643 #paths.append(objects.shapes.Merge())
644 return self.add_shapes(element, paths, shape_parent, parent_style)
646 def parse_children(self, element, shape_parent, parent_style):
647 for child in element:
648 tag = self.unqualified(child.tag)
649 if not self.parse_shape(child, shape_parent, parent_style): 649 ↛ 650line 649 didn't jump to line 650, because the condition on line 649 was never true
650 handler = getattr(self, "_parse_" + tag, None)
651 if handler:
652 handler(child)
654 def parse_shape(self, element, shape_parent, parent_style):
655 handler = getattr(self, "_parseshape_" + self.unqualified(element.tag), None)
656 if handler: 656 ↛ 662line 656 didn't jump to line 662, because the condition on line 656 was never false
657 out = handler(element, shape_parent, parent_style)
658 self.parse_animations(out, element)
659 if element.attrib.get("id"): 659 ↛ 661line 659 didn't jump to line 661, because the condition on line 659 was never false
660 self.defs.items[element.attrib["id"]] = out
661 return out
662 return None
664 def _parse_defs(self, element):
665 self.parse_children(element, self.defs, {})
667 def _apply_transform_element_to_matrix(self, matrix, t):
668 name = t[1]
669 params = list(map(float, t[2].strip().replace(",", " ").split()))
670 if name == "translate": 670 ↛ 671line 670 didn't jump to line 671, because the condition on line 670 was never true
671 matrix.translate(
672 params[0],
673 (params[1] if len(params) > 1 else 0),
674 )
675 elif name == "scale": 675 ↛ 676line 675 didn't jump to line 676, because the condition on line 675 was never true
676 xfac = params[0]
677 yfac = params[1] if len(params) > 1 else xfac
678 matrix.scale(xfac, yfac)
679 elif name == "rotate": 679 ↛ 680line 679 didn't jump to line 680, because the condition on line 679 was never true
680 ang = params[0]
681 x = y = 0
682 if len(params) > 2:
683 x = params[1]
684 y = params[2]
685 matrix.translate(-x, -y)
686 matrix.rotate(math.radians(ang))
687 matrix.translate(x, y)
688 else:
689 matrix.rotate(math.radians(ang))
690 elif name == "skewX": 690 ↛ 691line 690 didn't jump to line 691, because the condition on line 690 was never true
691 matrix.skew(math.radians(params[0]), 0)
692 elif name == "skewY": 692 ↛ 693line 692 didn't jump to line 693, because the condition on line 692 was never true
693 matrix.skew(0, math.radians(params[0]))
694 elif name == "matrix": 694 ↛ exitline 694 didn't return from function '_apply_transform_element_to_matrix', because the condition on line 694 was never false
695 m = TransformMatrix()
696 m.a, m.b, m.c, m.d, m.tx, m.ty = params
697 matrix *= m
699 def _transform_to_matrix(self, transform):
700 matrix = TransformMatrix()
702 for t in re.finditer(r"([a-zA-Z]+)\s*\(([^\)]*)\)", transform):
703 self._apply_transform_element_to_matrix(matrix, t)
705 return matrix
707 def _gradient(self, element, grad):
708 grad.matrix = self._transform_to_matrix(element.attrib.get("gradientTransform", ""))
710 id = element.attrib["id"]
711 if id in self.gradients:
712 grad.colors = self.gradients[id].colors
713 grad.parse_attrs(element.attrib)
714 href = element.attrib.get(self.qualified("xlink", "href"))
715 if href:
716 srcid = href.strip("#")
717 if srcid in self.gradients:
718 src = self.gradients[srcid]
719 else:
720 src = grad.__class__()
721 self.gradients[srcid] = src
722 grad.colors = src.colors
724 for stop in element.findall("./%s" % self.qualified("svg", "stop")):
725 offset = stop.attrib.get("offset", "0")
726 off = float(offset.strip("%"))
727 if offset.endswith("%"):
728 off /= 100
729 style = self.parse_style(stop, {})
730 color = self.parse_color(style.get("stop-color", "black"))
731 if "stop-opacity" in style:
732 color[3] = float(style["stop-opacity"])
733 grad.add_color(off, color)
734 self.gradients[id] = grad
736 def _parse_linearGradient(self, element):
737 self._gradient(element, SvgLinearGradient())
739 def _parse_radialGradient(self, element):
740 self._gradient(element, SvgRadialGradient())
742 def get_color_url(self, color, gradientclass, shape):
743 match = re.match(r"""url\(['"]?#([^)'"]+)['"]?\)""", color)
744 if not match:
745 return None
746 id = match[1]
747 if id not in self.gradients:
748 return None
749 grad = self.gradients[id]
750 outgrad = gradientclass()
751 grad.to_lottie(outgrad, shape)
752 if self.name_mode != NameMode.NoName:
753 grad.name = id
754 return outgrad
756 ## @todo Parse single font property, fallback family etc
757 def _parse_text_style(self, style, font_style=None):
758 if "font-family" in style:
759 font_style.query.family(style["font-family"].strip("'\""))
761 if "font-style" in style:
762 if style["font-style"] == "oblique":
763 font_style.query.custom("slant", 110)
764 elif style["font-style"] == "italic":
765 font_style.query.custom("slant", 100)
767 if "font-weight" in style:
768 if style["font-weight"] in {"bold", "bolder"}:
769 font_style.query.weight(200)
770 elif style["font-weight"] == "lighter":
771 font_style.query.weight(50)
772 elif style["font-weight"].isdigit():
773 font_style.query.css_weight(int(style["font-weight"]))
775 if "font-size" in style:
776 fz = style["font-size"]
777 fz_names = {
778 "xx-small": 8,
779 "x-small": 16,
780 "small": 32,
781 "medium": 64,
782 "large": 128,
783 "x-large": 256,
784 "xx-large": 512,
785 }
786 if fz in fz_names:
787 font_style.size = fz_names[fz]
788 elif fz == "smaller":
789 font_style.size /= 2
790 elif fz == "larger":
791 font_style.size *= 2
792 elif fz.endswith("px"):
793 font_style.size = float(fz[:-2])
794 elif fz.isnumeric():
795 font_style.size = float(fz)
797 if "text-align" in style:
798 ta = style["text-align"]
799 if ta in ("left", "start"):
800 font_style.justify = font.TextJustify.Left
801 elif ta == "center":
802 font_style.justify = font.TextJustify.Center
803 elif ta in ("right", "end"):
804 font_style.justify = font.TextJustify.Right
806 def _parse_text_elem(self, element, style, group, parent_style, font_style):
807 self._parse_text_style(style, font_style)
809 if "x" in element.attrib or "y" in element.attrib:
810 font_style.position = NVector(
811 float(element.attrib["x"]),
812 float(element.attrib["y"]),
813 )
815 childpos = NVector(0, font_style.position.y)
817 if element.text:
818 fs = font.FontShape(element.text, font_style)
819 fs.refresh()
820 group.add_shape(fs)
821 childpos.x = fs.wrapped.next_x
823 for child in element:
824 if child.tag == self.qualified("svg", "tspan"):
825 child_style = font_style.clone()
826 child_style.position = childpos.clone()
827 fs = self._parseshape_text(child, group, parent_style, child_style)
828 childpos.x = fs.next_x
829 if child.tail:
830 child_style = font_style.clone()
831 child_style.position = childpos.clone()
832 fs = font.FontShape(child.tail, child_style)
833 fs.refresh()
834 group.add_shape(fs)
835 childpos.x = fs.wrapped.next_x
837 group.next_x = childpos.x
839 def _parseshape_text(self, element, shape_parent, parent_style, font_style=None):
840 group = objects.Group()
842 style = self.parse_style(element, parent_style)
843 self.apply_common_style(style, group.transform)
844 self.apply_visibility(style, group)
845 group.name = self._get_id(element)
847 if has_font:
848 if font_style is None:
849 font_style = font.FontStyle("", 64)
850 self._parse_text_elem(element, style, group, style, font_style)
852 style.setdefault("fill", "none")
853 self._add_style_shapes(style, group)
855 ## @todo text-anchor when it doesn't match text-align
856 #if element.tag == self.qualified("svg", "text"):
857 #dx = 0
858 #dy = 0
860 #ta = style.get("text-anchor", style.get("text-align", ""))
861 #if ta == "middle":
862 #dx -= group.bounding_box().width / 2
863 #elif ta == "end":
864 #dx -= group.bounding_box().width
866 #if dx or dy:
867 #ng = objects.Group()
868 #ng.add_shape(group)
869 #group.transform.position.value.x += dx
870 #group.transform.position.value.y += dy
871 #group = ng
873 shape_parent.shapes.insert(0, group)
874 self.parse_transform(element, group, group.transform)
875 return group
877 def parse_animations(self, lottie, element):
878 animations = {}
879 for child in element: 879 ↛ 880line 879 didn't jump to line 880, because the loop on line 879 never started
880 if self.unqualified(child.tag) == "animate":
881 att = child.attrib["attributeName"]
883 from_val = child.attrib["from"]
884 if att == "d":
885 ## @todo
886 continue
887 else:
888 from_val = float(from_val)
889 if "to" in child.attrib:
890 to_val = float(child.attrib["to"])
891 elif "by" in child.attrib:
892 to_val = float(child.attrib["by"]) + from_val
894 begin = self.parse_animation_time(child.attrib.get("begin", 0)) or 0
895 if "dur" in child.attrib:
896 end = (self.parse_animation_time(child.attrib["dur"]) or 0) + begin
897 elif "end" in child.attrib:
898 end = self.parse_animation_time(child.attrib["dur"]) or 0
899 else:
900 continue
902 if att not in animations:
903 animations[att] = {}
904 animations[att][begin] = from_val
905 animations[att][end] = to_val
906 if self.max_time < end:
907 self.max_time = end
909 tag = self.unqualified(element.tag)
910 handler = getattr(self, "_parseshape_anim_" + tag, None)
911 if handler: 911 ↛ 912line 911 didn't jump to line 912, because the condition on line 911 was never true
912 handler(lottie, element, animations)
914 def parse_animation_time(self, value):
915 """!
916 @see https://developer.mozilla.org/en-US/docs/Web/SVG/Content_type#Clock-value
917 """
918 if not value:
919 return None
920 try:
921 seconds = 0
922 if ":" in value:
923 mult = 1
924 for elem in reversed(value.split(":")):
925 seconds += float(elem) * mult
926 mult *= 60
927 elif value.endswith("s"):
928 seconds = float(value[:-1])
929 elif value.endswith("ms"):
930 seconds = float(value[:-2]) / 1000
931 elif value.endswith("min"):
932 seconds = float(value[:-3]) * 60
933 elif value.endswith("h"):
934 seconds = float(value[:-1]) * 60 * 60
935 else:
936 seconds = float(value)
937 return seconds * self.animation.frame_rate
938 except ValueError:
939 pass
940 return None
942 def _merge_animations(self, element, animations, val1, val2, dest, merge=NVector):
943 if val1 not in animations and val2 not in animations:
944 return
946 dict1 = list(sorted(animations.pop(val1, {}).items()))
947 dict2 = list(sorted(animations.pop(val2, {}).items()))
949 x = float(element.attrib[val1])
950 y = float(element.attrib[val2])
951 values = {}
952 while dict1 or dict2:
953 if not dict1 or (dict2 and dict1[0][0] > dict2[0][0]):
954 t, y = dict2.pop(0)
955 elif not dict2 or dict1[0][0] < dict2[0][0]:
956 t, x = dict1.pop(0)
957 else:
958 t, x = dict1.pop(0)
959 t, y = dict2.pop(0)
961 values[t] = merge(x, y)
963 animations[dest] = values
965 def _apply_animations(self, animatable, name, animations, transform=lambda v: v): 965 ↛ exitline 965 didn't run the lambda on line 965
966 if name in animations:
967 for t, v in animations[name].items():
968 animatable.add_keyframe(t, transform(v))
971class PathDParser:
972 _re = re.compile("|".join((
973 r"[a-zA-Z]",
974 r"[-+]?[0-9]*\.?[0-9]*[eE][-+]?[0-9]+",
975 r"[-+]?[0-9]*\.?[0-9]+",
976 )))
978 def __init__(self, d_string):
979 self.path = objects.properties.Bezier()
980 self.paths = []
981 self.p = NVector(0, 0)
982 self.la = None
983 self.la_type = None
984 self.tokens = list(map(self.d_subsplit, self._re.findall(d_string)))
985 self.add_p = True
986 self.implicit = "M"
988 def d_subsplit(self, tok):
989 if tok.isalpha():
990 return tok
991 return float(tok)
993 def next_token(self):
994 if self.tokens:
995 self.la = self.tokens.pop(0)
996 if isinstance(self.la, str):
997 self.la_type = 0
998 else:
999 self.la_type = 1
1000 else:
1001 self.la = None
1002 return self.la
1004 def next_vec(self):
1005 x = self.next_token()
1006 y = self.next_token()
1007 return NVector(x, y)
1009 def cur_vec(self):
1010 x = self.la
1011 y = self.next_token()
1012 return NVector(x, y)
1014 def parse(self):
1015 self.next_token()
1016 while self.la is not None:
1017 if self.la_type == 0:
1018 parser = "_parse_" + self.la
1019 self.next_token()
1020 getattr(self, parser)()
1021 else:
1022 parser = "_parse_" + self.implicit
1023 getattr(self, parser)()
1025 def _push_path(self):
1026 self.path = objects.properties.Bezier()
1027 self.add_p = True
1029 def _parse_M(self):
1030 if self.la_type != 1: 1030 ↛ 1031line 1030 didn't jump to line 1031, because the condition on line 1030 was never true
1031 self.next_token()
1032 return
1033 self.p = self.cur_vec()
1034 self.implicit = "L"
1035 if not self.add_p:
1036 self._push_path()
1037 self.next_token()
1039 def _parse_m(self):
1040 if self.la_type != 1: 1040 ↛ 1041line 1040 didn't jump to line 1041, because the condition on line 1040 was never true
1041 self.next_token()
1042 return
1043 self.p += self.cur_vec()
1044 self.implicit = "l"
1045 if not self.add_p:
1046 self._push_path()
1047 self.next_token()
1049 def _rpoint(self, point, rel=None):
1050 return (point - (rel or self.p)) if point is not None else NVector(0, 0)
1052 def _do_add_p(self, outp=None):
1053 if self.add_p:
1054 self.paths.append(self.path)
1055 self.path.add_point(self.p.clone(), NVector(0, 0), self._rpoint(outp))
1056 self.add_p = False
1057 elif outp: 1057 ↛ 1058line 1057 didn't jump to line 1058, because the condition on line 1057 was never true
1058 rp = self.path.vertices[-1]
1059 self.path.out_tangents[-1] = self._rpoint(outp, rp)
1061 def _parse_L(self):
1062 if self.la_type != 1: 1062 ↛ 1063line 1062 didn't jump to line 1063, because the condition on line 1062 was never true
1063 self.next_token()
1064 return
1065 self._do_add_p()
1066 self.p = self.cur_vec()
1067 self.path.add_point(self.p.clone(), NVector(0, 0), NVector(0, 0))
1068 self.implicit = "L"
1069 self.next_token()
1071 def _parse_l(self):
1072 if self.la_type != 1: 1072 ↛ 1073line 1072 didn't jump to line 1073, because the condition on line 1072 was never true
1073 self.next_token()
1074 return
1075 self._do_add_p()
1076 self.p += self.cur_vec()
1077 self.path.add_point(self.p.clone(), NVector(0, 0), NVector(0, 0))
1078 self.implicit = "l"
1079 self.next_token()
1081 def _parse_H(self):
1082 if self.la_type != 1: 1082 ↛ 1083line 1082 didn't jump to line 1083, because the condition on line 1082 was never true
1083 self.next_token()
1084 return
1085 self._do_add_p()
1086 self.p[0] = self.la
1087 self.path.add_point(self.p.clone(), NVector(0, 0), NVector(0, 0))
1088 self.implicit = "H"
1089 self.next_token()
1091 def _parse_h(self):
1092 if self.la_type != 1: 1092 ↛ 1093line 1092 didn't jump to line 1093, because the condition on line 1092 was never true
1093 self.next_token()
1094 return
1095 self._do_add_p()
1096 self.p[0] += self.la
1097 self.path.add_point(self.p.clone(), NVector(0, 0), NVector(0, 0))
1098 self.implicit = "h"
1099 self.next_token()
1101 def _parse_V(self):
1102 if self.la_type != 1: 1102 ↛ 1103line 1102 didn't jump to line 1103, because the condition on line 1102 was never true
1103 self.next_token()
1104 return
1105 self._do_add_p()
1106 self.p[1] = self.la
1107 self.path.add_point(self.p.clone(), NVector(0, 0), NVector(0, 0))
1108 self.implicit = "V"
1109 self.next_token()
1111 def _parse_v(self):
1112 if self.la_type != 1: 1112 ↛ 1113line 1112 didn't jump to line 1113, because the condition on line 1112 was never true
1113 self.next_token()
1114 return
1115 self._do_add_p()
1116 self.p[1] += self.la
1117 self.path.add_point(self.p.clone(), NVector(0, 0), NVector(0, 0))
1118 self.implicit = "v"
1119 self.next_token()
1121 def _parse_C(self):
1122 if self.la_type != 1: 1122 ↛ 1123line 1122 didn't jump to line 1123, because the condition on line 1122 was never true
1123 self.next_token()
1124 return
1125 pout = self.cur_vec()
1126 self._do_add_p(pout)
1127 pin = self.next_vec()
1128 self.p = self.next_vec()
1129 self.path.add_point(
1130 self.p.clone(),
1131 (pin - self.p),
1132 NVector(0, 0)
1133 )
1134 self.implicit = "C"
1135 self.next_token()
1137 def _parse_c(self):
1138 if self.la_type != 1: 1138 ↛ 1139line 1138 didn't jump to line 1139, because the condition on line 1138 was never true
1139 self.next_token()
1140 return
1141 pout = self.p + self.cur_vec()
1142 self._do_add_p(pout)
1143 pin = self.p + self.next_vec()
1144 self.p += self.next_vec()
1145 self.path.add_point(
1146 self.p.clone(),
1147 (pin - self.p),
1148 NVector(0, 0)
1149 )
1150 self.implicit = "c"
1151 self.next_token()
1153 def _parse_S(self):
1154 if self.la_type != 1: 1154 ↛ 1155line 1154 didn't jump to line 1155, because the condition on line 1154 was never true
1155 self.next_token()
1156 return
1157 pin = self.cur_vec()
1158 self._do_add_p()
1159 handle = self.path.in_tangents[-1]
1160 self.path.out_tangents[-1] = (-handle)
1161 self.p = self.next_vec()
1162 self.path.add_point(
1163 self.p.clone(),
1164 (pin - self.p),
1165 NVector(0, 0)
1166 )
1167 self.implicit = "S"
1168 self.next_token()
1170 def _parse_s(self):
1171 if self.la_type != 1: 1171 ↛ 1172line 1171 didn't jump to line 1172, because the condition on line 1171 was never true
1172 self.next_token()
1173 return
1174 pin = self.cur_vec() + self.p
1175 self._do_add_p()
1176 handle = self.path.in_tangents[-1]
1177 self.path.out_tangents[-1] = (-handle)
1178 self.p += self.next_vec()
1179 self.path.add_point(
1180 self.p.clone(),
1181 (pin - self.p),
1182 NVector(0, 0)
1183 )
1184 self.implicit = "s"
1185 self.next_token()
1187 def _parse_Q(self):
1188 if self.la_type != 1: 1188 ↛ 1189line 1188 didn't jump to line 1189, because the condition on line 1188 was never true
1189 self.next_token()
1190 return
1191 self._do_add_p()
1192 pin = self.cur_vec()
1193 self.p = self.next_vec()
1194 self.path.add_point(
1195 self.p.clone(),
1196 (pin - self.p),
1197 NVector(0, 0)
1198 )
1199 self.implicit = "Q"
1200 self.next_token()
1202 def _parse_q(self):
1203 if self.la_type != 1: 1203 ↛ 1204line 1203 didn't jump to line 1204, because the condition on line 1203 was never true
1204 self.next_token()
1205 return
1206 self._do_add_p()
1207 pin = self.p + self.cur_vec()
1208 self.p += self.next_vec()
1209 self.path.add_point(
1210 self.p.clone(),
1211 (pin - self.p),
1212 NVector(0, 0)
1213 )
1214 self.implicit = "q"
1215 self.next_token()
1217 def _parse_T(self):
1218 if self.la_type != 1: 1218 ↛ 1219line 1218 didn't jump to line 1219, because the condition on line 1218 was never true
1219 self.next_token()
1220 return
1221 self._do_add_p()
1222 handle = self.p - self.path.in_tangents[-1]
1223 self.p = self.cur_vec()
1224 self.path.add_point(
1225 self.p.clone(),
1226 (handle - self.p),
1227 NVector(0, 0)
1228 )
1229 self.implicit = "T"
1230 self.next_token()
1232 def _parse_t(self):
1233 if self.la_type != 1: 1233 ↛ 1234line 1233 didn't jump to line 1234, because the condition on line 1233 was never true
1234 self.next_token()
1235 return
1236 self._do_add_p()
1237 handle = -self.path.in_tangents[-1] + self.p
1238 self.p += self.cur_vec()
1239 self.path.add_point(
1240 self.p.clone(),
1241 (handle - self.p),
1242 NVector(0, 0)
1243 )
1244 self.implicit = "t"
1245 self.next_token()
1247 def _parse_A(self):
1248 if self.la_type != 1: 1248 ↛ 1249line 1248 didn't jump to line 1249, because the condition on line 1248 was never true
1249 self.next_token()
1250 return
1251 r = self.cur_vec()
1252 xrot = self.next_token()
1253 large = self.next_token()
1254 sweep = self.next_token()
1255 dest = self.next_vec()
1256 self._do_arc(r[0], r[1], xrot, large, sweep, dest)
1257 self.implicit = "A"
1258 self.next_token()
1260 def _do_arc(self, rx, ry, xrot, large, sweep, dest):
1261 self._do_add_p()
1262 if self.p == dest: 1262 ↛ 1263line 1262 didn't jump to line 1263, because the condition on line 1262 was never true
1263 return
1265 if rx == 0 or ry == 0: 1265 ↛ 1267line 1265 didn't jump to line 1267, because the condition on line 1265 was never true
1266 # Straight line
1267 self.p = dest
1268 self.path.add_point(
1269 self.p.clone(),
1270 NVector(0, 0),
1271 NVector(0, 0)
1272 )
1273 return
1275 ellipse, theta1, deltatheta = Ellipse.from_svg_arc(self.p, rx, ry, xrot, large, sweep, dest)
1276 points = ellipse.to_bezier_points(theta1, deltatheta)
1278 self._do_add_p()
1279 self.path.out_tangents[-1] = points[0].out_tangent
1280 for point in points[1:-1]:
1281 self.path.add_point(
1282 point.vertex,
1283 point.in_tangent,
1284 point.out_tangent,
1285 )
1286 self.path.add_point(
1287 dest.clone(),
1288 points[-1].in_tangent,
1289 NVector(0, 0),
1290 )
1291 self.p = dest
1293 def _parse_a(self):
1294 if self.la_type != 1:
1295 self.next_token()
1296 return
1297 r = self.cur_vec()
1298 xrot = self.next_token()
1299 large = self.next_token()
1300 sweep = self.next_token()
1301 dest = self.p + self.next_vec()
1302 self._do_arc(r[0], r[1], xrot, large, sweep, dest)
1303 self.implicit = "a"
1304 self.next_token()
1306 def _parse_Z(self):
1307 if self.path.vertices: 1307 ↛ 1309line 1307 didn't jump to line 1309, because the condition on line 1307 was never false
1308 self.p = self.path.vertices[0].clone()
1309 self.path.close()
1310 self._push_path()
1312 def _parse_z(self):
1313 self._parse_Z()
1316def parse_svg_etree(etree, layer_frames=0, *args, **kwargs):
1317 parser = SvgParser()
1318 return parser.parse_etree(etree, layer_frames, *args, **kwargs)
1321def parse_svg_file(file, layer_frames=0, *args, **kwargs):
1322 return parse_svg_etree(ElementTree.parse(file), layer_frames, *args, **kwargs)