python-lottie  0.6.11+devc144cca
A framework to work with lottie files and telegram animated stickers (tgs)
builder.py
Go to the documentation of this file.
1 import re
2 import math
3 from xml.etree import ElementTree
4 
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
12 try:
13  from ...utils import font
14  has_font = True
15 except ImportError:
16  has_font = False
17 
18 
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 }
31 
32 
34  def __init__(self, pcl: objects.PreCompLayer):
35  self.pcl = pcl
36 
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
42 
43  return remap - self.pcl.start_time
44 
45 
46 class 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))
55 
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 = []
70 
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
78 
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
87 
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)
94 
95  dom.attrib["id"] = idn
96  return idn
97 
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
106 
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
113 
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"
130 
131  self._current_layer = [animation]
132  return self.svg
133 
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
144 
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
151 
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
157 
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
176 
177  def _on_layer(self, layer_builder, dom_parent):
178  lot = layer_builder.lottie
179  self._current_layer.append(lot)
180 
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
184 
185  if layer_builder.matte_target:
186  dom_parent = self._matte_source_to_def(layer_builder)
187 
188  g = self.group_from_lottie(lot, dom_parent, True)
189 
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
197 
198  if isinstance(lot, objects.PreCompLayer):
199  self.precomp_times.append(PrecompTime(lot))
200 
201  for layer in self._precomps.get(lot.reference_id, []):
202  self.process_layer(layer, g)
203 
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)
217 
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;"
226 
227  return g
228 
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  }
234 
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"
249 
250  text.attrib["fill"] = color_to_css(doc.color)
251  text.text = doc.text
252 
253  def _on_layer_end(self, out_layer):
254  self._current_layer.pop()
255 
256  def _on_precomp(self, id, dom_parent, layers):
257  self._precomps[id] = layers
258 
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.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)
271 
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
277 
278  if v is None:
279  return default
280  if isinstance(v, NVector):
281  return v.clone()
282  return v
283 
284  def set_transform(self, dom, transform, auto_orient=False):
285  if not transform:
286  return
287 
288  mat = transform.to_matrix(self.time, auto_orient)
289  dom.attrib["transform"] = mat.to_css_2d()
290 
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)
295 
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))
303 
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
308 
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"
315 
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"
322 
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
337 
338  def _style_to_css(self, style):
339  return ";".join(map(
340  lambda x: ":".join(map(str, x)),
341  style.items()
342  ))
343 
344  def _split_stroke(self, group, fill_layer, out_parent):
345  if not group.stroke:# or group.stroke_above:
346  return
347 
348  style = self._get_group_stroke(group)
349  if style.get("stroke-width", 0) <= 0 or style["stroke-opacity"] <= 0:
350  return
351 
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
359 
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
370 
371  g.append(use)
372  g.append(fill_layer)
373 
374  use.attrib[self.qualified("xlink", "href")] = "#" + fill_layer.attrib["id"]
375  use.attrib["style"] = self._style_to_css(style)
376  return g
377 
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))
386 
387  if group.fill.fill_rule:
388  style["fill-rule"] = "evenodd" if group.fill.fill_rule == objects.FillRule.EvenOdd else "nonzero"
389 
390  if group.lottie.hidden:
391  style["display"] = "none"
392  #if group.stroke_above:
393  #style.update(self._get_group_stroke(group))
394 
395  return self._style_to_css(style)
396 
397  def process_gradient(self, gradient):
398  spos = gradient.start_point.get_value(self.time)
399  epos = gradient.end_point.get_value(self.time)
400 
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)
416 
417  id = self.set_id(dom, gradient, force=True)
418  dom.attrib["gradientUnits"] = "userSpaceOnUse"
419 
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])
426 
427  return id
428 
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
436 
437  def _on_shapegroup(self, group, dom_parent):
438  if group.empty():
439  return
440 
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)
447 
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)
452 
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
459 
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)
478 
479  if shape.hidden:
480  svgshape.attrib["style"] += "display: none;"
481  return svgshape
482 
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
493 
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
503 
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)
516 
517  path.attrib["d"] = d
518  return path
519 
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
525 
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
533 
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  )
549 
550  return d
551 
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
562 
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
567 
568  ncopies = int(round(shape.copies.get_value(self.time)))
569  if ncopies == 1:
570  return
571 
572  out_parent.remove(original)
573 
574  g = ElementTree.SubElement(out_parent, "g")
575  self.set_clean_id(g, "repeater")
576 
577  for copy in range(ncopies-1):
578  use = ElementTree.SubElement(g, "use")
579  use.attrib[self.qualified("xlink", "href")] = "#" + original.attrib["id"]
580 
581  orig_wrapper = ElementTree.SubElement(g, "g")
582  orig_wrapper.append(original)
583 
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
597 
598  return g
599 
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)
603 
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]
611 
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
616 
617  multidata = {}
618  length = 0
619 
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
626 
627  return self._modifier_process(
628  child, shapegroup, out_parent, self._build_trim_path_shape,
629  start+offset, end+offset, multidata, length
630  )
631 
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
642 
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]
646 
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))
650 
651  def _build_trim_path_shape(self, shape, start, end, multidata, total_length):
652  if not isinstance(shape, objects.Shape):
653  return [shape]
654 
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
666 
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)]
675 
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)]
685 
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
691 
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)
706 
707  def _custom_object_supported(self, shape):
708  if has_font and isinstance(shape, font.FontShape):
709  return True
710  return False
711 
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"
723 
724  text.attrib["font-size"] = str(shape.size)
725 
726  text.attrib["white-space"] = "pre"
727 
728  pos = shape.style.position
729  text.attrib["x"] = str(pos.x)
730  text.attrib["y"] = str(pos.y)
731  text.text = shape.text
732 
733  return text
734 
735 
736 def 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]))
742 
743 
744 def 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")))
748 
749  if glaxnimate_helpers.has_glaxnimate:
750  data = glaxnimate_helpers.serialize(animation, "svg")
751  return ElementTree.ElementTree(ElementTree.fromstring(data.decode("utf8")))
752 
753  builder = SvgBuilder(time)
754  builder.process(animation)
755  return builder.dom
lottie.parsers.svg.builder.SvgBuilder.group_to_style
def group_to_style(self, group)
Definition: builder.py:378
lottie.parsers.svg.builder.SvgBuilder.actual_time
actual_time
Definition: builder.py:64
lottie.parsers.svg.builder.SvgBuilder.gen_id
def gen_id(self, prefix="id")
Definition: builder.py:79
lottie.parsers.svg.builder.SvgBuilder.build_ellipse
def build_ellipse(self, shape, parent)
Definition: builder.py:494
lottie.parsers.svg.handler.SvgHandler.ns_map
dictionary ns_map
Definition: handler.py:6
lottie.parsers.svg.handler.SvgHandler
Definition: handler.py:5
lottie.parsers.svg.builder.SvgBuilder.precomp_times
precomp_times
Definition: builder.py:65
lottie.parsers.svg.builder.SvgBuilder.ids
ids
Definition: builder.py:61
lottie.parsers.svg.builder.PrecompTime.pcl
pcl
Definition: builder.py:35
lottie.objects.layers.NullLayer
Layer with no data, useful to group layers together.
Definition: layers.py:167
lottie.parsers.svg.builder.SvgBuilder._on_masks
def _on_masks(self, masks)
Definition: builder.py:158
lottie.parsers.svg.builder.SvgBuilder._assets
_assets
Definition: builder.py:67
lottie.objects.layers.ImageLayer
Definition: layers.py:219
lottie.objects.shapes.GradientStroke
Gradient stroke.
Definition: shapes.py:666
lottie.parsers.svg.builder.SvgBuilder._trim_offlocal
def _trim_offlocal(self, t, local_start, local_length, total_length)
Definition: builder.py:647
lottie.parsers.svg.builder.SvgBuilder.svg
svg
Definition: builder.py:58
lottie.parsers.svg.builder.SvgBuilder.build_repeater
def build_repeater(self, shape, child, shapegroup, out_parent)
Definition: builder.py:563
lottie.objects.shapes.Path
Animatable Bezier curve.
Definition: shapes.py:398
lottie.parsers.svg.builder.SvgBuilder._mask_to_def
def _mask_to_def(self, mask)
Definition: builder.py:134
lottie.objects.shapes.Trim
Definition: shapes.py:728
lottie.objects.shapes.Repeater
Definition: shapes.py:754
lottie.parsers.svg.builder.SvgBuilder.build_rouded_corners
def build_rouded_corners(self, shape, child, shapegroup, out_parent)
Definition: builder.py:600
lottie.parsers.svg.builder.PrecompTime.__init__
def __init__(self, objects.PreCompLayer pcl)
Definition: builder.py:34
lottie.parsers.svg.builder.SvgBuilder._modifier_foreach_shape
def _modifier_foreach_shape(self, shape)
Definition: builder.py:632
lottie.parsers.svg.builder.SvgBuilder.build_path
def build_path(self, shapes, parent)
Definition: builder.py:504
lottie.parsers.svg.builder.SvgBuilder._modifier_process_children
def _modifier_process_children(self, shapegroup, out_parent, callback, *args)
Definition: builder.py:686
lottie.parsers.svg.builder.SvgBuilder.build_trim_path
def build_trim_path(self, shape, child, shapegroup, out_parent)
Definition: builder.py:612
lottie.parsers.svg.builder.SvgBuilder
Definition: builder.py:46
lottie.objects.shapes.Rect
A simple rectangle shape.
Definition: shapes.py:149
lottie.parsers.svg.builder.SvgBuilder._modifier_process
def _modifier_process(self, child, shapegroup, out_parent, callback, *args)
Definition: builder.py:643
lottie.parsers.svg.builder.SvgBuilder.group_from_lottie
def group_from_lottie(self, lottie, dom_parent, layer)
Definition: builder.py:429
lottie.objects.assets.Image
External image.
Definition: assets.py:26
lottie.objects.layers.PreCompLayer
Definition: layers.py:233
lottie.parsers.svg.builder.SvgBuilder._style_to_css
def _style_to_css(self, style)
Definition: builder.py:338
lottie.objects.layers.TextLayer
Definition: layers.py:179
lottie.parsers.svg.builder.SvgBuilder.build_rect
def build_rect(self, shape, parent)
Definition: builder.py:483
lottie.parsers.svg.builder.SvgBuilder._matte_source_to_def
def _matte_source_to_def(self, layer_builder)
Definition: builder.py:145
lottie.parsers.svg.builder.to_svg
def to_svg(animation, time, animated=False)
Definition: builder.py:744
lottie.parsers.svg.builder.SvgBuilder.defs
defs
Definition: builder.py:120
lottie.parsers.svg.builder.SvgBuilder._split_stroke
def _split_stroke(self, group, fill_layer, out_parent)
Definition: builder.py:344
lottie.parsers.svg.builder.SvgBuilder._build_trim_path_shape
def _build_trim_path_shape(self, shape, start, end, multidata, total_length)
Definition: builder.py:651
lottie.parsers.svg.builder.SvgBuilder._fonts
_fonts
Definition: builder.py:68
lottie.parsers.svg.builder.SvgBuilder.set_id
def set_id(self, dom, lottieobj, inkscape_qual=None, force=False)
Definition: builder.py:98
lottie.parsers.svg.builder.SvgBuilder.name_mode
name_mode
Definition: builder.py:63
lottie.objects.shapes.GradientFill
Gradient fill.
Definition: shapes.py:563
lottie.parsers.svg.builder.SvgBuilder._build_rouded_corners_shape
def _build_rouded_corners_shape(self, shape, round_amount)
Definition: builder.py:604
lottie.parsers.svg.builder.SvgBuilder._get_group_stroke
def _get_group_stroke(self, group)
Definition: builder.py:296
lottie.parsers.svg.builder.SvgBuilder.time
def time(self)
Definition: builder.py:72
lottie.parsers.svg.builder.SvgBuilder._bezier_tangent
def _bezier_tangent(self, tangent)
Definition: builder.py:520
lottie.parsers.svg.builder.SvgBuilder._bezier_to_d
def _bezier_to_d(self, bez)
Definition: builder.py:526
lottie.parsers.svg.builder.SvgBuilder.dom
dom
Definition: builder.py:59
lottie.objects.shapes.Ellipse
Ellipse shape.
Definition: shapes.py:335
lottie.parsers.svg.builder.SvgBuilder.idc
idc
Definition: builder.py:62
lottie.parsers.svg.builder.PrecompTime
Definition: builder.py:33
lottie.objects.shapes.RoundedCorners
Definition: shapes.py:781
lottie.parsers.svg.builder.SvgBuilder.build_text
def build_text(self, shape, parent)
Definition: builder.py:712
lottie.parsers.svg.builder.color_to_css
def color_to_css(color)
Definition: builder.py:736
lottie.objects.helpers.Transform
Layer transform.
Definition: helpers.py:7
lottie.parsers.svg.builder.SvgBuilder._precomps
_precomps
Definition: builder.py:66
lottie.parsers.svg.builder.SvgBuilder.set_clean_id
def set_clean_id(self, dom, n)
Definition: builder.py:88
lottie.parsers.svg.builder.PrecompTime.get_time_offset
def get_time_offset(self, time, lot)
Definition: builder.py:37
lottie.parsers.svg.builder.SvgBuilder.process_gradient
def process_gradient(self, gradient)
Definition: builder.py:397
lottie.parsers.svg.builder.SvgBuilder.id_re
id_re
Definition: builder.py:54
lottie.parsers.svg.builder.SvgBuilder._on_text_layer
def _on_text_layer(self, g, lot)
Definition: builder.py:235
lottie.objects.shapes.Star
Star shape.
Definition: shapes.py:239
lottie.parsers.svg.builder.SvgBuilder.__init__
def __init__(self, time=0)
Definition: builder.py:56
lottie.parsers.svg.handler.SvgHandler.qualified
def qualified(self, ns, name)
Definition: handler.py:20
lottie.objects.layers.SolidColorLayer
Layer with a solid color rectangle.
Definition: layers.py:259
lottie.parsers.svg.builder.SvgBuilder.set_transform
def set_transform(self, dom, transform, auto_orient=False)
Definition: builder.py:284
lottie.parsers.svg.builder.SvgBuilder._current_layer
_current_layer
Definition: builder.py:69
lottie.nvector.NVector
Definition: nvector.py:9
lottie.parsers.svg.builder.SvgBuilder._modifier_process_child
def _modifier_process_child(self, shape, shapegroup, out_parent, callback, *args)
Definition: builder.py:692
lottie.objects.shapes.Shape
Drawable shape.
Definition: shapes.py:128