Coverage for lib/lottie/parsers/svg/builder.py: 8%
586 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
3from xml.etree import ElementTree
5from .handler import SvgHandler, NameMode
6from ... import objects
7from ...nvector import NVector
8from ...utils import restructure
9from ...utils.transform import TransformMatrix
10from ...parsers import glaxnimate_helpers
11from ...utils.color import Color, ColorMode
12try:
13 from ...utils import font
14 has_font = True
15except ImportError:
16 has_font = False
19_supported_font_weights = {
20 "Thin": 100, "Hairline": 100,
21 "ExtraLight": 200, "UltraLight": 200,
22 "Light": 300,
23 "Regular": 400, "Normal": 400, "Plain": 400, "Standard": 400, "Roman": 400,
24 "Medium": 500,
25 "SemiBold": 600, "Demi": 600, "DemiBold": 600,
26 "Bold": 700,
27 "Extra": 800, "ExtraBold": 800, "Ultra": 800, "UltraBold": 800,
28 "Black": 900, "Heavy": 900,
29 "ExtraBlack": 1000, "UltraBlack": 1000, "UltraHeavy": 1000,
30}
33class PrecompTime:
34 def __init__(self, pcl: objects.PreCompLayer):
35 self.pcl = pcl
37 def get_time_offset(self, time, lot):
38 remap = time
39 if self.pcl.time_remapping:
40 remapf = self.pcl.time_remapping.get_value(time)
41 remap = lot.in_point * (1-remapf) + lot.out_point * remapf
43 return remap - self.pcl.start_time
46class SvgBuilder(SvgHandler, restructure.AbstractBuilder):
47 merge_paths = True
48 namestart = (
49 r":_A-Za-z\xC0-\xD6\xD8-\xF6\xF8-\u02FF\u0370-\u037D\u037F-\u1FFF" +
50 r"\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF" +
51 r"\uFDF0-\uFFFD\U00010000-\U000EFFFF"
52 )
53 namenostart = r"-.0-9\xB7\u0300-\u036F\u203F-\u2040"
54 id_re = re.compile("^[%s][%s%s]*$" % (namestart, namenostart, namestart))
56 def __init__(self, time=0):
57 super().__init__()
58 self.svg = ElementTree.Element("svg")
59 self.dom = ElementTree.ElementTree(self.svg)
60 self.svg.attrib["xmlns"] = self.ns_map["svg"]
61 self.ids = set()
62 self.idc = 0
63 self.name_mode = NameMode.Inkscape
64 self.actual_time = time
65 self.precomp_times = []
66 self._precomps = {}
67 self._assets = {}
68 self._fonts = {}
69 self._current_layer = []
71 @property
72 def time(self):
73 time = self.actual_time
74 if self.precomp_times:
75 for pct in self.precomp_times:
76 time = pct.get_time_offset(time, self._current_layer[-1])
77 return time
79 def gen_id(self, prefix="id"):
80 while True:
81 self.idc += 1
82 id = "%s_%s" % (prefix, self.idc)
83 if id not in self.ids:
84 break
85 self.ids.add(id)
86 return id
88 def set_clean_id(self, dom, n):
89 idn = n.replace(" ", "_")
90 if self.id_re.match(idn) and idn not in self.ids:
91 self.ids.add(idn)
92 else:
93 idn = self.gen_id(dom.tag)
95 dom.attrib["id"] = idn
96 return idn
98 def set_id(self, dom, lottieobj, inkscape_qual=None, force=False):
99 n = getattr(lottieobj, "name", None)
100 if n is None or self.name_mode == NameMode.NoName:
101 if force:
102 id = self.gen_id(dom.tag)
103 dom.attrib["id"] = id
104 return id
105 return None
107 idn = self.set_clean_id(dom, n)
108 if inkscape_qual is None:
109 inkscape_qual = self.qualified("inkscape", "label")
110 if inkscape_qual:
111 dom.attrib[inkscape_qual] = n
112 return idn
114 def _on_animation(self, animation: objects.Animation):
115 self.svg.attrib["width"] = str(animation.width)
116 self.svg.attrib["height"] = str(animation.height)
117 self.svg.attrib["viewBox"] = "0 0 %s %s" % (animation.width, animation.height)
118 self.svg.attrib["version"] = "1.1"
119 self.set_id(self.svg, animation, self.qualified("sodipodi", "docname"))
120 self.defs = ElementTree.SubElement(self.svg, "defs")
121 if self.name_mode == NameMode.Inkscape:
122 self.svg.attrib[self.qualified("inkscape", "export-xdpi")] = "96"
123 self.svg.attrib[self.qualified("inkscape", "export-ydpi")] = "96"
124 namedview = ElementTree.SubElement(self.svg, self.qualified("sodipodi", "namedview"))
125 namedview.attrib[self.qualified("inkscape", "pagecheckerboard")] = "true"
126 namedview.attrib["borderlayer"] = "true"
127 namedview.attrib["bordercolor"] = "#666666"
128 namedview.attrib["pagecolor"] = "#ffffff"
129 self.svg.attrib["style"] = "fill: none; stroke: none"
131 self._current_layer = [animation]
132 return self.svg
134 def _mask_to_def(self, mask):
135 svgmask = ElementTree.SubElement(self.defs, "mask")
136 mask_id = self.gen_id()
137 svgmask.attrib["id"] = mask_id
138 svgmask.attrib["mask-type"] = "alpha"
139 path = ElementTree.SubElement(svgmask, "path")
140 path.attrib["d"] = self._bezier_to_d(mask.shape.get_value(self.time))
141 path.attrib["fill"] = "#fff"
142 path.attrib["fill-opacity"] = str(mask.opacity.get_value(self.time) / 100)
143 return mask_id
145 def _matte_source_to_def(self, layer_builder):
146 svgmask = ElementTree.SubElement(self.defs, "mask")
147 if not layer_builder.matte_id:
148 layer_builder.matte_id = self.gen_id()
149 svgmask.attrib["id"] = layer_builder.matte_id
150 matte_mode = layer_builder.matte_target.lottie.matte_mode
152 mask_type = "alpha"
153 if matte_mode == objects.MatteMode.Luma:
154 mask_type = "luminance"
155 svgmask.attrib["mask-type"] = mask_type
156 return svgmask
158 def _on_masks(self, masks):
159 if len(masks) == 1:
160 return self._mask_to_def(masks[0])
161 mask_ids = list(map(self._mask_to_def, masks))
162 mask_def = ElementTree.SubElement(self.defs, "mask")
163 mask_id = self.gen_id()
164 mask_def.attrib["id"] = mask_id
165 g = mask_def
166 for mid in mask_ids:
167 g = ElementTree.SubElement(g, "g")
168 g.attrib["mask"] = "url(#%s)" % mid
169 full = ElementTree.SubElement(g, "rect")
170 full.attrib["fill"] = "#fff"
171 full.attrib["width"] = self.svg.attrib["width"]
172 full.attrib["height"] = self.svg.attrib["height"]
173 full.attrib["x"] = "0"
174 full.attrib["y"] = "0"
175 return mask_id
177 def _on_layer(self, layer_builder, dom_parent):
178 lot = layer_builder.lottie
179 self._current_layer.append(lot)
181 if not self.precomp_times and (lot.in_point > self.time or lot.out_point < self.time):
182 self._current_layer.pop()
183 return None
185 if layer_builder.matte_target:
186 dom_parent = self._matte_source_to_def(layer_builder)
188 g = self.group_from_lottie(lot, dom_parent, True)
190 if lot.masks:
191 g.attrib["mask"] = "url(#%s)" % self._on_masks(lot.masks)
192 elif layer_builder.matte_source:
193 matte_id = layer_builder.matte_source.matte_id
194 if not matte_id:
195 matte_id = layer_builder.matte_source.matte_id = self.gen_id()
196 g.attrib["mask"] = "url(#%s)" % matte_id
198 if isinstance(lot, objects.PreCompLayer):
199 self.precomp_times.append(PrecompTime(lot))
201 for layer in self._precomps.get(lot.reference_id, []):
202 self.process_layer(layer, g)
204 self.precomp_times.pop()
205 elif isinstance(lot, objects.NullLayer):
206 g.attrib["opacity"] = "1"
207 elif isinstance(lot, objects.ImageLayer):
208 use = ElementTree.SubElement(g, "use")
209 use.attrib[self.qualified("xlink", "href")] = "#" + self._assets[lot.image_id]
210 elif isinstance(lot, objects.TextLayer):
211 self._on_text_layer(g, lot)
212 elif isinstance(lot, objects.SolidColorLayer):
213 rect = ElementTree.SubElement(g, "rect")
214 rect.attrib["width"] = str(lot.width)
215 rect.attrib["height"] = str(lot.height)
216 rect.attrib["fill"] = color_to_css(lot.color)
218 if not lot.name:
219 g.attrib[self.qualified("inkscape", "label")] = lot.__class__.__name__
220 if layer_builder.shapegroup:
221 g.attrib["style"] = self.group_to_style(layer_builder.shapegroup)
222 self._split_stroke(layer_builder.shapegroup, g, dom_parent)
223 #if lot.hidden:
224 #g.attrib.setdefault("style", "")
225 #g.attrib["style"] += "display: none;"
227 return g
229 def _on_font(self, font):
230 self._fonts[font.name] = {
231 "font-family": font.font_family,
232 "font-weight": str(_supported_font_weights.get(font.font_style, 400)),
233 }
235 def _on_text_layer(self, g, lot):
236 text = ElementTree.SubElement(g, "text")
237 doc = lot.data.get_value(self.time)
238 if doc:
239 text.attrib.update(self._fonts.get(doc.font_family, {}))
240 text.attrib["font-size"] = str(doc.font_size)
241 if doc.line_height:
242 text.attrib["line-height"] = "%s%%" % doc.line_height
243 if doc.justify == objects.text.TextJustify.Left:
244 text.attrib["text-anchor"] = "start"
245 elif doc.justify == objects.text.TextJustify.Center:
246 text.attrib["text-anchor"] = "middle"
247 elif doc.justify == objects.text.TextJustify.Right:
248 text.attrib["text-anchor"] = "end"
250 text.attrib["fill"] = color_to_css(doc.color)
251 text.text = doc.text
253 def _on_layer_end(self, out_layer):
254 self._current_layer.pop()
256 def _on_precomp(self, id, dom_parent, layers):
257 self._precomps[id] = layers
259 def _on_asset(self, asset):
260 if isinstance(asset, objects.assets.Image):
261 img = ElementTree.SubElement(self.defs, "image")
262 xmlid = self.set_clean_id(img, asset.id)
263 self._assets[asset.id] = xmlid
264 if asset.is_embedded:
265 url = asset.image
266 else:
267 url = asset.path + asset.file_name
268 img.attrib[self.qualified("xlink", "href")] = url
269 img.attrib["width"] = str(asset.width)
270 img.attrib["height"] = str(asset.height)
272 def _get_value(self, prop, default=NVector(0, 0)):
273 if prop:
274 v = prop.get_value(self.time)
275 else:
276 v = default
278 if v is None:
279 return default
280 if isinstance(v, NVector):
281 return v.clone()
282 return v
284 def set_transform(self, dom, transform, auto_orient=False):
285 if not transform:
286 return
288 mat = transform.to_matrix(self.time, auto_orient)
289 dom.attrib["transform"] = mat.to_css_2d()
291 if transform.opacity is not None:
292 op = transform.opacity.get_value(self.time)
293 if op != 100:
294 dom.attrib["opacity"] = str(op/100)
296 def _get_group_stroke(self, group):
297 style = {}
298 if group.stroke:
299 if isinstance(group.stroke, objects.GradientStroke):
300 style["stroke"] = "url(#%s)" % self.process_gradient(group.stroke)
301 else:
302 style["stroke"] = color_to_css(group.stroke.color.get_value(self.time))
304 style["stroke-opacity"] = group.stroke.opacity.get_value(self.time) / 100
305 style["stroke-width"] = group.stroke.width.get_value(self.time)
306 if group.stroke.miter_limit is not None:
307 style["stroke-miterlimit"] = group.stroke.miter_limit
309 if group.stroke.line_cap == objects.LineCap.Round:
310 style["stroke-linecap"] = "round"
311 elif group.stroke.line_cap == objects.LineCap.Butt:
312 style["stroke-linecap"] = "butt"
313 elif group.stroke.line_cap == objects.LineCap.Square:
314 style["stroke-linecap"] = "square"
316 if group.stroke.line_join == objects.LineJoin.Round:
317 style["stroke-linejoin"] = "round"
318 elif group.stroke.line_join == objects.LineJoin.Bevel:
319 style["stroke-linejoin"] = "bevel"
320 elif group.stroke.line_join == objects.LineJoin.Miter:
321 style["stroke-linejoin"] = "miter"
323 if group.stroke.dashes:
324 dasharray = []
325 last = 0
326 last_mode = objects.StrokeDashType.Dash
327 for dash in group.stroke.dashes:
328 if last_mode == dash.type:
329 last += dash.length.get_value(self.time)
330 else:
331 if last_mode != objects.StrokeDashType.Offset:
332 dasharray.append(str(last))
333 last = 0
334 last_mode = dash.type
335 style["stroke-dasharray"] = " ".join(dasharray)
336 return style
338 def _style_to_css(self, style):
339 return ";".join(map(
340 lambda x: ":".join(map(str, x)),
341 style.items()
342 ))
344 def _split_stroke(self, group, fill_layer, out_parent):
345 if not group.stroke:# or group.stroke_above:
346 return
348 style = self._get_group_stroke(group)
349 if style.get("stroke-width", 0) <= 0 or style["stroke-opacity"] <= 0:
350 return
352 if group.stroke_above:
353 if fill_layer.attrib.get("style", ""):
354 fill_layer.attrib["style"] += ";"
355 else:
356 fill_layer.attrib["style"] = ""
357 fill_layer.attrib["style"] += self._style_to_css(style)
358 return fill_layer
360 g = ElementTree.Element("g")
361 self.set_clean_id(g, "stroke")
362 use = ElementTree.Element("use")
363 for i, e in enumerate(out_parent):
364 if e is fill_layer:
365 out_parent.insert(i, g)
366 out_parent.remove(fill_layer)
367 break
368 else:
369 return
371 g.append(use)
372 g.append(fill_layer)
374 use.attrib[self.qualified("xlink", "href")] = "#" + fill_layer.attrib["id"]
375 use.attrib["style"] = self._style_to_css(style)
376 return g
378 def group_to_style(self, group):
379 style = {}
380 if group.fill:
381 style["fill-opacity"] = group.fill.opacity.get_value(self.time) / 100
382 if isinstance(group.fill, objects.GradientFill):
383 style["fill"] = "url(#%s)" % self.process_gradient(group.fill)
384 else:
385 style["fill"] = color_to_css(group.fill.color.get_value(self.time))
387 if group.fill.fill_rule:
388 style["fill-rule"] = "evenodd" if group.fill.fill_rule == objects.FillRule.EvenOdd else "nonzero"
390 if group.lottie.hidden:
391 style["display"] = "none"
392 #if group.stroke_above:
393 #style.update(self._get_group_stroke(group))
395 return self._style_to_css(style)
397 def process_gradient(self, gradient):
398 spos = gradient.start_point.get_value(self.time)
399 epos = gradient.end_point.get_value(self.time)
401 if gradient.gradient_type == objects.GradientType.Linear:
402 dom = ElementTree.SubElement(self.defs, "linearGradient")
403 dom.attrib["x1"] = str(spos[0])
404 dom.attrib["y1"] = str(spos[1])
405 dom.attrib["x2"] = str(epos[0])
406 dom.attrib["y2"] = str(epos[1])
407 elif gradient.gradient_type == objects.GradientType.Radial:
408 dom = ElementTree.SubElement(self.defs, "radialGradient")
409 dom.attrib["cx"] = str(spos[0])
410 dom.attrib["cy"] = str(spos[1])
411 dom.attrib["r"] = str((epos-spos).length)
412 a = gradient.highlight_angle.get_value(self.time) * math.pi / 180
413 l = gradient.highlight_length.get_value(self.time)
414 dom.attrib["fx"] = str(spos[0] + math.cos(a) * l)
415 dom.attrib["fy"] = str(spos[1] + math.sin(a) * l)
417 id = self.set_id(dom, gradient, force=True)
418 dom.attrib["gradientUnits"] = "userSpaceOnUse"
420 for off, color in gradient.colors.stops_at(self.time):
421 stop = ElementTree.SubElement(dom, "stop")
422 stop.attrib["offset"] = "%s%%" % (off * 100)
423 stop.attrib["stop-color"] = color_to_css(color[:3])
424 if len(color) > 3:
425 stop.attrib["stop-opacity"] = str(color[3])
427 return id
429 def group_from_lottie(self, lottie, dom_parent, layer):
430 g = ElementTree.SubElement(dom_parent, "g")
431 if layer and self.name_mode == NameMode.Inkscape:
432 g.attrib[self.qualified("inkscape", "groupmode")] = "layer"
433 self.set_id(g, lottie, force=True)
434 self.set_transform(g, lottie.transform, getattr(lottie, "auto_orient", False))
435 return g
437 def _on_shapegroup(self, group, dom_parent):
438 if group.empty():
439 return
441 if len(group.children) == 1 and isinstance(group.children[0], restructure.RestructuredPathMerger):
442 path = self.build_path(group.paths.paths, dom_parent)
443 self.set_id(path, group.paths.paths[0], force=True)
444 path.attrib["style"] = self.group_to_style(group)
445 self.set_transform(path, group.lottie.transform)
446 return self._split_stroke(group, path, dom_parent)
448 g = self.group_from_lottie(group.lottie, dom_parent, group.layer)
449 g.attrib["style"] = self.group_to_style(group)
450 self.shapegroup_process_children(group, g)
451 return self._split_stroke(group, g, dom_parent)
453 def _on_merged_path(self, shape, shapegroup, out_parent):
454 path = self.build_path(shape.paths, out_parent)
455 self.set_id(path, shape.paths[0])
456 path.attrib["style"] = self.group_to_style(shapegroup)
457 #self._split_stroke(shapegroup, path, out_parent)
458 return path
460 def _on_shape(self, shape, shapegroup, out_parent):
461 if isinstance(shape, objects.Rect):
462 svgshape = self.build_rect(shape, out_parent)
463 elif isinstance(shape, objects.Ellipse):
464 svgshape = self.build_ellipse(shape, out_parent)
465 elif isinstance(shape, objects.Star):
466 svgshape = self.build_path([shape.to_bezier()], out_parent)
467 elif isinstance(shape, objects.Path):
468 svgshape = self.build_path([shape], out_parent)
469 elif has_font and isinstance(shape, font.FontShape):
470 svgshape = self.build_text(shape, out_parent)
471 else:
472 return
473 self.set_id(svgshape, shape, force=True)
474 if "style" not in svgshape.attrib:
475 svgshape.attrib["style"] = ""
476 svgshape.attrib["style"] += self.group_to_style(shapegroup)
477 #self._split_stroke(shapegroup, svgshape, out_parent)
479 if shape.hidden:
480 svgshape.attrib["style"] += "display: none;"
481 return svgshape
483 def build_rect(self, shape, parent):
484 rect = ElementTree.SubElement(parent, "rect")
485 size = shape.size.get_value(self.time)
486 pos = shape.position.get_value(self.time)
487 rect.attrib["width"] = str(size[0])
488 rect.attrib["height"] = str(size[1])
489 rect.attrib["x"] = str(pos[0] - size[0] / 2)
490 rect.attrib["y"] = str(pos[1] - size[1] / 2)
491 rect.attrib["rx"] = str(shape.rounded.get_value(self.time))
492 return rect
494 def build_ellipse(self, shape, parent):
495 ellipse = ElementTree.SubElement(parent, "ellipse")
496 size = shape.size.get_value(self.time)
497 pos = shape.position.get_value(self.time)
498 ellipse.attrib["rx"] = str(size[0] / 2)
499 ellipse.attrib["ry"] = str(size[1] / 2)
500 ellipse.attrib["cx"] = str(pos[0])
501 ellipse.attrib["cy"] = str(pos[1])
502 return ellipse
504 def build_path(self, shapes, parent):
505 path = ElementTree.SubElement(parent, "path")
506 d = ""
507 for shape in shapes:
508 bez = shape.shape.get_value(self.time)
509 if isinstance(bez, list):
510 bez = bez[0]
511 if not bez.vertices:
512 continue
513 if d:
514 d += "\n"
515 d += self._bezier_to_d(bez)
517 path.attrib["d"] = d
518 return path
520 def _bezier_tangent(self, tangent):
521 _tangent_threshold = 0.5
522 if tangent.length < _tangent_threshold:
523 return NVector(0, 0)
524 return tangent
526 def _bezier_to_d(self, bez):
527 d = "M %s,%s " % tuple(bez.vertices[0].components[:2])
528 for i in range(1, len(bez.vertices)):
529 qfrom = bez.vertices[i-1]
530 h1 = self._bezier_tangent(bez.out_tangents[i-1]) + qfrom
531 qto = bez.vertices[i]
532 h2 = self._bezier_tangent(bez.in_tangents[i]) + qto
534 d += "C %s,%s %s,%s %s,%s " % (
535 h1[0], h1[1],
536 h2[0], h2[1],
537 qto[0], qto[1],
538 )
539 if bez.closed:
540 qfrom = bez.vertices[-1]
541 h1 = self._bezier_tangent(bez.out_tangents[-1]) + qfrom
542 qto = bez.vertices[0]
543 h2 = self._bezier_tangent(bez.in_tangents[0]) + qto
544 d += "C %s,%s %s,%s %s,%s Z" % (
545 h1[0], h1[1],
546 h2[0], h2[1],
547 qto[0], qto[1],
548 )
550 return d
552 def _on_shape_modifier(self, shape, shapegroup, out_parent):
553 if isinstance(shape.lottie, objects.Repeater):
554 svgshape = self.build_repeater(shape.lottie, shape.child, shapegroup, out_parent)
555 elif isinstance(shape.lottie, objects.RoundedCorners):
556 svgshape = self.build_rouded_corners(shape.lottie, shape.child, shapegroup, out_parent)
557 elif isinstance(shape.lottie, objects.Trim):
558 svgshape = self.build_trim_path(shape.lottie, shape.child, shapegroup, out_parent)
559 else:
560 return self.shapegroup_process_child(shape.child, shapegroup, out_parent)
561 return svgshape
563 def build_repeater(self, shape, child, shapegroup, out_parent):
564 original = self.shapegroup_process_child(child, shapegroup, out_parent)
565 if not original:
566 return
568 ncopies = int(round(shape.copies.get_value(self.time)))
569 if ncopies == 1:
570 return
572 out_parent.remove(original)
574 g = ElementTree.SubElement(out_parent, "g")
575 self.set_clean_id(g, "repeater")
577 for copy in range(ncopies-1):
578 use = ElementTree.SubElement(g, "use")
579 use.attrib[self.qualified("xlink", "href")] = "#" + original.attrib["id"]
581 orig_wrapper = ElementTree.SubElement(g, "g")
582 orig_wrapper.append(original)
584 transform = objects.Transform()
585 so = shape.transform.start_opacity.get_value(self.time)
586 eo = shape.transform.end_opacity.get_value(self.time)
587 position = shape.transform.position.get_value(self.time)
588 rotation = shape.transform.rotation.get_value(self.time)
589 anchor_point = shape.transform.anchor_point.get_value(self.time)
590 for i in range(ncopies-1, -1, -1):
591 of = i / (ncopies-1)
592 transform.opacity.value = so * of + eo * (1 - of)
593 self.set_transform(g[i], transform)
594 transform.position.value += position
595 transform.rotation.value += rotation
596 transform.anchor_point.value += anchor_point
598 return g
600 def build_rouded_corners(self, shape, child, shapegroup, out_parent):
601 round_amount = shape.radius.get_value(self.time)
602 return self._modifier_process(child, shapegroup, out_parent, self._build_rouded_corners_shape, round_amount)
604 def _build_rouded_corners_shape(self, shape, round_amount):
605 if not isinstance(shape, objects.Shape):
606 return [shape]
607 path = shape.to_bezier()
608 bezier = path.shape.get_value(self.time).rounded(round_amount)
609 path.shape.clear_animation(bezier)
610 return [path]
612 def build_trim_path(self, shape, child, shapegroup, out_parent):
613 start = max(0, min(1, shape.start.get_value(self.time) / 100))
614 end = max(0, min(1, shape.end.get_value(self.time) / 100))
615 offset = shape.offset.get_value(self.time) / 360 % 1
617 multidata = {}
618 length = 0
620 if shape.multiple == objects.TrimMultipleShapes.Individually:
621 for visishape in reversed(list(self._modifier_foreach_shape(child))):
622 bez = visishape.to_bezier().shape.get_value(self.time)
623 local_length = bez.rough_length()
624 multidata[visishape] = (bez, length, local_length)
625 length += local_length
627 return self._modifier_process(
628 child, shapegroup, out_parent, self._build_trim_path_shape,
629 start+offset, end+offset, multidata, length
630 )
632 def _modifier_foreach_shape(self, shape):
633 if isinstance(shape, restructure.RestructuredShapeGroup):
634 for child in shape.children:
635 for chsh in self._modifier_foreach_shape(child):
636 yield chsh
637 elif isinstance(shape, restructure.RestructuredPathMerger):
638 for p in shape.paths:
639 yield p
640 elif isinstance(shape, objects.Shape):
641 yield shape
643 def _modifier_process(self, child, shapegroup, out_parent, callback, *args):
644 children = self._modifier_process_child(child, shapegroup, out_parent, callback, *args)
645 return [self.shapegroup_process_child(ch, shapegroup, out_parent) for ch in children]
647 def _trim_offlocal(self, t, local_start, local_length, total_length):
648 gt = (t * total_length - local_start) / local_length
649 return max(0, min(1, gt))
651 def _build_trim_path_shape(self, shape, start, end, multidata, total_length):
652 if not isinstance(shape, objects.Shape):
653 return [shape]
655 if multidata:
656 bezier, local_start, local_length = multidata[shape]
657 if end > 1:
658 lstart = self._trim_offlocal(start, local_start, local_length, total_length)
659 lend = self._trim_offlocal(end-1, local_start, local_length, total_length)
660 out = []
661 if lstart < 1:
662 out.append(objects.Path(bezier.segment(lstart, 1)))
663 if lend > 0:
664 out.append(objects.Path(bezier.segment(0, lend)))
665 return out
667 lstart = self._trim_offlocal(start, local_start, local_length, total_length)
668 lend = self._trim_offlocal(end, local_start, local_length, total_length)
669 if lend <= 0 or lstart >= 1:
670 return []
671 if lstart <= 0 and lend >= 1:
672 return [objects.Path(bezier)]
673 seg = bezier.segment(lstart, lend)
674 return [objects.Path(seg)]
676 path = shape.to_bezier()
677 bezier = path.shape.get_value(self.time)
678 if end > 1:
679 bez1 = bezier.segment(start, 1)
680 bez2 = bezier.segment(0, end-1)
681 return [objects.Path(bez1), objects.Path(bez2)]
682 else:
683 seg = bezier.segment(start, end)
684 return [objects.Path(seg)]
686 def _modifier_process_children(self, shapegroup, out_parent, callback, *args):
687 children = []
688 for shape in shapegroup.children:
689 children.extend(self._modifier_process_child(shape, shapegroup, out_parent, callback, *args))
690 shapegroup.children = children
692 def _modifier_process_child(self, shape, shapegroup, out_parent, callback, *args):
693 if isinstance(shape, restructure.RestructuredShapeGroup):
694 self._modifier_process_children(shape, out_parent, callback, *args)
695 return [shape]
696 elif isinstance(shape, restructure.RestructuredPathMerger):
697 paths = []
698 for p in shape.paths:
699 paths.extend(callback(p, *args))
700 shape.paths = paths
701 if paths:
702 return [shape]
703 return []
704 else:
705 return callback(shape, *args)
707 def _custom_object_supported(self, shape):
708 if has_font and isinstance(shape, font.FontShape):
709 return True
710 return False
712 def build_text(self, shape, parent):
713 text = ElementTree.SubElement(parent, "text")
714 if "family" in shape.query:
715 text.attrib["font-family"] = shape.query["family"]
716 if "weight" in shape.query:
717 text.attrib["font-weight"] = str(shape.query.weight_to_css())
718 slant = int(shape.query.get("slant", 0))
719 if slant > 0 and slant < 110:
720 text.attrib["font-style"] = "italic"
721 elif slant >= 110:
722 text.attrib["font-style"] = "oblique"
724 text.attrib["font-size"] = str(shape.size)
726 text.attrib["white-space"] = "pre"
728 pos = shape.style.position
729 text.attrib["x"] = str(pos.x)
730 text.attrib["y"] = str(pos.y)
731 text.text = shape.text
733 return text
736def color_to_css(color):
737 #if len(color) == 4:
738 #return ("rgba(%s, %s, %s" % tuple(map(lambda c: int(round(c*255)), color[:3]))) + ", %s)" % color[3]
739 if isinstance(color, Color) and color.mode != ColorMode.RGB:
740 color = color.converted(ColorMode.RGB)
741 return "rgb(%s, %s, %s)" % tuple(map(lambda c: int(round(c*255)), color[:3]))
744def to_svg(animation, time, animated=False):
745 if animated:
746 data = glaxnimate_helpers.convert(animation, "svg")
747 return ElementTree.ElementTree(ElementTree.fromstring(data.decode("utf8")))
749 if glaxnimate_helpers.has_glaxnimate:
750 data = glaxnimate_helpers.serialize(animation, "svg")
751 return ElementTree.ElementTree(ElementTree.fromstring(data.decode("utf8")))
753 builder = SvgBuilder(time)
754 builder.process(animation)
755 return builder.dom