Coverage for lib/lottie/parsers/svg/builder.py: 8%

586 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2023-03-20 16:17 +0100

1import re 

2import math 

3from xml.etree import ElementTree 

4 

5from .handler import SvgHandler, NameMode 

6from ... import objects 

7from ...nvector import NVector 

8from ...utils import restructure 

9from ...utils.transform import TransformMatrix 

10from ...parsers import glaxnimate_helpers 

11from ...utils.color import Color, ColorMode 

12try: 

13 from ...utils import font 

14 has_font = True 

15except ImportError: 

16 has_font = False 

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 

33class PrecompTime: 

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 

46class SvgBuilder(SvgHandler, restructure.AbstractBuilder): 

47 merge_paths = True 

48 namestart = ( 

49 r":_A-Za-z\xC0-\xD6\xD8-\xF6\xF8-\u02FF\u0370-\u037D\u037F-\u1FFF" + 

50 r"\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF" + 

51 r"\uFDF0-\uFFFD\U00010000-\U000EFFFF" 

52 ) 

53 namenostart = r"-.0-9\xB7\u0300-\u036F\u203F-\u2040" 

54 id_re = re.compile("^[%s][%s%s]*$" % (namestart, namenostart, namestart)) 

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.path + asset.file_name 

268 img.attrib[self.qualified("xlink", "href")] = url 

269 img.attrib["width"] = str(asset.width) 

270 img.attrib["height"] = str(asset.height) 

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 

736def color_to_css(color): 

737 #if len(color) == 4: 

738 #return ("rgba(%s, %s, %s" % tuple(map(lambda c: int(round(c*255)), color[:3]))) + ", %s)" % color[3] 

739 if isinstance(color, Color) and color.mode != ColorMode.RGB: 

740 color = color.converted(ColorMode.RGB) 

741 return "rgb(%s, %s, %s)" % tuple(map(lambda c: int(round(c*255)), color[:3])) 

742 

743 

744def to_svg(animation, time, animated=False): 

745 if animated: 

746 data = glaxnimate_helpers.convert(animation, "svg") 

747 return ElementTree.ElementTree(ElementTree.fromstring(data.decode("utf8"))) 

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