File indexing completed on 2025-01-19 03:59:53

0001 import re
0002 import math
0003 from xml.etree import ElementTree
0004 
0005 from .handler import SvgHandler, NameMode
0006 from ... import objects
0007 from ...nvector import NVector
0008 from ...utils import restructure
0009 from ...utils.transform import TransformMatrix
0010 try:
0011     from ...utils import font
0012     has_font = True
0013 except ImportError:
0014     has_font = False
0015 
0016 
0017 class PrecompTime:
0018     def __init__(self, pcl: objects.PreCompLayer):
0019         self.pcl = pcl
0020 
0021     def get_time_offset(self, time, lot):
0022         remap = time
0023         if self.pcl.time_remapping:
0024             remapf = self.pcl.time_remapping.get_value(time)
0025             remap = lot.in_point * (1-remapf) + lot.out_point * remapf
0026 
0027         return remap - self.pcl.start_time
0028 
0029 
0030 class SvgBuilder(SvgHandler, restructure.AbstractBuilder):
0031     merge_paths = True
0032     namestart = (
0033         r":_A-Za-z\xC0-\xD6\xD8-\xF6\xF8-\u02FF\u0370-\u037D\u037F-\u1FFF" +
0034         r"\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF" +
0035         r"\uFDF0-\uFFFD\U00010000-\U000EFFFF"
0036     )
0037     namenostart = r"-.0-9\xB7\u0300-\u036F\u203F-\u2040"
0038     id_re = re.compile("^[%s][%s%s]*$" % (namestart, namenostart, namestart))
0039 
0040     def __init__(self, time=0):
0041         super().__init__()
0042         self.svg = ElementTree.Element("svg")
0043         self.dom = ElementTree.ElementTree(self.svg)
0044         self.svg.attrib["xmlns"] = self.ns_map["svg"]
0045         self.ids = set()
0046         self.idc = 0
0047         self.name_mode = NameMode.Inkscape
0048         self.actual_time = time
0049         self.precomp_times = []
0050         self._precomps = {}
0051         self._assets = {}
0052         self._current_layer = []
0053 
0054     @property
0055     def time(self):
0056         time = self.actual_time
0057         if self.precomp_times:
0058             for pct in self.precomp_times:
0059                 time = pct.get_time_offset(time, self._current_layer[-1])
0060         return time
0061 
0062     def gen_id(self, prefix="id"):
0063         while True:
0064             self.idc += 1
0065             id = "%s_%s" % (prefix, self.idc)
0066             if id not in self.ids:
0067                 break
0068         self.ids.add(id)
0069         return id
0070 
0071     def set_clean_id(self, dom, n):
0072         idn = n.replace(" ", "_")
0073         if self.id_re.match(idn) and idn not in self.ids:
0074             self.ids.add(idn)
0075         else:
0076             idn = self.gen_id(dom.tag)
0077 
0078         dom.attrib["id"] = idn
0079         return idn
0080 
0081     def set_id(self, dom, lottieobj, inkscape_qual=None, force=False):
0082         n = getattr(lottieobj, "name", None)
0083         if n is None or self.name_mode == NameMode.NoName:
0084             if force:
0085                 id = self.gen_id(dom.tag)
0086                 dom.attrib["id"] = id
0087                 return id
0088             return None
0089 
0090         idn = self.set_clean_id(dom, n)
0091         if inkscape_qual is None:
0092             inkscape_qual = self.qualified("inkscape", "label")
0093         if inkscape_qual:
0094             dom.attrib[inkscape_qual] = n
0095         return idn
0096 
0097     def _on_animation(self, animation: objects.Animation):
0098         self.svg.attrib["width"] = str(animation.width)
0099         self.svg.attrib["height"] = str(animation.height)
0100         self.svg.attrib["viewBox"] = "0 0 %s %s" % (animation.width, animation.height)
0101         self.svg.attrib["version"] = "1.1"
0102         self.set_id(self.svg, animation, self.qualified("sodipodi", "docname"))
0103         self.defs = ElementTree.SubElement(self.svg, "defs")
0104         if self.name_mode == NameMode.Inkscape:
0105             self.svg.attrib[self.qualified("inkscape", "export-xdpi")] = "96"
0106             self.svg.attrib[self.qualified("inkscape", "export-ydpi")] = "96"
0107             namedview = ElementTree.SubElement(self.svg, self.qualified("sodipodi", "namedview"))
0108             namedview.attrib[self.qualified("inkscape", "pagecheckerboard")] = "true"
0109             namedview.attrib["borderlayer"] = "true"
0110             namedview.attrib["bordercolor"] = "#666666"
0111             namedview.attrib["pagecolor"] = "#ffffff"
0112         self.svg.attrib["style"] = "fill: none; stroke: none"
0113 
0114         self._current_layer = [animation]
0115         return self.svg
0116 
0117     def _mask_to_def(self, mask):
0118         svgmask = ElementTree.SubElement(self.defs, "mask")
0119         mask_id = self.gen_id()
0120         svgmask.attrib["id"] = mask_id
0121         svgmask.attrib["mask-type"] = "alpha"
0122         path = ElementTree.SubElement(svgmask, "path")
0123         path.attrib["d"] = self._bezier_to_d(mask.shape.get_value(self.time))
0124         path.attrib["fill"] = "#fff"
0125         path.attrib["fill-opacity"] = str(mask.opacity.get_value(self.time) / 100)
0126         return mask_id
0127 
0128     def _matte_source_to_def(self, layer_builder):
0129         svgmask = ElementTree.SubElement(self.defs, "mask")
0130         if not layer_builder.matte_id:
0131             layer_builder.matte_id = self.gen_id()
0132         svgmask.attrib["id"] = layer_builder.matte_id
0133         matte_mode = layer_builder.matte_target.lottie.matte_mode
0134 
0135         mask_type = "alpha"
0136         if matte_mode == objects.MatteMode.Luma:
0137             mask_type = "luminance"
0138         svgmask.attrib["mask-type"] = mask_type
0139         return svgmask
0140 
0141     def _on_masks(self, masks):
0142         if len(masks) == 1:
0143             return self._mask_to_def(masks[0])
0144         mask_ids = list(map(self._mask_to_def, masks))
0145         mask_def = ElementTree.SubElement(self.defs, "mask")
0146         mask_id = self.gen_id()
0147         mask_def.attrib["id"] = mask_id
0148         g = mask_def
0149         for mid in mask_ids:
0150             g = ElementTree.SubElement(g, "g")
0151             g.attrib["mask"] = "url(#%s)" % mid
0152         full = ElementTree.SubElement(g, "rect")
0153         full.attrib["fill"] = "#fff"
0154         full.attrib["width"] = self.svg.attrib["width"]
0155         full.attrib["height"] = self.svg.attrib["height"]
0156         full.attrib["x"] = "0"
0157         full.attrib["y"] = "0"
0158         return mask_id
0159 
0160     def _on_layer(self, layer_builder, dom_parent):
0161         lot = layer_builder.lottie
0162         self._current_layer.append(lot)
0163 
0164         if not self.precomp_times and (lot.in_point > self.time or lot.out_point < self.time):
0165             self._current_layer.pop()
0166             return None
0167 
0168         if layer_builder.matte_target:
0169             dom_parent = self._matte_source_to_def(layer_builder)
0170 
0171         g = self.group_from_lottie(lot, dom_parent, True)
0172 
0173         if lot.masks:
0174             g.attrib["mask"] = "url(#%s)" % self._on_masks(lot.masks)
0175         elif layer_builder.matte_source:
0176             matte_id = layer_builder.matte_source.matte_id
0177             if not matte_id:
0178                 matte_id = layer_builder.matte_source.matte_id = self.gen_id()
0179             g.attrib["mask"] = "url(#%s)" % matte_id
0180 
0181         if isinstance(lot, objects.PreCompLayer):
0182             self.precomp_times.append(PrecompTime(lot))
0183 
0184             for layer in self._precomps.get(lot.reference_id, []):
0185                 self.process_layer(layer, g)
0186 
0187             self.precomp_times.pop()
0188         elif isinstance(lot, objects.NullLayer):
0189             g.attrib["opacity"] = "1"
0190         elif isinstance(lot, objects.ImageLayer):
0191             use = ElementTree.SubElement(g, "use")
0192             use.attrib[self.qualified("xlink", "href")] = "#" + self._assets[lot.image_id]
0193         elif isinstance(lot, objects.TextLayer):
0194             self._on_text_layer(g, lot)
0195         elif isinstance(lot, objects.SolidColorLayer):
0196             rect = ElementTree.SubElement(g, "rect")
0197             rect.attrib["width"] = str(lot.width)
0198             rect.attrib["height"] = str(lot.height)
0199             rect.attrib["fill"] = lot.color
0200 
0201         if not lot.name:
0202             g.attrib[self.qualified("inkscape", "label")] = lot.__class__.__name__
0203         if layer_builder.shapegroup:
0204             g.attrib["style"] = self.group_to_style(layer_builder.shapegroup)
0205             self._split_stroke(layer_builder.shapegroup, g, dom_parent)
0206         #if lot.hidden:
0207             #g.attrib.setdefault("style", "")
0208             #g.attrib["style"] += "display: none;"
0209 
0210         return g
0211 
0212     def _on_text_layer(self, g, lot):
0213         text = ElementTree.SubElement(g, "text")
0214         doc = lot.data.get_value(self.time)
0215         if doc:
0216             text.attrib["font-family"] = doc.font_family
0217             text.attrib["font-size"] = str(doc.font_size)
0218             if doc.line_height:
0219                 text.attrib["line-height"] = "%s%%" % doc.line_height
0220             if doc.justify == objects.text.TextJustify.Left:
0221                 text.attrib["text-align"] = "start"
0222             elif doc.justify == objects.text.TextJustify.Center:
0223                 text.attrib["text-align"] = "center"
0224             elif doc.justify == objects.text.TextJustify.Right:
0225                 text.attrib["text-align"] = "end"
0226 
0227             text.attrib["fill"] = color_to_css(doc.color)
0228             text.text = doc.text
0229 
0230     def _on_layer_end(self, out_layer):
0231         self._current_layer.pop()
0232 
0233     def _on_precomp(self, id, dom_parent, layers):
0234         self._precomps[id] = layers
0235 
0236     def _on_asset(self, asset):
0237         if isinstance(asset, objects.assets.Image):
0238             img = ElementTree.SubElement(self.defs, "image")
0239             xmlid = self.set_clean_id(img, asset.id)
0240             self._assets[asset.id] = xmlid
0241             if asset.is_embedded:
0242                 url = asset.image
0243             else:
0244                 url = asset.image_path + asset.image
0245             img.attrib[self.qualified("xlink", "href")] = url
0246             img.attrib["width"] = str(asset.width)
0247             img.attrib["height"] = str(asset.height)
0248 
0249     def _get_value(self, prop, default=NVector(0, 0)):
0250         if prop:
0251             v = prop.get_value(self.time)
0252         else:
0253             v = default
0254 
0255         if v is None:
0256             return default
0257         if isinstance(v, NVector):
0258             return v.clone()
0259         return v
0260 
0261     def set_transform(self, dom, transform, auto_orient=False):
0262         mat = transform.to_matrix(self.time, auto_orient)
0263         dom.attrib["transform"] = mat.to_css_2d()
0264 
0265         if transform.opacity is not None:
0266             op = transform.opacity.get_value(self.time)
0267             if op != 100:
0268                 dom.attrib["opacity"] = str(op/100)
0269 
0270     def _get_group_stroke(self, group):
0271         style = {}
0272         if group.stroke:
0273             if isinstance(group.stroke, objects.GradientStroke):
0274                 style["stroke"] = "url(#%s)" % self.process_gradient(group.stroke)
0275             else:
0276                 style["stroke"] = color_to_css(group.stroke.color.get_value(self.time))
0277 
0278             style["stroke-opacity"] = group.stroke.opacity.get_value(self.time) / 100
0279             style["stroke-width"] = group.stroke.width.get_value(self.time)
0280             if group.stroke.miter_limit is not None:
0281                 style["stroke-miterlimit"] = group.stroke.miter_limit
0282 
0283             if group.stroke.line_cap == objects.LineCap.Round:
0284                 style["stroke-linecap"] = "round"
0285             elif group.stroke.line_cap == objects.LineCap.Butt:
0286                 style["stroke-linecap"] = "butt"
0287             elif group.stroke.line_cap == objects.LineCap.Square:
0288                 style["stroke-linecap"] = "square"
0289 
0290             if group.stroke.line_join == objects.LineJoin.Round:
0291                 style["stroke-linejoin"] = "round"
0292             elif group.stroke.line_join == objects.LineJoin.Bevel:
0293                 style["stroke-linejoin"] = "bevel"
0294             elif group.stroke.line_join == objects.LineJoin.Miter:
0295                 style["stroke-linejoin"] = "miter"
0296 
0297             if group.stroke.dashes:
0298                 dasharray = []
0299                 last = 0
0300                 last_mode = objects.StrokeDashType.Dash
0301                 for dash in group.stroke.dashes:
0302                     if last_mode == dash.type:
0303                         last += dash.length.get_value(self.time)
0304                     else:
0305                         if last_mode != objects.StrokeDashType.Offset:
0306                             dasharray.append(str(last))
0307                             last = 0
0308                         last_mode = dash.type
0309                 style["stroke-dasharray"] = " ".join(dasharray)
0310         return style
0311 
0312     def _style_to_css(self, style):
0313         return ";".join(map(
0314             lambda x: ":".join(map(str, x)),
0315             style.items()
0316         ))
0317 
0318     def _split_stroke(self, group, fill_layer, out_parent):
0319         if not group.stroke:# or group.stroke_above:
0320             return
0321 
0322         style = self._get_group_stroke(group)
0323         if style.get("stroke-width", 0) <= 0 or style["stroke-opacity"] <= 0:
0324             return
0325 
0326         if group.stroke_above:
0327             if fill_layer.attrib.get("style", ""):
0328                 fill_layer.attrib["style"] += ";"
0329             else:
0330                 fill_layer.attrib["style"] = ""
0331             fill_layer.attrib["style"] += self._style_to_css(style)
0332             return fill_layer
0333 
0334         g = ElementTree.Element("g")
0335         self.set_clean_id(g, "stroke")
0336         use = ElementTree.Element("use")
0337         for i, e in enumerate(out_parent):
0338             if e is fill_layer:
0339                 out_parent.insert(i, g)
0340                 out_parent.remove(fill_layer)
0341                 break
0342         else:
0343             return
0344 
0345         g.append(use)
0346         g.append(fill_layer)
0347 
0348         use.attrib[self.qualified("xlink", "href")] = "#" + fill_layer.attrib["id"]
0349         use.attrib["style"] = self._style_to_css(style)
0350         return g
0351 
0352     def group_to_style(self, group):
0353         style = {}
0354         if group.fill:
0355             style["fill-opacity"] = group.fill.opacity.get_value(self.time) / 100
0356             if isinstance(group.fill, objects.GradientFill):
0357                 style["fill"] = "url(#%s)" % self.process_gradient(group.fill)
0358             else:
0359                 style["fill"] = color_to_css(group.fill.color.get_value(self.time))
0360 
0361             if group.fill.fill_rule:
0362                 style["fill-rule"] = "evenodd" if group.fill.fill_rule == objects.FillRule.EvenOdd else "nonzero"
0363 
0364         if group.lottie.hidden:
0365             style["display"] = "none"
0366         #if group.stroke_above:
0367             #style.update(self._get_group_stroke(group))
0368 
0369         return self._style_to_css(style)
0370 
0371     def process_gradient(self, gradient):
0372         spos = gradient.start_point.get_value(self.time)
0373         epos = gradient.end_point.get_value(self.time)
0374 
0375         if gradient.gradient_type == objects.GradientType.Linear:
0376             dom = ElementTree.SubElement(self.defs, "linearGradient")
0377             dom.attrib["x1"] = str(spos[0])
0378             dom.attrib["y1"] = str(spos[1])
0379             dom.attrib["x2"] = str(epos[0])
0380             dom.attrib["y2"] = str(epos[1])
0381         elif gradient.gradient_type == objects.GradientType.Radial:
0382             dom = ElementTree.SubElement(self.defs, "radialGradient")
0383             dom.attrib["cx"] = str(spos[0])
0384             dom.attrib["cy"] = str(spos[1])
0385             dom.attrib["r"] = str((epos-spos).length)
0386             a = gradient.highlight_angle.get_value(self.time) * math.pi / 180
0387             l = gradient.highlight_length.get_value(self.time)
0388             dom.attrib["fx"] = str(spos[0] + math.cos(a) * l)
0389             dom.attrib["fy"] = str(spos[1] + math.sin(a) * l)
0390 
0391         id = self.set_id(dom, gradient, force=True)
0392         dom.attrib["gradientUnits"] = "userSpaceOnUse"
0393 
0394         for off, color in gradient.colors.stops_at(self.time):
0395             stop = ElementTree.SubElement(dom, "stop")
0396             stop.attrib["offset"] = "%s%%" % (off * 100)
0397             stop.attrib["stop-color"] = color_to_css(color[:3])
0398             if len(color) > 3:
0399                 stop.attrib["stop-opacity"] = str(color[3])
0400 
0401         return id
0402 
0403     def group_from_lottie(self, lottie, dom_parent, layer):
0404         g = ElementTree.SubElement(dom_parent, "g")
0405         if layer and self.name_mode == NameMode.Inkscape:
0406             g.attrib[self.qualified("inkscape", "groupmode")] = "layer"
0407         self.set_id(g, lottie, force=True)
0408         self.set_transform(g, lottie.transform, getattr(lottie, "auto_orient", False))
0409         return g
0410 
0411     def _on_shapegroup(self, group, dom_parent):
0412         if group.empty():
0413             return
0414 
0415         if len(group.children) == 1 and isinstance(group.children[0], restructure.RestructuredPathMerger):
0416             path = self.build_path(group.paths.paths, dom_parent)
0417             self.set_id(path, group.paths.paths[0], force=True)
0418             path.attrib["style"] = self.group_to_style(group)
0419             self.set_transform(path, group.lottie.transform)
0420             return self._split_stroke(group, path, dom_parent)
0421 
0422         g = self.group_from_lottie(group.lottie, dom_parent, group.layer)
0423         g.attrib["style"] = self.group_to_style(group)
0424         self.shapegroup_process_children(group, g)
0425         return self._split_stroke(group, g, dom_parent)
0426 
0427     def _on_merged_path(self, shape, shapegroup, out_parent):
0428         path = self.build_path(shape.paths, out_parent)
0429         self.set_id(path, shape.paths[0])
0430         path.attrib["style"] = self.group_to_style(shapegroup)
0431         #self._split_stroke(shapegroup, path, out_parent)
0432         return path
0433 
0434     def _on_shape(self, shape, shapegroup, out_parent):
0435         if isinstance(shape, objects.Rect):
0436             svgshape = self.build_rect(shape, out_parent)
0437         elif isinstance(shape, objects.Ellipse):
0438             svgshape = self.build_ellipse(shape, out_parent)
0439         elif isinstance(shape, objects.Star):
0440             svgshape = self.build_path([shape.to_bezier()], out_parent)
0441         elif isinstance(shape, objects.Path):
0442             svgshape = self.build_path([shape], out_parent)
0443         elif has_font and isinstance(shape, font.FontShape):
0444             svgshape = self.build_text(shape, out_parent)
0445         else:
0446             return
0447         self.set_id(svgshape, shape, force=True)
0448         if "style" not in svgshape.attrib:
0449             svgshape.attrib["style"] = ""
0450         svgshape.attrib["style"] += self.group_to_style(shapegroup)
0451         #self._split_stroke(shapegroup, svgshape, out_parent)
0452 
0453         if shape.hidden:
0454             svgshape.attrib["style"] += "display: none;"
0455         return svgshape
0456 
0457     def build_rect(self, shape, parent):
0458         rect = ElementTree.SubElement(parent, "rect")
0459         size = shape.size.get_value(self.time)
0460         pos = shape.position.get_value(self.time)
0461         rect.attrib["width"] = str(size[0])
0462         rect.attrib["height"] = str(size[1])
0463         rect.attrib["x"] = str(pos[0] - size[0] / 2)
0464         rect.attrib["y"] = str(pos[1] - size[1] / 2)
0465         rect.attrib["rx"] = str(shape.rounded.get_value(self.time))
0466         return rect
0467 
0468     def build_ellipse(self, shape, parent):
0469         ellipse = ElementTree.SubElement(parent, "ellipse")
0470         size = shape.size.get_value(self.time)
0471         pos = shape.position.get_value(self.time)
0472         ellipse.attrib["rx"] = str(size[0] / 2)
0473         ellipse.attrib["ry"] = str(size[1] / 2)
0474         ellipse.attrib["cx"] = str(pos[0])
0475         ellipse.attrib["cy"] = str(pos[1])
0476         return ellipse
0477 
0478     def build_path(self, shapes, parent):
0479         path = ElementTree.SubElement(parent, "path")
0480         d = ""
0481         for shape in shapes:
0482             bez = shape.shape.get_value(self.time)
0483             if isinstance(bez, list):
0484                 bez = bez[0]
0485             if not bez.vertices:
0486                 continue
0487             if d:
0488                 d += "\n"
0489             d += self._bezier_to_d(bez)
0490 
0491         path.attrib["d"] = d
0492         return path
0493 
0494     def _bezier_tangent(self, tangent):
0495         _tangent_threshold = 0.5
0496         if tangent.length < _tangent_threshold:
0497             return NVector(0, 0)
0498         return tangent
0499 
0500     def _bezier_to_d(self, bez):
0501         d = "M %s,%s " % tuple(bez.vertices[0].components[:2])
0502         for i in range(1, len(bez.vertices)):
0503             qfrom = bez.vertices[i-1]
0504             h1 = self._bezier_tangent(bez.out_tangents[i-1]) + qfrom
0505             qto = bez.vertices[i]
0506             h2 = self._bezier_tangent(bez.in_tangents[i]) + qto
0507 
0508             d += "C %s,%s %s,%s %s,%s " % (
0509                 h1[0], h1[1],
0510                 h2[0], h2[1],
0511                 qto[0], qto[1],
0512             )
0513         if bez.closed:
0514             qfrom = bez.vertices[-1]
0515             h1 = self._bezier_tangent(bez.out_tangents[-1]) + qfrom
0516             qto = bez.vertices[0]
0517             h2 = self._bezier_tangent(bez.in_tangents[0]) + qto
0518             d += "C %s,%s %s,%s %s,%s Z" % (
0519                 h1[0], h1[1],
0520                 h2[0], h2[1],
0521                 qto[0], qto[1],
0522             )
0523 
0524         return d
0525 
0526     def _on_shape_modifier(self, shape, shapegroup, out_parent):
0527         if isinstance(shape.lottie, objects.Repeater):
0528             svgshape = self.build_repeater(shape.lottie, shape.child, shapegroup, out_parent)
0529         elif isinstance(shape.lottie, objects.RoundedCorners):
0530             svgshape = self.build_rouded_corners(shape.lottie, shape.child, shapegroup, out_parent)
0531         elif isinstance(shape.lottie, objects.Trim):
0532             svgshape = self.build_trim_path(shape.lottie, shape.child, shapegroup, out_parent)
0533         else:
0534             return self.shapegroup_process_child(shape.child, shapegroup, out_parent)
0535         return svgshape
0536 
0537     def build_repeater(self, shape, child, shapegroup, out_parent):
0538         original = self.shapegroup_process_child(child, shapegroup, out_parent)
0539         if not original:
0540             return
0541 
0542         ncopies = int(round(shape.copies.get_value(self.time)))
0543         if ncopies == 1:
0544             return
0545 
0546         out_parent.remove(original)
0547 
0548         g = ElementTree.SubElement(out_parent, "g")
0549         self.set_clean_id(g, "repeater")
0550 
0551         for copy in range(ncopies-1):
0552             use = ElementTree.SubElement(g, "use")
0553             use.attrib[self.qualified("xlink", "href")] = "#" + original.attrib["id"]
0554 
0555         orig_wrapper = ElementTree.SubElement(g, "g")
0556         orig_wrapper.append(original)
0557 
0558         transform = objects.Transform()
0559         so = shape.transform.start_opacity.get_value(self.time)
0560         eo = shape.transform.end_opacity.get_value(self.time)
0561         position = shape.transform.position.get_value(self.time)
0562         rotation = shape.transform.rotation.get_value(self.time)
0563         anchor_point = shape.transform.anchor_point.get_value(self.time)
0564         for i in range(ncopies-1, -1, -1):
0565             of = i / (ncopies-1)
0566             transform.opacity.value = so * of + eo * (1 - of)
0567             self.set_transform(g[i], transform)
0568             transform.position.value += position
0569             transform.rotation.value += rotation
0570             transform.anchor_point.value += anchor_point
0571 
0572         return g
0573 
0574     def build_rouded_corners(self, shape, child, shapegroup, out_parent):
0575         round_amount = shape.radius.get_value(self.time)
0576         return self._modifier_process(child, shapegroup, out_parent, self._build_rouded_corners_shape, round_amount)
0577 
0578     def _build_rouded_corners_shape(self, shape, round_amount):
0579         if not isinstance(shape, objects.Shape):
0580             return [shape]
0581         path = shape.to_bezier()
0582         bezier = path.shape.get_value(self.time).rounded(round_amount)
0583         path.shape.clear_animation(bezier)
0584         return [path]
0585 
0586     def build_trim_path(self, shape, child, shapegroup, out_parent):
0587         start = max(0, min(1, shape.start.get_value(self.time) / 100))
0588         end = max(0, min(1, shape.end.get_value(self.time) / 100))
0589         offset = shape.offset.get_value(self.time) / 360 % 1
0590 
0591         multidata = {}
0592         length = 0
0593 
0594         if shape.multiple == objects.TrimMultipleShapes.Individually:
0595             for visishape in reversed(list(self._modifier_foreach_shape(child))):
0596                 bez = visishape.to_bezier().shape.get_value(self.time)
0597                 local_length = bez.rough_length()
0598                 multidata[visishape] = (bez, length, local_length)
0599                 length += local_length
0600 
0601         return self._modifier_process(
0602             child, shapegroup, out_parent, self._build_trim_path_shape,
0603             start+offset, end+offset, multidata, length
0604         )
0605 
0606     def _modifier_foreach_shape(self, shape):
0607         if isinstance(shape, restructure.RestructuredShapeGroup):
0608             for child in shape.children:
0609                 for chsh in self._modifier_foreach_shape(child):
0610                     yield chsh
0611         elif isinstance(shape, restructure.RestructuredPathMerger):
0612             for p in shape.paths:
0613                 yield p
0614         elif isinstance(shape, objects.Shape):
0615             yield shape
0616 
0617     def _modifier_process(self, child, shapegroup, out_parent, callback, *args):
0618         children = self._modifier_process_child(child, shapegroup, out_parent, callback, *args)
0619         return [self.shapegroup_process_child(ch, shapegroup, out_parent) for ch in children]
0620 
0621     def _trim_offlocal(self, t, local_start, local_length, total_length):
0622         gt = (t * total_length - local_start) / local_length
0623         return max(0, min(1, gt))
0624 
0625     def _build_trim_path_shape(self, shape, start, end, multidata, total_length):
0626         if not isinstance(shape, objects.Shape):
0627             return [shape]
0628 
0629         if multidata:
0630             bezier, local_start, local_length = multidata[shape]
0631             if end > 1:
0632                 lstart = self._trim_offlocal(start, local_start, local_length, total_length)
0633                 lend = self._trim_offlocal(end-1, local_start, local_length, total_length)
0634                 out = []
0635                 if lstart < 1:
0636                     out.append(objects.Path(bezier.segment(lstart, 1)))
0637                 if lend > 0:
0638                     out.append(objects.Path(bezier.segment(0, lend)))
0639                 return out
0640 
0641             lstart = self._trim_offlocal(start, local_start, local_length, total_length)
0642             lend = self._trim_offlocal(end, local_start, local_length, total_length)
0643             if lend <= 0 or lstart >= 1:
0644                 return []
0645             if lstart <= 0 and lend >= 1:
0646                 return [objects.Path(bezier)]
0647             seg = bezier.segment(lstart, lend)
0648             return [objects.Path(seg)]
0649 
0650         path = shape.to_bezier()
0651         bezier = path.shape.get_value(self.time)
0652         if end > 1:
0653             bez1 = bezier.segment(start, 1)
0654             bez2 = bezier.segment(0, end-1)
0655             return [objects.Path(bez1), objects.Path(bez2)]
0656         else:
0657             seg = bezier.segment(start, end)
0658             return [objects.Path(seg)]
0659 
0660     def _modifier_process_children(self, shapegroup, out_parent, callback, *args):
0661         children = []
0662         for shape in shapegroup.children:
0663             children.extend(self._modifier_process_child(shape, shapegroup, out_parent, callback, *args))
0664         shapegroup.children = children
0665 
0666     def _modifier_process_child(self, shape, shapegroup, out_parent, callback, *args):
0667         if isinstance(shape, restructure.RestructuredShapeGroup):
0668             self._modifier_process_children(shape, out_parent, callback, *args)
0669             return [shape]
0670         elif isinstance(shape, restructure.RestructuredPathMerger):
0671             paths = []
0672             for p in shape.paths:
0673                 paths.extend(callback(p, *args))
0674             shape.paths = paths
0675             if paths:
0676                 return [shape]
0677             return []
0678         else:
0679             return callback(shape, *args)
0680 
0681     def _custom_object_supported(self, shape):
0682         if has_font and isinstance(shape, font.FontShape):
0683             return True
0684         return False
0685 
0686     def build_text(self, shape, parent):
0687         text = ElementTree.SubElement(parent, "text")
0688         if "family" in shape.query:
0689             text.attrib["font-family"] = shape.query["family"]
0690         if "weight" in shape.query:
0691             text.attrib["font-weight"] = str(shape.query.weight_to_css())
0692         slant = int(shape.query.get("slant", 0))
0693         if slant > 0 and slant < 110:
0694             text.attrib["font-style"] = "italic"
0695         elif slant >= 110:
0696             text.attrib["font-style"] = "oblique"
0697 
0698         text.attrib["font-size"] = str(shape.size)
0699 
0700         text.attrib["white-space"] = "pre"
0701 
0702         pos = shape.style.position
0703         text.attrib["x"] = str(pos.x)
0704         text.attrib["y"] = str(pos.y)
0705         text.text = shape.text
0706 
0707         return text
0708 
0709 
0710 def color_to_css(color):
0711     #if len(color) == 4:
0712         #return ("rgba(%s, %s, %s" % tuple(map(lambda c: int(round(c*255)), color[:3]))) + ", %s)" % color[3]
0713     return "rgb(%s, %s, %s)" % tuple(map(lambda c: int(round(c*255)), color[:3]))
0714 
0715 
0716 def to_svg(animation, time):
0717     builder = SvgBuilder(time)
0718     builder.process(animation)
0719     return builder.dom