3 from xml.etree
import ElementTree
5 from .handler
import SvgHandler, NameMode
6 from ...
import objects
7 from ...nvector
import NVector
8 from ...utils
import restructure
9 from ...utils.transform
import TransformMatrix
10 from ...parsers
import glaxnimate_helpers
11 from ...utils.color
import Color, ColorMode
13 from ...utils
import font
19 _supported_font_weights = {
20 "Thin": 100,
"Hairline": 100,
21 "ExtraLight": 200,
"UltraLight": 200,
23 "Regular": 400,
"Normal": 400,
"Plain": 400,
"Standard": 400,
"Roman": 400,
25 "SemiBold": 600,
"Demi": 600,
"DemiBold": 600,
27 "Extra": 800,
"ExtraBold": 800,
"Ultra": 800,
"UltraBold": 800,
28 "Black": 900,
"Heavy": 900,
29 "ExtraBlack": 1000,
"UltraBlack": 1000,
"UltraHeavy": 1000,
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
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"
53 namenostart =
r"-.0-9\xB7\u0300-\u036F\u203F-\u2040"
54 id_re = re.compile(
"^[%s][%s%s]*$" % (namestart, namenostart, namestart))
58 self.
svg = ElementTree.Element(
"svg")
59 self.
dom = ElementTree.ElementTree(self.
svg)
60 self.
svg.attrib[
"xmlns"] = self.
ns_map[
"svg"]
82 id =
"%s_%s" % (prefix, self.
idc)
83 if id
not in self.
ids:
89 idn = n.replace(
" ",
"_")
90 if self.
id_re.match(idn)
and idn
not in self.
ids:
95 dom.attrib[
"id"] = 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:
103 dom.attrib[
"id"] = id
108 if inkscape_qual
is None:
109 inkscape_qual = self.
qualified(
"inkscape",
"label")
111 dom.attrib[inkscape_qual] = n
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"
120 self.
defs = ElementTree.SubElement(self.
svg,
"defs")
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"
134 def _mask_to_def(self, mask):
135 svgmask = ElementTree.SubElement(self.
defs,
"mask")
137 svgmask.attrib[
"id"] = mask_id
138 svgmask.attrib[
"mask-type"] =
"alpha"
139 path = ElementTree.SubElement(svgmask,
"path")
141 path.attrib[
"fill"] =
"#fff"
142 path.attrib[
"fill-opacity"] = str(mask.opacity.get_value(self.
time) / 100)
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
153 if matte_mode == objects.MatteMode.Luma:
154 mask_type =
"luminance"
155 svgmask.attrib[
"mask-type"] = mask_type
158 def _on_masks(self, masks):
162 mask_def = ElementTree.SubElement(self.
defs,
"mask")
164 mask_def.attrib[
"id"] = mask_id
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"
177 def _on_layer(self, layer_builder, dom_parent):
178 lot = layer_builder.lottie
185 if layer_builder.matte_target:
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
195 matte_id = layer_builder.matte_source.matte_id = self.
gen_id()
196 g.attrib[
"mask"] =
"url(#%s)" % matte_id
201 for layer
in self.
_precomps.get(lot.reference_id, []):
202 self.process_layer(layer, g)
206 g.attrib[
"opacity"] =
"1"
208 use = ElementTree.SubElement(g,
"use")
209 use.attrib[self.
qualified(
"xlink",
"href")] =
"#" + self.
_assets[lot.image_id]
213 rect = ElementTree.SubElement(g,
"rect")
214 rect.attrib[
"width"] = str(lot.width)
215 rect.attrib[
"height"] = str(lot.height)
219 g.attrib[self.
qualified(
"inkscape",
"label")] = lot.__class__.__name__
220 if layer_builder.shapegroup:
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)),
235 def _on_text_layer(self, g, lot):
236 text = ElementTree.SubElement(g,
"text")
237 doc = lot.data.get_value(self.
time)
239 text.attrib.update(self.
_fonts.get(doc.font_family, {}))
240 text.attrib[
"font-size"] = str(doc.font_size)
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"
253 def _on_layer_end(self, out_layer):
256 def _on_precomp(self, id, dom_parent, layers):
259 def _on_asset(self, asset):
261 img = ElementTree.SubElement(self.
defs,
"image")
264 if asset.is_embedded:
267 url = asset.image_path + asset.image
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)):
274 v = prop.get_value(self.
time)
280 if isinstance(v, NVector):
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)
294 dom.attrib[
"opacity"] = str(op/100)
296 def _get_group_stroke(self, group):
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:
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)
331 if last_mode != objects.StrokeDashType.Offset:
332 dasharray.append(str(last))
334 last_mode = dash.type
335 style[
"stroke-dasharray"] =
" ".join(dasharray)
338 def _style_to_css(self, style):
340 lambda x:
":".join(map(str, x)),
344 def _split_stroke(self, group, fill_layer, out_parent):
349 if style.get(
"stroke-width", 0) <= 0
or style[
"stroke-opacity"] <= 0:
352 if group.stroke_above:
353 if fill_layer.attrib.get(
"style",
""):
354 fill_layer.attrib[
"style"] +=
";"
356 fill_layer.attrib[
"style"] =
""
360 g = ElementTree.Element(
"g")
362 use = ElementTree.Element(
"use")
363 for i, e
in enumerate(out_parent):
365 out_parent.insert(i, g)
366 out_parent.remove(fill_layer)
374 use.attrib[self.
qualified(
"xlink",
"href")] =
"#" + fill_layer.attrib[
"id"]
381 style[
"fill-opacity"] = group.fill.opacity.get_value(self.
time) / 100
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"
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)
425 stop.attrib[
"stop-opacity"] = str(color[3])
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))
437 def _on_shapegroup(self, group, dom_parent):
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)
450 self.shapegroup_process_children(group, g)
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])
460 def _on_shape(self, shape, shapegroup, out_parent):
466 svgshape = self.
build_path([shape.to_bezier()], out_parent)
468 svgshape = self.
build_path([shape], out_parent)
469 elif has_font
and isinstance(shape, font.FontShape):
473 self.
set_id(svgshape, shape, force=
True)
474 if "style" not in svgshape.attrib:
475 svgshape.attrib[
"style"] =
""
480 svgshape.attrib[
"style"] +=
"display: none;"
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))
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])
505 path = ElementTree.SubElement(parent,
"path")
508 bez = shape.shape.get_value(self.
time)
509 if isinstance(bez, list):
520 def _bezier_tangent(self, tangent):
521 _tangent_threshold = 0.5
522 if tangent.length < _tangent_threshold:
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]
531 qto = bez.vertices[i]
534 d +=
"C %s,%s %s,%s %s,%s " % (
540 qfrom = bez.vertices[-1]
542 qto = bez.vertices[0]
544 d +=
"C %s,%s %s,%s %s,%s Z" % (
552 def _on_shape_modifier(self, shape, shapegroup, out_parent):
554 svgshape = self.
build_repeater(shape.lottie, shape.child, shapegroup, out_parent)
558 svgshape = self.
build_trim_path(shape.lottie, shape.child, shapegroup, out_parent)
560 return self.shapegroup_process_child(shape.child, shapegroup, out_parent)
564 original = self.shapegroup_process_child(child, shapegroup, out_parent)
568 ncopies = int(round(shape.copies.get_value(self.
time)))
572 out_parent.remove(original)
574 g = ElementTree.SubElement(out_parent,
"g")
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)
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):
592 transform.opacity.value = so * of + eo * (1 - of)
594 transform.position.value += position
595 transform.rotation.value += rotation
596 transform.anchor_point.value += anchor_point
601 round_amount = shape.radius.get_value(self.
time)
604 def _build_rouded_corners_shape(self, shape, round_amount):
607 path = shape.to_bezier()
608 bezier = path.shape.get_value(self.
time).rounded(round_amount)
609 path.shape.clear_animation(bezier)
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
620 if shape.multiple == objects.TrimMultipleShapes.Individually:
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
629 start+offset, end+offset, multidata, length
632 def _modifier_foreach_shape(self, shape):
633 if isinstance(shape, restructure.RestructuredShapeGroup):
634 for child
in shape.children:
637 elif isinstance(shape, restructure.RestructuredPathMerger):
638 for p
in shape.paths:
643 def _modifier_process(self, 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):
656 bezier, local_start, local_length = multidata[shape]
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)
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:
671 if lstart <= 0
and lend >= 1:
673 seg = bezier.segment(lstart, lend)
676 path = shape.to_bezier()
677 bezier = path.shape.get_value(self.
time)
679 bez1 = bezier.segment(start, 1)
680 bez2 = bezier.segment(0, end-1)
683 seg = bezier.segment(start, end)
686 def _modifier_process_children(self, shapegroup, out_parent, callback, *args):
688 for shape
in shapegroup.children:
690 shapegroup.children = children
692 def _modifier_process_child(self, shape, shapegroup, out_parent, callback, *args):
693 if isinstance(shape, restructure.RestructuredShapeGroup):
696 elif isinstance(shape, restructure.RestructuredPathMerger):
698 for p
in shape.paths:
699 paths.extend(callback(p, *args))
705 return callback(shape, *args)
707 def _custom_object_supported(self, shape):
708 if has_font
and isinstance(shape, font.FontShape):
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"
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
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]))
744 def to_svg(animation, time, animated=False):
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")))
754 builder.process(animation)