Files
OmniLottie/lottie/parsers/svg/builder.py
T

720 lines
28 KiB
Python
Raw Normal View History

2026-03-01 21:36:54 +08:00
import re
import math
from xml.etree import ElementTree
from .handler import SvgHandler, NameMode
from ... import objects
from ...nvector import NVector
from ...utils import restructure
from ...utils.transform import TransformMatrix
try:
from ...utils import font
has_font = True
except ImportError:
has_font = False
class PrecompTime:
def __init__(self, pcl: objects.PreCompLayer):
self.pcl = pcl
def get_time_offset(self, time, lot):
remap = time
if self.pcl.time_remapping:
remapf = self.pcl.time_remapping.get_value(time)
remap = lot.in_point * (1-remapf) + lot.out_point * remapf
return remap - self.pcl.start_time
class SvgBuilder(SvgHandler, restructure.AbstractBuilder):
merge_paths = True
namestart = (
r":_A-Za-z\xC0-\xD6\xD8-\xF6\xF8-\u02FF\u0370-\u037D\u037F-\u1FFF" +
r"\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF" +
r"\uFDF0-\uFFFD\U00010000-\U000EFFFF"
)
namenostart = r"-.0-9\xB7\u0300-\u036F\u203F-\u2040"
id_re = re.compile("^[%s][%s%s]*$" % (namestart, namenostart, namestart))
def __init__(self, time=0):
super().__init__()
self.svg = ElementTree.Element("svg")
self.dom = ElementTree.ElementTree(self.svg)
self.svg.attrib["xmlns"] = self.ns_map["svg"]
self.ids = set()
self.idc = 0
self.name_mode = NameMode.Inkscape
self.actual_time = time
self.precomp_times = []
self._precomps = {}
self._assets = {}
self._current_layer = []
@property
def time(self):
time = self.actual_time
if self.precomp_times:
for pct in self.precomp_times:
time = pct.get_time_offset(time, self._current_layer[-1])
return time
def gen_id(self, prefix="id"):
while True:
self.idc += 1
id = "%s_%s" % (prefix, self.idc)
if id not in self.ids:
break
self.ids.add(id)
return id
def set_clean_id(self, dom, n):
idn = n.replace(" ", "_")
if self.id_re.match(idn) and idn not in self.ids:
self.ids.add(idn)
else:
idn = self.gen_id(dom.tag)
dom.attrib["id"] = idn
return idn
def set_id(self, dom, lottieobj, inkscape_qual=None, force=False):
n = getattr(lottieobj, "name", None)
if n is None or self.name_mode == NameMode.NoName:
if force:
id = self.gen_id(dom.tag)
dom.attrib["id"] = id
return id
return None
idn = self.set_clean_id(dom, n)
if inkscape_qual is None:
inkscape_qual = self.qualified("inkscape", "label")
if inkscape_qual:
dom.attrib[inkscape_qual] = n
return idn
def _on_animation(self, animation: objects.Animation):
self.svg.attrib["width"] = str(animation.width)
self.svg.attrib["height"] = str(animation.height)
self.svg.attrib["viewBox"] = "0 0 %s %s" % (animation.width, animation.height)
self.svg.attrib["version"] = "1.1"
self.set_id(self.svg, animation, self.qualified("sodipodi", "docname"))
self.defs = ElementTree.SubElement(self.svg, "defs")
if self.name_mode == NameMode.Inkscape:
self.svg.attrib[self.qualified("inkscape", "export-xdpi")] = "96"
self.svg.attrib[self.qualified("inkscape", "export-ydpi")] = "96"
namedview = ElementTree.SubElement(self.svg, self.qualified("sodipodi", "namedview"))
namedview.attrib[self.qualified("inkscape", "pagecheckerboard")] = "true"
namedview.attrib["borderlayer"] = "true"
namedview.attrib["bordercolor"] = "#666666"
namedview.attrib["pagecolor"] = "#ffffff"
self.svg.attrib["style"] = "fill: none; stroke: none"
self._current_layer = [animation]
return self.svg
def _mask_to_def(self, mask):
svgmask = ElementTree.SubElement(self.defs, "mask")
mask_id = self.gen_id()
svgmask.attrib["id"] = mask_id
svgmask.attrib["mask-type"] = "alpha"
path = ElementTree.SubElement(svgmask, "path")
path.attrib["d"] = self._bezier_to_d(mask.shape.get_value(self.time))
path.attrib["fill"] = "#fff"
path.attrib["fill-opacity"] = str(mask.opacity.get_value(self.time) / 100)
return mask_id
def _matte_source_to_def(self, layer_builder):
svgmask = ElementTree.SubElement(self.defs, "mask")
if not layer_builder.matte_id:
layer_builder.matte_id = self.gen_id()
svgmask.attrib["id"] = layer_builder.matte_id
matte_mode = layer_builder.matte_target.lottie.matte_mode
mask_type = "alpha"
if matte_mode == objects.MatteMode.Luma:
mask_type = "luminance"
svgmask.attrib["mask-type"] = mask_type
return svgmask
def _on_masks(self, masks):
if len(masks) == 1:
return self._mask_to_def(masks[0])
mask_ids = list(map(self._mask_to_def, masks))
mask_def = ElementTree.SubElement(self.defs, "mask")
mask_id = self.gen_id()
mask_def.attrib["id"] = mask_id
g = mask_def
for mid in mask_ids:
g = ElementTree.SubElement(g, "g")
g.attrib["mask"] = "url(#%s)" % mid
full = ElementTree.SubElement(g, "rect")
full.attrib["fill"] = "#fff"
full.attrib["width"] = self.svg.attrib["width"]
full.attrib["height"] = self.svg.attrib["height"]
full.attrib["x"] = "0"
full.attrib["y"] = "0"
return mask_id
def _on_layer(self, layer_builder, dom_parent):
lot = layer_builder.lottie
self._current_layer.append(lot)
if not self.precomp_times and (lot.in_point > self.time or lot.out_point < self.time):
self._current_layer.pop()
return None
if layer_builder.matte_target:
dom_parent = self._matte_source_to_def(layer_builder)
g = self.group_from_lottie(lot, dom_parent, True)
if lot.masks:
g.attrib["mask"] = "url(#%s)" % self._on_masks(lot.masks)
elif layer_builder.matte_source:
matte_id = layer_builder.matte_source.matte_id
if not matte_id:
matte_id = layer_builder.matte_source.matte_id = self.gen_id()
g.attrib["mask"] = "url(#%s)" % matte_id
if isinstance(lot, objects.PreCompLayer):
self.precomp_times.append(PrecompTime(lot))
for layer in self._precomps.get(lot.reference_id, []):
self.process_layer(layer, g)
self.precomp_times.pop()
elif isinstance(lot, objects.NullLayer):
g.attrib["opacity"] = "1"
elif isinstance(lot, objects.ImageLayer):
use = ElementTree.SubElement(g, "use")
use.attrib[self.qualified("xlink", "href")] = "#" + self._assets[lot.image_id]
elif isinstance(lot, objects.TextLayer):
self._on_text_layer(g, lot)
elif isinstance(lot, objects.SolidColorLayer):
rect = ElementTree.SubElement(g, "rect")
rect.attrib["width"] = str(lot.width)
rect.attrib["height"] = str(lot.height)
rect.attrib["fill"] = lot.color
if not lot.name:
g.attrib[self.qualified("inkscape", "label")] = lot.__class__.__name__
if layer_builder.shapegroup:
g.attrib["style"] = self.group_to_style(layer_builder.shapegroup)
self._split_stroke(layer_builder.shapegroup, g, dom_parent)
#if lot.hidden:
#g.attrib.setdefault("style", "")
#g.attrib["style"] += "display: none;"
return g
def _on_text_layer(self, g, lot):
text = ElementTree.SubElement(g, "text")
doc = lot.data.get_value(self.time)
if doc:
text.attrib["font-family"] = doc.font_family
text.attrib["font-size"] = str(doc.font_size)
if doc.line_height:
text.attrib["line-height"] = "%s%%" % doc.line_height
if doc.justify == objects.text.TextJustify.Left:
text.attrib["text-align"] = "start"
elif doc.justify == objects.text.TextJustify.Center:
text.attrib["text-align"] = "center"
elif doc.justify == objects.text.TextJustify.Right:
text.attrib["text-align"] = "end"
text.attrib["fill"] = color_to_css(doc.color)
text.text = doc.text
def _on_layer_end(self, out_layer):
self._current_layer.pop()
def _on_precomp(self, id, dom_parent, layers):
self._precomps[id] = layers
def _on_asset(self, asset):
if isinstance(asset, objects.assets.Image):
img = ElementTree.SubElement(self.defs, "image")
xmlid = self.set_clean_id(img, asset.id)
self._assets[asset.id] = xmlid
if asset.is_embedded:
url = asset.image
else:
url = asset.image_path + asset.image
img.attrib[self.qualified("xlink", "href")] = url
img.attrib["width"] = str(asset.width)
img.attrib["height"] = str(asset.height)
def _get_value(self, prop, default=NVector(0, 0)):
if prop:
v = prop.get_value(self.time)
else:
v = default
if v is None:
return default
if isinstance(v, NVector):
return v.clone()
return v
def set_transform(self, dom, transform, auto_orient=False):
mat = transform.to_matrix(self.time, auto_orient)
dom.attrib["transform"] = mat.to_css_2d()
if transform.opacity is not None:
op = transform.opacity.get_value(self.time)
if op != 100:
dom.attrib["opacity"] = str(op/100)
def _get_group_stroke(self, group):
style = {}
if group.stroke:
if isinstance(group.stroke, objects.GradientStroke):
style["stroke"] = "url(#%s)" % self.process_gradient(group.stroke)
else:
style["stroke"] = color_to_css(group.stroke.color.get_value(self.time))
style["stroke-opacity"] = group.stroke.opacity.get_value(self.time) / 100
style["stroke-width"] = group.stroke.width.get_value(self.time)
if group.stroke.miter_limit is not None:
style["stroke-miterlimit"] = group.stroke.miter_limit
if group.stroke.line_cap == objects.LineCap.Round:
style["stroke-linecap"] = "round"
elif group.stroke.line_cap == objects.LineCap.Butt:
style["stroke-linecap"] = "butt"
elif group.stroke.line_cap == objects.LineCap.Square:
style["stroke-linecap"] = "square"
if group.stroke.line_join == objects.LineJoin.Round:
style["stroke-linejoin"] = "round"
elif group.stroke.line_join == objects.LineJoin.Bevel:
style["stroke-linejoin"] = "bevel"
elif group.stroke.line_join == objects.LineJoin.Miter:
style["stroke-linejoin"] = "miter"
if group.stroke.dashes:
dasharray = []
last = 0
last_mode = objects.StrokeDashType.Dash
for dash in group.stroke.dashes:
if last_mode == dash.type:
last += dash.length.get_value(self.time)
else:
if last_mode != objects.StrokeDashType.Offset:
dasharray.append(str(last))
last = 0
last_mode = dash.type
style["stroke-dasharray"] = " ".join(dasharray)
return style
def _style_to_css(self, style):
return ";".join(map(
lambda x: ":".join(map(str, x)),
style.items()
))
def _split_stroke(self, group, fill_layer, out_parent):
if not group.stroke:# or group.stroke_above:
return
style = self._get_group_stroke(group)
if style.get("stroke-width", 0) <= 0 or style["stroke-opacity"] <= 0:
return
if group.stroke_above:
if fill_layer.attrib.get("style", ""):
fill_layer.attrib["style"] += ";"
else:
fill_layer.attrib["style"] = ""
fill_layer.attrib["style"] += self._style_to_css(style)
return fill_layer
g = ElementTree.Element("g")
self.set_clean_id(g, "stroke")
use = ElementTree.Element("use")
for i, e in enumerate(out_parent):
if e is fill_layer:
out_parent.insert(i, g)
out_parent.remove(fill_layer)
break
else:
return
g.append(use)
g.append(fill_layer)
use.attrib[self.qualified("xlink", "href")] = "#" + fill_layer.attrib["id"]
use.attrib["style"] = self._style_to_css(style)
return g
def group_to_style(self, group):
style = {}
if group.fill:
style["fill-opacity"] = group.fill.opacity.get_value(self.time) / 100
if isinstance(group.fill, objects.GradientFill):
style["fill"] = "url(#%s)" % self.process_gradient(group.fill)
else:
style["fill"] = color_to_css(group.fill.color.get_value(self.time))
if group.fill.fill_rule:
style["fill-rule"] = "evenodd" if group.fill.fill_rule == objects.FillRule.EvenOdd else "nonzero"
if group.lottie.hidden:
style["display"] = "none"
#if group.stroke_above:
#style.update(self._get_group_stroke(group))
return self._style_to_css(style)
def process_gradient(self, gradient):
spos = gradient.start_point.get_value(self.time)
epos = gradient.end_point.get_value(self.time)
if gradient.gradient_type == objects.GradientType.Linear:
dom = ElementTree.SubElement(self.defs, "linearGradient")
dom.attrib["x1"] = str(spos[0])
dom.attrib["y1"] = str(spos[1])
dom.attrib["x2"] = str(epos[0])
dom.attrib["y2"] = str(epos[1])
elif gradient.gradient_type == objects.GradientType.Radial:
dom = ElementTree.SubElement(self.defs, "radialGradient")
dom.attrib["cx"] = str(spos[0])
dom.attrib["cy"] = str(spos[1])
dom.attrib["r"] = str((epos-spos).length)
a = gradient.highlight_angle.get_value(self.time) * math.pi / 180
l = gradient.highlight_length.get_value(self.time)
dom.attrib["fx"] = str(spos[0] + math.cos(a) * l)
dom.attrib["fy"] = str(spos[1] + math.sin(a) * l)
id = self.set_id(dom, gradient, force=True)
dom.attrib["gradientUnits"] = "userSpaceOnUse"
for off, color in gradient.colors.stops_at(self.time):
stop = ElementTree.SubElement(dom, "stop")
stop.attrib["offset"] = "%s%%" % (off * 100)
stop.attrib["stop-color"] = color_to_css(color[:3])
if len(color) > 3:
stop.attrib["stop-opacity"] = str(color[3])
return id
def group_from_lottie(self, lottie, dom_parent, layer):
g = ElementTree.SubElement(dom_parent, "g")
if layer and self.name_mode == NameMode.Inkscape:
g.attrib[self.qualified("inkscape", "groupmode")] = "layer"
self.set_id(g, lottie, force=True)
self.set_transform(g, lottie.transform, getattr(lottie, "auto_orient", False))
return g
def _on_shapegroup(self, group, dom_parent):
if group.empty():
return
if len(group.children) == 1 and isinstance(group.children[0], restructure.RestructuredPathMerger):
path = self.build_path(group.paths.paths, dom_parent)
self.set_id(path, group.paths.paths[0], force=True)
path.attrib["style"] = self.group_to_style(group)
self.set_transform(path, group.lottie.transform)
return self._split_stroke(group, path, dom_parent)
g = self.group_from_lottie(group.lottie, dom_parent, group.layer)
g.attrib["style"] = self.group_to_style(group)
self.shapegroup_process_children(group, g)
return self._split_stroke(group, g, dom_parent)
def _on_merged_path(self, shape, shapegroup, out_parent):
path = self.build_path(shape.paths, out_parent)
self.set_id(path, shape.paths[0])
path.attrib["style"] = self.group_to_style(shapegroup)
#self._split_stroke(shapegroup, path, out_parent)
return path
def _on_shape(self, shape, shapegroup, out_parent):
if isinstance(shape, objects.Rect):
svgshape = self.build_rect(shape, out_parent)
elif isinstance(shape, objects.Ellipse):
svgshape = self.build_ellipse(shape, out_parent)
elif isinstance(shape, objects.Star):
svgshape = self.build_path([shape.to_bezier()], out_parent)
elif isinstance(shape, objects.Path):
svgshape = self.build_path([shape], out_parent)
elif has_font and isinstance(shape, font.FontShape):
svgshape = self.build_text(shape, out_parent)
else:
return
self.set_id(svgshape, shape, force=True)
if "style" not in svgshape.attrib:
svgshape.attrib["style"] = ""
svgshape.attrib["style"] += self.group_to_style(shapegroup)
#self._split_stroke(shapegroup, svgshape, out_parent)
if shape.hidden:
svgshape.attrib["style"] += "display: none;"
return svgshape
def build_rect(self, shape, parent):
rect = ElementTree.SubElement(parent, "rect")
size = shape.size.get_value(self.time)
pos = shape.position.get_value(self.time)
rect.attrib["width"] = str(size[0])
rect.attrib["height"] = str(size[1])
rect.attrib["x"] = str(pos[0] - size[0] / 2)
rect.attrib["y"] = str(pos[1] - size[1] / 2)
rect.attrib["rx"] = str(shape.rounded.get_value(self.time))
return rect
def build_ellipse(self, shape, parent):
ellipse = ElementTree.SubElement(parent, "ellipse")
size = shape.size.get_value(self.time)
pos = shape.position.get_value(self.time)
ellipse.attrib["rx"] = str(size[0] / 2)
ellipse.attrib["ry"] = str(size[1] / 2)
ellipse.attrib["cx"] = str(pos[0])
ellipse.attrib["cy"] = str(pos[1])
return ellipse
def build_path(self, shapes, parent):
path = ElementTree.SubElement(parent, "path")
d = ""
for shape in shapes:
bez = shape.shape.get_value(self.time)
if isinstance(bez, list):
bez = bez[0]
if not bez.vertices:
continue
if d:
d += "\n"
d += self._bezier_to_d(bez)
path.attrib["d"] = d
return path
def _bezier_tangent(self, tangent):
_tangent_threshold = 0.5
if tangent.length < _tangent_threshold:
return NVector(0, 0)
return tangent
def _bezier_to_d(self, bez):
d = "M %s,%s " % tuple(bez.vertices[0].components[:2])
for i in range(1, len(bez.vertices)):
qfrom = bez.vertices[i-1]
h1 = self._bezier_tangent(bez.out_tangents[i-1]) + qfrom
qto = bez.vertices[i]
h2 = self._bezier_tangent(bez.in_tangents[i]) + qto
d += "C %s,%s %s,%s %s,%s " % (
h1[0], h1[1],
h2[0], h2[1],
qto[0], qto[1],
)
if bez.closed:
qfrom = bez.vertices[-1]
h1 = self._bezier_tangent(bez.out_tangents[-1]) + qfrom
qto = bez.vertices[0]
h2 = self._bezier_tangent(bez.in_tangents[0]) + qto
d += "C %s,%s %s,%s %s,%s Z" % (
h1[0], h1[1],
h2[0], h2[1],
qto[0], qto[1],
)
return d
def _on_shape_modifier(self, shape, shapegroup, out_parent):
if isinstance(shape.lottie, objects.Repeater):
svgshape = self.build_repeater(shape.lottie, shape.child, shapegroup, out_parent)
elif isinstance(shape.lottie, objects.RoundedCorners):
svgshape = self.build_rouded_corners(shape.lottie, shape.child, shapegroup, out_parent)
elif isinstance(shape.lottie, objects.Trim):
svgshape = self.build_trim_path(shape.lottie, shape.child, shapegroup, out_parent)
else:
return self.shapegroup_process_child(shape.child, shapegroup, out_parent)
return svgshape
def build_repeater(self, shape, child, shapegroup, out_parent):
original = self.shapegroup_process_child(child, shapegroup, out_parent)
if not original:
return
ncopies = int(round(shape.copies.get_value(self.time)))
if ncopies == 1:
return
out_parent.remove(original)
g = ElementTree.SubElement(out_parent, "g")
self.set_clean_id(g, "repeater")
for copy in range(ncopies-1):
use = ElementTree.SubElement(g, "use")
use.attrib[self.qualified("xlink", "href")] = "#" + original.attrib["id"]
orig_wrapper = ElementTree.SubElement(g, "g")
orig_wrapper.append(original)
transform = objects.Transform()
so = shape.transform.start_opacity.get_value(self.time)
eo = shape.transform.end_opacity.get_value(self.time)
position = shape.transform.position.get_value(self.time)
rotation = shape.transform.rotation.get_value(self.time)
anchor_point = shape.transform.anchor_point.get_value(self.time)
for i in range(ncopies-1, -1, -1):
of = i / (ncopies-1)
transform.opacity.value = so * of + eo * (1 - of)
self.set_transform(g[i], transform)
transform.position.value += position
transform.rotation.value += rotation
transform.anchor_point.value += anchor_point
return g
def build_rouded_corners(self, shape, child, shapegroup, out_parent):
round_amount = shape.radius.get_value(self.time)
return self._modifier_process(child, shapegroup, out_parent, self._build_rouded_corners_shape, round_amount)
def _build_rouded_corners_shape(self, shape, round_amount):
if not isinstance(shape, objects.Shape):
return [shape]
path = shape.to_bezier()
bezier = path.shape.get_value(self.time).rounded(round_amount)
path.shape.clear_animation(bezier)
return [path]
def build_trim_path(self, shape, child, shapegroup, out_parent):
start = max(0, min(1, shape.start.get_value(self.time) / 100))
end = max(0, min(1, shape.end.get_value(self.time) / 100))
offset = shape.offset.get_value(self.time) / 360 % 1
multidata = {}
length = 0
if shape.multiple == objects.TrimMultipleShapes.Individually:
for visishape in reversed(list(self._modifier_foreach_shape(child))):
bez = visishape.to_bezier().shape.get_value(self.time)
local_length = bez.rough_length()
multidata[visishape] = (bez, length, local_length)
length += local_length
return self._modifier_process(
child, shapegroup, out_parent, self._build_trim_path_shape,
start+offset, end+offset, multidata, length
)
def _modifier_foreach_shape(self, shape):
if isinstance(shape, restructure.RestructuredShapeGroup):
for child in shape.children:
for chsh in self._modifier_foreach_shape(child):
yield chsh
elif isinstance(shape, restructure.RestructuredPathMerger):
for p in shape.paths:
yield p
elif isinstance(shape, objects.Shape):
yield shape
def _modifier_process(self, child, shapegroup, out_parent, callback, *args):
children = self._modifier_process_child(child, shapegroup, out_parent, callback, *args)
return [self.shapegroup_process_child(ch, shapegroup, out_parent) for ch in children]
def _trim_offlocal(self, t, local_start, local_length, total_length):
gt = (t * total_length - local_start) / local_length
return max(0, min(1, gt))
def _build_trim_path_shape(self, shape, start, end, multidata, total_length):
if not isinstance(shape, objects.Shape):
return [shape]
if multidata:
bezier, local_start, local_length = multidata[shape]
if end > 1:
lstart = self._trim_offlocal(start, local_start, local_length, total_length)
lend = self._trim_offlocal(end-1, local_start, local_length, total_length)
out = []
if lstart < 1:
out.append(objects.Path(bezier.segment(lstart, 1)))
if lend > 0:
out.append(objects.Path(bezier.segment(0, lend)))
return out
lstart = self._trim_offlocal(start, local_start, local_length, total_length)
lend = self._trim_offlocal(end, local_start, local_length, total_length)
if lend <= 0 or lstart >= 1:
return []
if lstart <= 0 and lend >= 1:
return [objects.Path(bezier)]
seg = bezier.segment(lstart, lend)
return [objects.Path(seg)]
path = shape.to_bezier()
bezier = path.shape.get_value(self.time)
if end > 1:
bez1 = bezier.segment(start, 1)
bez2 = bezier.segment(0, end-1)
return [objects.Path(bez1), objects.Path(bez2)]
else:
seg = bezier.segment(start, end)
return [objects.Path(seg)]
def _modifier_process_children(self, shapegroup, out_parent, callback, *args):
children = []
for shape in shapegroup.children:
children.extend(self._modifier_process_child(shape, shapegroup, out_parent, callback, *args))
shapegroup.children = children
def _modifier_process_child(self, shape, shapegroup, out_parent, callback, *args):
if isinstance(shape, restructure.RestructuredShapeGroup):
self._modifier_process_children(shape, out_parent, callback, *args)
return [shape]
elif isinstance(shape, restructure.RestructuredPathMerger):
paths = []
for p in shape.paths:
paths.extend(callback(p, *args))
shape.paths = paths
if paths:
return [shape]
return []
else:
return callback(shape, *args)
def _custom_object_supported(self, shape):
if has_font and isinstance(shape, font.FontShape):
return True
return False
def build_text(self, shape, parent):
text = ElementTree.SubElement(parent, "text")
if "family" in shape.query:
text.attrib["font-family"] = shape.query["family"]
if "weight" in shape.query:
text.attrib["font-weight"] = str(shape.query.weight_to_css())
slant = int(shape.query.get("slant", 0))
if slant > 0 and slant < 110:
text.attrib["font-style"] = "italic"
elif slant >= 110:
text.attrib["font-style"] = "oblique"
text.attrib["font-size"] = str(shape.size)
text.attrib["white-space"] = "pre"
pos = shape.style.position
text.attrib["x"] = str(pos.x)
text.attrib["y"] = str(pos.y)
text.text = shape.text
return text
def color_to_css(color):
#if len(color) == 4:
#return ("rgba(%s, %s, %s" % tuple(map(lambda c: int(round(c*255)), color[:3]))) + ", %s)" % color[3]
return "rgb(%s, %s, %s)" % tuple(map(lambda c: int(round(c*255)), color[:3]))
def to_svg(animation, time):
builder = SvgBuilder(time)
builder.process(animation)
return builder.dom