Coverage for lib/lottie/parsers/svg/importer.py: 50%

978 statements  

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

1import re 

2import math 

3import colorsys 

4from xml.etree import ElementTree 

5from ... import objects 

6from ...nvector import NVector 

7from .svgdata import color_table, css_atrrs 

8from .handler import SvgHandler, NameMode 

9from ...utils.ellipse import Ellipse 

10from ...utils.transform import TransformMatrix 

11from ...utils.color import Color 

12 

13try: 

14 from ...utils import font 

15 has_font = True 

16except ImportError: 

17 has_font = False 

18 

19nocolor = {"none"} 

20 

21 

22class SvgGradientCoord: 

23 def __init__(self, name, comp, value, percent): 

24 self.name = name 

25 self.comp = comp 

26 self.value = value 

27 self.percent = percent 

28 

29 def to_value(self, bbox, default=None): 

30 if self.value is None: 

31 return default 

32 

33 if not self.percent: 

34 return self.value 

35 

36 if self.comp == "w": 

37 return (bbox.x2 - bbox.x1) * self.value 

38 

39 if self.comp == "x": 

40 return bbox.x1 + (bbox.x2 - bbox.x1) * self.value 

41 

42 return bbox.y1 + (bbox.y2 - bbox.y1) * self.value 

43 

44 def parse(self, attr, default_percent): 

45 if attr is None: 

46 return 

47 if attr.endswith("%"): 

48 self.percent = True 

49 self.value = float(attr[:-1])/100 

50 else: 

51 self.percent = default_percent 

52 self.value = float(attr) 

53 

54 

55class SvgGradient: 

56 def __init__(self): 

57 self.colors = [] 

58 self.coords = [] 

59 self.matrix = TransformMatrix() 

60 

61 def add_color(self, offset, color): 

62 self.colors.append((offset, color[:4])) 

63 

64 def to_lottie(self, gradient_shape, shape, time=0): 

65 """! 

66 @param gradient_shape Should be a GradientFill or GradientStroke 

67 @param shape ShapeElement to apply the gradient to 

68 @param time Time to fetch properties from @p shape 

69 

70 """ 

71 for off, col in self.colors: 

72 gradient_shape.colors.add_color(off, col) 

73 

74 def add_coord(self, value): 

75 setattr(self, value.name, value) 

76 self.coords.append(value) 

77 

78 def parse_attrs(self, attrib): 

79 relunits = attrib.get("gradientUnits", "") != "userSpaceOnUse" 

80 for c in self.coords: 

81 c.parse(attrib.get(c.name, None), relunits) 

82 

83 

84class SvgLinearGradient(SvgGradient): 

85 def __init__(self): 

86 super().__init__() 

87 self.add_coord(SvgGradientCoord("x1", "x", 0, True)) 

88 self.add_coord(SvgGradientCoord("y1", "y", 0, True)) 

89 self.add_coord(SvgGradientCoord("x2", "x", 1, True)) 

90 self.add_coord(SvgGradientCoord("y2", "y", 0, True)) 

91 

92 def to_lottie(self, gradient_shape, shape, time=0): 

93 bbox = shape.bounding_box(time) 

94 gradient_shape.start_point.value = self.matrix.apply(NVector( 

95 self.x1.to_value(bbox), 

96 self.y1.to_value(bbox), 

97 )) 

98 gradient_shape.end_point.value = self.matrix.apply(NVector( 

99 self.x2.to_value(bbox), 

100 self.y2.to_value(bbox), 

101 )) 

102 gradient_shape.gradient_type = objects.GradientType.Linear 

103 

104 super().to_lottie(gradient_shape, shape, time) 

105 

106 

107class SvgRadialGradient(SvgGradient): 

108 def __init__(self): 

109 super().__init__() 

110 self.add_coord(SvgGradientCoord("cx", "x", 0.5, True)) 

111 self.add_coord(SvgGradientCoord("cy", "y", 0.5, True)) 

112 self.add_coord(SvgGradientCoord("fx", "x", None, True)) 

113 self.add_coord(SvgGradientCoord("fy", "y", None, True)) 

114 self.add_coord(SvgGradientCoord("r", "w", 0.5, True)) 

115 

116 def to_lottie(self, gradient_shape, shape, time=0): 

117 bbox = shape.bounding_box(time) 

118 cx = self.cx.to_value(bbox) 

119 cy = self.cy.to_value(bbox) 

120 gradient_shape.start_point.value = self.matrix.apply(NVector(cx, cy)) 

121 r = self.r.to_value(bbox) 

122 gradient_shape.end_point.value = self.matrix.apply(NVector(cx+r, cy)) 

123 

124 fx = self.fx.to_value(bbox, cx) - cx 

125 fy = self.fy.to_value(bbox, cy) - cy 

126 gradient_shape.highlight_angle.value = math.atan2(fy, fx) * 180 / math.pi 

127 gradient_shape.highlight_length.value = math.hypot(fx, fy) 

128 

129 gradient_shape.gradient_type = objects.GradientType.Radial 

130 

131 super().to_lottie(gradient_shape, shape, time) 

132 

133 

134def parse_color(color, current_color=Color(0, 0, 0, 1)): 

135 """! 

136 Parses CSS colors 

137 @see https://www.w3.org/wiki/CSS/Properties/color 

138 """ 

139 # #112233 

140 if re.match(r"^#[0-9a-fA-F]{6}$", color): 

141 return Color(int(color[1:3], 16) / 0xff, int(color[3:5], 16) / 0xff, int(color[5:7], 16) / 0xff, 1) 

142 # #fff 

143 if re.match(r"^#[0-9a-fA-F]{3}$", color): 

144 return Color(int(color[1], 16) / 0xf, int(color[2], 16) / 0xf, int(color[3], 16) / 0xf, 1) 

145 # #112233aa 

146 if re.match(r"^#[0-9a-fA-F]{8}$", color): 146 ↛ 147line 146 didn't jump to line 147, because the condition on line 146 was never true

147 return Color( 

148 int(color[1:3], 16) / 0xff, 

149 int(color[3:5], 16) / 0xff, 

150 int(color[5:7], 16) / 0xff, 

151 int(color[7:9], 16) / 0xff 

152 ) 

153 # #fffa 

154 if re.match(r"^#[0-9a-fA-F]{4}$", color): 154 ↛ 155line 154 didn't jump to line 155, because the condition on line 154 was never true

155 return Color(int(color[1], 16) / 0xf, int(color[2], 16) / 0xf, int(color[3], 16) / 0xf, int(color[4], 16) / 0xf) 

156 # rgba(123, 123, 123, 0.7) 

157 match = re.match(r"^rgba\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9.eE]+)\s*\)$", color) 

158 if match: 

159 return Color(int(match[1])/255, int(match[2])/255, int(match[3])/255, float(match[4])) 

160 # rgb(123, 123, 123) 

161 match = re.match(r"^rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)$", color) 

162 if match: 

163 return Color(int(match[1])/255, int(match[2])/255, int(match[3])/255, 1) 

164 # rgb(60%, 30%, 20%) 

165 match = re.match(r"^rgb\s*\(\s*([0-9.eE]+)%\s*,\s*([0-9.eE]+)%\s*,\s*([0-9.eE]+)%\s*\)$", color) 

166 if match: 

167 return Color(float(match[1])/100, float(match[2])/100, float(match[3])/100, 1) 

168 # rgba(60%, 30%, 20%, 0.7) 

169 match = re.match(r"^rgba\s*\(\s*([0-9.eE]+)%\s*,\s*([0-9.eE]+)%\s*,\s*([0-9.eE]+)%\s*,\s*([0-9.eE]+)\s*\)$", color) 

170 if match: 

171 return Color(float(match[1])/100, float(match[2])/100, float(match[3])/100, float(match[4])) 

172 # transparent 

173 if color == "transparent": 

174 return Color(0, 0, 0, 0) 

175 # hsl(60, 30%, 20%) 

176 match = re.match(r"^hsl\s*\(\s*([0-9.eE]+)\s*,\s*([0-9.eE]+)%\s*,\s*([0-9.eE]+)%\s*\)$", color) 

177 if match: 

178 return Color(*(colorsys.hls_to_rgb(float(match[1])/360, float(match[3])/100, float(match[2])/100) + (1,))) 

179 # hsla(60, 30%, 20%, 0.7) 

180 match = re.match(r"^hsla\s*\(\s*([0-9.eE]+)\s*,\s*([0-9.eE]+)%\s*,\s*([0-9.eE]+)%\s*,\s*([0-9.eE]+)\s*\)$", color) 

181 if match: 

182 return Color(*(colorsys.hls_to_rgb(float(match[1])/360, float(match[3])/100, float(match[2])/100) + (float(match[4]),))) 

183 # currentColor 

184 if color in {"currentColor", "inherit"}: 

185 return current_color.clone() 

186 # red 

187 return Color(*color_table[color]) 

188 

189 

190class SvgDefsParent: 

191 def __init__(self): 

192 self.items = {} 

193 

194 def insert(self, dummy, shape): 

195 self.items[shape.name] = shape 

196 

197 def __getitem__(self, key): 

198 return self.items[key] 

199 

200 def __setitem__(self, key, value): 

201 self.items[key] = value 

202 

203 def __contains__(self, key): 

204 return key in self.items 

205 

206 @property 

207 def shapes(self): 

208 return self 

209 

210 

211class SvgParser(SvgHandler): 

212 def __init__(self, name_mode=NameMode.Inkscape): 

213 self.init_etree() 

214 self.name_mode = name_mode 

215 self.current_color = Color(0, 0, 0, 1) 

216 self.gradients = {} 

217 self.max_time = 0 

218 self.defs = SvgDefsParent() 

219 self.dpi = 96 

220 

221 def _get_name(self, element, inkscapequal): 

222 if self.name_mode == NameMode.Inkscape: 222 ↛ 224line 222 didn't jump to line 224, because the condition on line 222 was never false

223 return element.attrib.get(inkscapequal, element.attrib.get("id")) 

224 return self._get_id(element) 

225 

226 def _get_id(self, element): 

227 if self.name_mode != NameMode.NoName: 

228 return element.attrib.get("id") 

229 return None 

230 

231 def _parse_viewbox(self, attrib): 

232 return map(float, attrib.replace(",", " ").split()) 

233 

234 def parse_etree(self, etree, layer_frames=0, *args, **kwargs): 

235 animation = objects.Animation(*args, **kwargs) 

236 self.animation = animation 

237 self.max_time = 0 

238 self.document = etree 

239 

240 svg = etree.getroot() 

241 

242 self._get_dpi(svg) 

243 

244 if "width" in svg.attrib and "height" in svg.attrib: 244 ↛ 245line 244 didn't jump to line 245, because the condition on line 244 was never true

245 animation.width = int(round(self._parse_unit(svg.attrib["width"]))) 

246 animation.height = int(round(self._parse_unit(svg.attrib["height"]))) 

247 else: 

248 _, _, animation.width, animation.height = self._parse_viewbox(svg.attrib["viewBox"]) 

249 animation.name = self._get_name(svg, self.qualified("sodipodi", "docname")) 

250 

251 if layer_frames: 251 ↛ 252line 251 didn't jump to line 252, because the condition on line 251 was never true

252 for frame in svg: 

253 if self.unqualified(frame.tag) == "g": 

254 layer = objects.ShapeLayer() 

255 layer.in_point = self.max_time 

256 animation.add_layer(layer) 

257 self._parseshape_g(frame, layer, {}) 

258 self.max_time += layer_frames 

259 layer.out_point = self.max_time 

260 else: 

261 self._svg_to_layer(animation, svg) 

262 

263 if self.max_time: 263 ↛ 264line 263 didn't jump to line 264, because the condition on line 263 was never true

264 animation.out_point = self.max_time 

265 

266 self._fix_viewbox(svg, (layer for layer in animation.layers if not layer.parent_index)) 266 ↛ exitline 266 didn't run the generator expression on line 266

267 

268 return animation 

269 

270 def etree_to_layer(self, animation, etree): 

271 svg = etree.getroot() 

272 self._get_dpi(svg) 

273 layer = self._svg_to_layer(animation, svg) 

274 self._fix_viewbox(svg, [layer]) 

275 return layer 

276 

277 def _get_dpi(self, svg): 

278 self.dpi = float(svg.attrib.get(self.qualified("inkscape", "export-xdpi"), self.dpi)) 

279 

280 def _svg_to_layer(self, animation, svg): 

281 self.animation = animation 

282 layer = objects.ShapeLayer() 

283 animation.add_layer(layer) 

284 self.parse_children(svg, layer, self.parse_style(svg, {})) 

285 if self.max_time: 285 ↛ 286line 285 didn't jump to line 286, because the condition on line 285 was never true

286 for sublayer in layer.find_all(objects.Layer): 

287 sublayer.out_point = self.max_time 

288 return layer 

289 

290 def _fix_viewbox(self, svg, layers): 

291 if "viewBox" in svg.attrib: 291 ↛ exitline 291 didn't return from function '_fix_viewbox', because the condition on line 291 was never false

292 vbx, vby, vbw, vbh = self._parse_viewbox(svg.attrib["viewBox"]) 

293 if vbx != 0 or vby != 0 or vbw != self.animation.width or vbh != self.animation.height: 293 ↛ 294line 293 didn't jump to line 294, because the condition on line 293 was never true

294 for layer in layers: 

295 layer.transform.position.value = -NVector(vbx, vby) 

296 layer.transform.scale.value = NVector(self.animation.width / vbw, self.animation.height / vbh) * 100 

297 

298 def _parse_unit(self, value): 

299 if not isinstance(value, str): 

300 return value 

301 

302 mult = 1 

303 cmin = 2.54 

304 if value.endswith("px"): 

305 value = value[:-2] 

306 elif value.endswith("vw"): 

307 value = value[:-2] 

308 mult = self.animation.width * 0.01 

309 elif value.endswith("vh"): 

310 value = value[:-2] 

311 mult = self.animation.height * 0.01 

312 elif value.endswith("vmin"): 

313 value = value[:-4] 

314 mult = min(self.animation.width, self.animation.height) * 0.01 

315 elif value.endswith("vmax"): 

316 value = value[:-4] 

317 mult = max(self.animation.width, self.animation.height) * 0.01 

318 elif value.endswith("in"): 

319 value = value[:-2] 

320 mult = self.dpi 

321 elif value.endswith("pc"): 

322 value = value[:-2] 

323 mult = self.dpi / 6 

324 elif value.endswith("pt"): 

325 value = value[:-2] 

326 mult = self.dpi / 72 

327 elif value.endswith("cm"): 

328 value = value[:-2] 

329 mult = self.dpi / cmin 

330 elif value.endswith("mm"): 

331 value = value[:-2] 

332 mult = self.dpi / cmin / 10 

333 elif value.endswith("Q"): 

334 value = value[:-1] 

335 mult = self.dpi / cmin / 40 

336 

337 return float(value) * mult 

338 

339 def parse_color(self, color): 

340 return parse_color(color, self.current_color) 

341 

342 def parse_transform(self, element, group, dest_trans): 

343 bb = group.bounding_box() 

344 if not bb.isnull(): 344 ↛ 359line 344 didn't jump to line 359, because the condition on line 344 was never false

345 itcx = self.qualified("inkscape", "transform-center-x") 

346 if itcx in element.attrib: 

347 cx = float(element.attrib[itcx]) 

348 cy = float(element.attrib[self.qualified("inkscape", "transform-center-y")]) 

349 bbx, bby = bb.center() 

350 cx += bbx 

351 cy = bby - cy 

352 dest_trans.anchor_point.value = NVector(cx, cy) 

353 dest_trans.position.value = NVector(cx, cy) 

354 #else: 

355 #c = bb.center() 

356 #dest_trans.anchor_point.value = c 

357 #dest_trans.position.value = c.clone() 

358 

359 if "transform" not in element.attrib: 

360 return 

361 

362 matrix = TransformMatrix() 

363 read_matrix = False 

364 

365 for t in re.finditer(r"([a-zA-Z]+)\s*\(([^\)]*)\)", element.attrib["transform"]): 

366 name = t[1] 

367 params = list(map(float, t[2].strip().replace(",", " ").split())) 

368 if name == "translate": 

369 dest_trans.position.value += NVector( 

370 params[0], 

371 (params[1] if len(params) > 1 else 0), 

372 ) 

373 elif name == "scale": 

374 xfac = params[0] 

375 dest_trans.scale.value[0] = (dest_trans.scale.value[0] / 100 * xfac) * 100 

376 yfac = params[1] if len(params) > 1 else xfac 

377 dest_trans.scale.value[1] = (dest_trans.scale.value[1] / 100 * yfac) * 100 

378 elif name == "rotate": 

379 ang = params[0] 

380 x = y = 0 

381 if len(params) > 2: 

382 x = params[1] 

383 y = params[2] 

384 ap = NVector(x, y) 

385 dap = ap - dest_trans.position.value 

386 dest_trans.position.value += dap 

387 dest_trans.anchor_point.value += dap 

388 dest_trans.rotation.value = ang 

389 else: 

390 read_matrix = True 

391 self._apply_transform_element_to_matrix(matrix, t) 

392 

393 if read_matrix: 

394 dest_trans.position.value -= dest_trans.anchor_point.value 

395 dest_trans.anchor_point.value = NVector(0, 0) 

396 trans = matrix.extract_transform() 

397 dest_trans.skew_axis.value = math.degrees(trans["skew_axis"]) 

398 dest_trans.skew.value = -math.degrees(trans["skew_angle"]) 

399 dest_trans.position.value += trans["translation"] 

400 dest_trans.rotation.value -= math.degrees(trans["angle"]) 

401 dest_trans.scale.value *= trans["scale"] 

402 

403 def parse_style(self, element, parent_style): 

404 style = parent_style.copy() 

405 for att in css_atrrs & set(element.attrib.keys()): 

406 if att in element.attrib: 406 ↛ 405line 406 didn't jump to line 405, because the condition on line 406 was never false

407 style[att] = element.attrib[att] 

408 if "style" in element.attrib: 

409 style.update(**dict(map( 

410 lambda x: map(lambda y: y.strip(), x.split(":")), 

411 filter(bool, element.attrib["style"].split(";")) 

412 ))) 

413 return style 

414 

415 def apply_common_style(self, style, transform): 

416 opacity = float(style.get("opacity", 1)) 

417 transform.opacity.value = opacity * 100 

418 

419 def apply_visibility(self, style, object): 

420 if style.get("display", "inline") == "none" or style.get("visibility", "visible") == "hidden": 

421 object.hidden = True 

422 

423 def add_shapes(self, element, shapes, shape_parent, parent_style): 

424 style = self.parse_style(element, parent_style) 

425 

426 group = objects.Group() 

427 self.apply_common_style(style, group.transform) 

428 self.apply_visibility(style, group) 

429 group.name = self._get_name(element, self.qualified("inkscape", "label")) 

430 

431 shape_parent.shapes.insert(0, group) 

432 for shape in shapes: 

433 group.add_shape(shape) 

434 

435 self._add_style_shapes(style, group) 

436 

437 self.parse_transform(element, group, group.transform) 

438 

439 return group 

440 

441 def _add_style_shapes(self, style, group): 

442 stroke_color = style.get("stroke", "none") 

443 if stroke_color not in nocolor: 443 ↛ 487line 443 didn't jump to line 487, because the condition on line 443 was never false

444 if stroke_color.startswith("url"): 444 ↛ 445line 444 didn't jump to line 445, because the condition on line 444 was never true

445 stroke = self.get_color_url(stroke_color, objects.GradientStroke, group) 

446 opacity = 1 

447 else: 

448 stroke = objects.Stroke() 

449 color = self.parse_color(stroke_color) 

450 stroke.color.value = color 

451 opacity = color[3] 

452 group.add_shape(stroke) 

453 

454 stroke.opacity.value = opacity * float(style.get("stroke-opacity", 1)) * 100 

455 

456 stroke.width.value = self._parse_unit(style.get("stroke-width", 1)) 

457 

458 linecap = style.get("stroke-linecap") 

459 if linecap == "round": 459 ↛ 460line 459 didn't jump to line 460, because the condition on line 459 was never true

460 stroke.line_cap = objects.shapes.LineCap.Round 

461 elif linecap == "butt": 461 ↛ 462line 461 didn't jump to line 462, because the condition on line 461 was never true

462 stroke.line_cap = objects.shapes.LineCap.Butt 

463 elif linecap == "square": 463 ↛ 464line 463 didn't jump to line 464, because the condition on line 463 was never true

464 stroke.line_cap = objects.shapes.LineCap.Square 

465 

466 linejoin = style.get("stroke-linejoin") 

467 if linejoin == "round": 467 ↛ 468line 467 didn't jump to line 468, because the condition on line 467 was never true

468 stroke.line_join = objects.shapes.LineJoin.Round 

469 elif linejoin == "bevel": 469 ↛ 470line 469 didn't jump to line 470, because the condition on line 469 was never true

470 stroke.line_join = objects.shapes.LineJoin.Bevel 

471 elif linejoin in {"miter", "arcs", "miter-clip"}: 471 ↛ 472line 471 didn't jump to line 472, because the condition on line 471 was never true

472 stroke.line_join = objects.shapes.LineJoin.Miter 

473 

474 stroke.miter_limit = self._parse_unit(style.get("stroke-miterlimit", 0)) 

475 

476 dash_array = style.get("stroke-dasharray") 

477 if dash_array and dash_array != "none": 477 ↛ 478line 477 didn't jump to line 478, because the condition on line 477 was never true

478 values = list(map(self._parse_unit, dash_array.replace(",", " ").split())) 

479 if len(values) % 2: 

480 values += values 

481 

482 stroke.dashes = [] 

483 for i in range(0, len(values), 2): 

484 stroke.dashes.append(objects.StrokeDash(values[i], objects.StrokeDashType.Dash)) 

485 stroke.dashes.append(objects.StrokeDash(values[i+1], objects.StrokeDashType.Gap)) 

486 

487 fill_color = style.get("fill", "inherit") 

488 if fill_color not in nocolor: 488 ↛ 489line 488 didn't jump to line 489, because the condition on line 488 was never true

489 if fill_color.startswith("url"): 

490 fill = self.get_color_url(fill_color, objects.GradientFill, group) 

491 opacity = 1 

492 else: 

493 color = self.parse_color(fill_color) 

494 fill = objects.Fill(color) 

495 opacity = color[3] 

496 opacity *= float(style.get("fill-opacity", 1)) 

497 fill.opacity.value = opacity * 100 

498 

499 if style.get("fill-rule", "") == "evenodd": 

500 fill.fill_rule = objects.FillRule.EvenOdd 

501 

502 group.add_shape(fill) 

503 

504 def _parseshape_use(self, element, shape_parent, parent_style): 

505 link = element.attrib.get(self.qualified("xlink", "href")) 

506 if link and link.startswith("#"): 

507 id = link[1:] 

508 base_element = self.document.find(".//*[@id='%s']" % id) 

509 use_style = self.parse_style(element, parent_style) 

510 used = objects.Group() 

511 shape_parent.add_shape(used) 

512 used.name = "use" 

513 used.transform.position.value.x = float(element.attrib.get("x", 0)) 

514 used.transform.position.value.y = float(element.attrib.get("y", 0)) 

515 self.parse_transform(element, used, used.transform) 

516 self.parse_shape(base_element, used, use_style) 

517 return used 

518 

519 def _parseshape_g(self, element, shape_parent, parent_style): 

520 group = objects.Group() 

521 shape_parent.shapes.insert(0, group) 

522 style = self.parse_style(element, parent_style) 

523 self.apply_common_style(style, group.transform) 

524 self.apply_visibility(style, group) 

525 group.name = self._get_name(element, self.qualified("inkscape", "label")) 

526 self.parse_children(element, group, style) 

527 self.parse_transform(element, group, group.transform) 

528 if group.hidden: # Lottie web doesn't seem to support .hd 

529 group.transform.opacity.value = 0 

530 return group 

531 

532 def _parseshape_ellipse(self, element, shape_parent, parent_style): 

533 ellipse = objects.Ellipse() 

534 ellipse.position.value = NVector( 

535 self._parse_unit(element.attrib["cx"]), 

536 self._parse_unit(element.attrib["cy"]) 

537 ) 

538 ellipse.size.value = NVector( 

539 self._parse_unit(element.attrib["rx"]) * 2, 

540 self._parse_unit(element.attrib["ry"]) * 2 

541 ) 

542 self.add_shapes(element, [ellipse], shape_parent, parent_style) 

543 return ellipse 

544 

545 def _parseshape_anim_ellipse(self, ellipse, element, animations): 

546 self._merge_animations(element, animations, "cx", "cy", "position") 

547 self._merge_animations(element, animations, "rx", "ry", "size", lambda x, y: NVector(x, y) * 2) 

548 self._apply_animations(ellipse.position, "position", animations) 

549 self._apply_animations(ellipse.size, "size", animations) 

550 

551 def _parseshape_circle(self, element, shape_parent, parent_style): 

552 ellipse = objects.Ellipse() 

553 ellipse.position.value = NVector( 

554 self._parse_unit(element.attrib["cx"]), 

555 self._parse_unit(element.attrib["cy"]) 

556 ) 

557 r = self._parse_unit(element.attrib["r"]) * 2 

558 ellipse.size.value = NVector(r, r) 

559 self.add_shapes(element, [ellipse], shape_parent, parent_style) 

560 return ellipse 

561 

562 def _parseshape_anim_circle(self, ellipse, element, animations): 

563 self._merge_animations(element, animations, "cx", "cy", "position") 

564 self._apply_animations(ellipse.position, "position", animations) 

565 self._apply_animations(ellipse.size, "r", animations, lambda r: NVector(r, r) * 2) 

566 

567 def _parseshape_rect(self, element, shape_parent, parent_style): 

568 rect = objects.Rect() 

569 w = self._parse_unit(element.attrib.get("width", 0)) 

570 h = self._parse_unit(element.attrib.get("height", 0)) 

571 rect.position.value = NVector( 

572 self._parse_unit(element.attrib.get("x", 0)) + w / 2, 

573 self._parse_unit(element.attrib.get("y", 0)) + h / 2 

574 ) 

575 rect.size.value = NVector(w, h) 

576 rx = self._parse_unit(element.attrib.get("rx", 0)) 

577 ry = self._parse_unit(element.attrib.get("ry", 0)) 

578 rect.rounded.value = (rx + ry) / 2 

579 self.add_shapes(element, [rect], shape_parent, parent_style) 

580 return rect 

581 

582 def _parseshape_anim_rect(self, rect, element, animations): 

583 self._merge_animations(element, animations, "width", "height", "size", lambda x, y: NVector(x, y)) 

584 self._apply_animations(rect.size, "size", animations) 

585 self._merge_animations(element, animations, "x", "y", "position") 

586 self._merge_animations(element, animations, "position", "size", "position", lambda p, s: p + s / 2) 

587 self._apply_animations(rect.position, "position", animations) 

588 self._merge_animations(element, animations, "rx", "ry", "rounded", lambda x, y: (x + y) / 2) 

589 self._apply_animations(rect.rounded, "rounded", animations) 

590 

591 def _parseshape_line(self, element, shape_parent, parent_style): 

592 line = objects.Path() 

593 line.shape.value.add_point(NVector( 

594 self._parse_unit(element.attrib["x1"]), 

595 self._parse_unit(element.attrib["y1"]) 

596 )) 

597 line.shape.value.add_point(NVector( 

598 self._parse_unit(element.attrib["x2"]), 

599 self._parse_unit(element.attrib["y2"]) 

600 )) 

601 return self.add_shapes(element, [line], shape_parent, parent_style) 

602 

603 def _parseshape_anim_line(self, group, element, animations): 

604 line = group.shapes[0] 

605 

606 self._merge_animations(element, animations, "x1", "y1", "p1") 

607 self._merge_animations(element, animations, "x2", "y2", "p2") 

608 

609 def combine(p1, p2): 

610 shape = objects.Bezier() 

611 shape.add_point(p1) 

612 shape.add_point(p2) 

613 return shape 

614 

615 self._merge_animations(element, animations, "p1", "p2", "points", combine) 

616 self._apply_animations(line.shape, "points", animations) 

617 

618 def _handle_poly(self, element): 

619 line = objects.Path() 

620 coords = list(map(float, element.attrib["points"].replace(",", " ").split())) 

621 for i in range(0, len(coords), 2): 

622 line.shape.value.add_point(NVector(*coords[i:i+2])) 

623 return line 

624 

625 def _parseshape_polyline(self, element, shape_parent, parent_style): 

626 line = self._handle_poly(element) 

627 return self.add_shapes(element, [line], shape_parent, parent_style) 

628 

629 def _parseshape_polygon(self, element, shape_parent, parent_style): 

630 line = self._handle_poly(element) 

631 line.shape.value.close() 

632 return self.add_shapes(element, [line], shape_parent, parent_style) 

633 

634 def _parseshape_path(self, element, shape_parent, parent_style): 

635 d_parser = PathDParser(element.attrib.get("d", "")) 

636 d_parser.parse() 

637 paths = [] 

638 for path in d_parser.paths: 

639 p = objects.Path() 

640 p.shape.value = path 

641 paths.append(p) 

642 #if len(d_parser.paths) > 1: 

643 #paths.append(objects.shapes.Merge()) 

644 return self.add_shapes(element, paths, shape_parent, parent_style) 

645 

646 def parse_children(self, element, shape_parent, parent_style): 

647 for child in element: 

648 tag = self.unqualified(child.tag) 

649 if not self.parse_shape(child, shape_parent, parent_style): 649 ↛ 650line 649 didn't jump to line 650, because the condition on line 649 was never true

650 handler = getattr(self, "_parse_" + tag, None) 

651 if handler: 

652 handler(child) 

653 

654 def parse_shape(self, element, shape_parent, parent_style): 

655 handler = getattr(self, "_parseshape_" + self.unqualified(element.tag), None) 

656 if handler: 656 ↛ 662line 656 didn't jump to line 662, because the condition on line 656 was never false

657 out = handler(element, shape_parent, parent_style) 

658 self.parse_animations(out, element) 

659 if element.attrib.get("id"): 659 ↛ 661line 659 didn't jump to line 661, because the condition on line 659 was never false

660 self.defs.items[element.attrib["id"]] = out 

661 return out 

662 return None 

663 

664 def _parse_defs(self, element): 

665 self.parse_children(element, self.defs, {}) 

666 

667 def _apply_transform_element_to_matrix(self, matrix, t): 

668 name = t[1] 

669 params = list(map(float, t[2].strip().replace(",", " ").split())) 

670 if name == "translate": 670 ↛ 671line 670 didn't jump to line 671, because the condition on line 670 was never true

671 matrix.translate( 

672 params[0], 

673 (params[1] if len(params) > 1 else 0), 

674 ) 

675 elif name == "scale": 675 ↛ 676line 675 didn't jump to line 676, because the condition on line 675 was never true

676 xfac = params[0] 

677 yfac = params[1] if len(params) > 1 else xfac 

678 matrix.scale(xfac, yfac) 

679 elif name == "rotate": 679 ↛ 680line 679 didn't jump to line 680, because the condition on line 679 was never true

680 ang = params[0] 

681 x = y = 0 

682 if len(params) > 2: 

683 x = params[1] 

684 y = params[2] 

685 matrix.translate(-x, -y) 

686 matrix.rotate(math.radians(ang)) 

687 matrix.translate(x, y) 

688 else: 

689 matrix.rotate(math.radians(ang)) 

690 elif name == "skewX": 690 ↛ 691line 690 didn't jump to line 691, because the condition on line 690 was never true

691 matrix.skew(math.radians(params[0]), 0) 

692 elif name == "skewY": 692 ↛ 693line 692 didn't jump to line 693, because the condition on line 692 was never true

693 matrix.skew(0, math.radians(params[0])) 

694 elif name == "matrix": 694 ↛ exitline 694 didn't return from function '_apply_transform_element_to_matrix', because the condition on line 694 was never false

695 m = TransformMatrix() 

696 m.a, m.b, m.c, m.d, m.tx, m.ty = params 

697 matrix *= m 

698 

699 def _transform_to_matrix(self, transform): 

700 matrix = TransformMatrix() 

701 

702 for t in re.finditer(r"([a-zA-Z]+)\s*\(([^\)]*)\)", transform): 

703 self._apply_transform_element_to_matrix(matrix, t) 

704 

705 return matrix 

706 

707 def _gradient(self, element, grad): 

708 grad.matrix = self._transform_to_matrix(element.attrib.get("gradientTransform", "")) 

709 

710 id = element.attrib["id"] 

711 if id in self.gradients: 

712 grad.colors = self.gradients[id].colors 

713 grad.parse_attrs(element.attrib) 

714 href = element.attrib.get(self.qualified("xlink", "href")) 

715 if href: 

716 srcid = href.strip("#") 

717 if srcid in self.gradients: 

718 src = self.gradients[srcid] 

719 else: 

720 src = grad.__class__() 

721 self.gradients[srcid] = src 

722 grad.colors = src.colors 

723 

724 for stop in element.findall("./%s" % self.qualified("svg", "stop")): 

725 offset = stop.attrib.get("offset", "0") 

726 off = float(offset.strip("%")) 

727 if offset.endswith("%"): 

728 off /= 100 

729 style = self.parse_style(stop, {}) 

730 color = self.parse_color(style.get("stop-color", "black")) 

731 if "stop-opacity" in style: 

732 color[3] = float(style["stop-opacity"]) 

733 grad.add_color(off, color) 

734 self.gradients[id] = grad 

735 

736 def _parse_linearGradient(self, element): 

737 self._gradient(element, SvgLinearGradient()) 

738 

739 def _parse_radialGradient(self, element): 

740 self._gradient(element, SvgRadialGradient()) 

741 

742 def get_color_url(self, color, gradientclass, shape): 

743 match = re.match(r"""url\(['"]?#([^)'"]+)['"]?\)""", color) 

744 if not match: 

745 return None 

746 id = match[1] 

747 if id not in self.gradients: 

748 return None 

749 grad = self.gradients[id] 

750 outgrad = gradientclass() 

751 grad.to_lottie(outgrad, shape) 

752 if self.name_mode != NameMode.NoName: 

753 grad.name = id 

754 return outgrad 

755 

756 ## @todo Parse single font property, fallback family etc 

757 def _parse_text_style(self, style, font_style=None): 

758 if "font-family" in style: 

759 font_style.query.family(style["font-family"].strip("'\"")) 

760 

761 if "font-style" in style: 

762 if style["font-style"] == "oblique": 

763 font_style.query.custom("slant", 110) 

764 elif style["font-style"] == "italic": 

765 font_style.query.custom("slant", 100) 

766 

767 if "font-weight" in style: 

768 if style["font-weight"] in {"bold", "bolder"}: 

769 font_style.query.weight(200) 

770 elif style["font-weight"] == "lighter": 

771 font_style.query.weight(50) 

772 elif style["font-weight"].isdigit(): 

773 font_style.query.css_weight(int(style["font-weight"])) 

774 

775 if "font-size" in style: 

776 fz = style["font-size"] 

777 fz_names = { 

778 "xx-small": 8, 

779 "x-small": 16, 

780 "small": 32, 

781 "medium": 64, 

782 "large": 128, 

783 "x-large": 256, 

784 "xx-large": 512, 

785 } 

786 if fz in fz_names: 

787 font_style.size = fz_names[fz] 

788 elif fz == "smaller": 

789 font_style.size /= 2 

790 elif fz == "larger": 

791 font_style.size *= 2 

792 elif fz.endswith("px"): 

793 font_style.size = float(fz[:-2]) 

794 elif fz.isnumeric(): 

795 font_style.size = float(fz) 

796 

797 if "text-align" in style: 

798 ta = style["text-align"] 

799 if ta in ("left", "start"): 

800 font_style.justify = font.TextJustify.Left 

801 elif ta == "center": 

802 font_style.justify = font.TextJustify.Center 

803 elif ta in ("right", "end"): 

804 font_style.justify = font.TextJustify.Right 

805 

806 def _parse_text_elem(self, element, style, group, parent_style, font_style): 

807 self._parse_text_style(style, font_style) 

808 

809 if "x" in element.attrib or "y" in element.attrib: 

810 font_style.position = NVector( 

811 float(element.attrib["x"]), 

812 float(element.attrib["y"]), 

813 ) 

814 

815 childpos = NVector(0, font_style.position.y) 

816 

817 if element.text: 

818 fs = font.FontShape(element.text, font_style) 

819 fs.refresh() 

820 group.add_shape(fs) 

821 childpos.x = fs.wrapped.next_x 

822 

823 for child in element: 

824 if child.tag == self.qualified("svg", "tspan"): 

825 child_style = font_style.clone() 

826 child_style.position = childpos.clone() 

827 fs = self._parseshape_text(child, group, parent_style, child_style) 

828 childpos.x = fs.next_x 

829 if child.tail: 

830 child_style = font_style.clone() 

831 child_style.position = childpos.clone() 

832 fs = font.FontShape(child.tail, child_style) 

833 fs.refresh() 

834 group.add_shape(fs) 

835 childpos.x = fs.wrapped.next_x 

836 

837 group.next_x = childpos.x 

838 

839 def _parseshape_text(self, element, shape_parent, parent_style, font_style=None): 

840 group = objects.Group() 

841 

842 style = self.parse_style(element, parent_style) 

843 self.apply_common_style(style, group.transform) 

844 self.apply_visibility(style, group) 

845 group.name = self._get_id(element) 

846 

847 if has_font: 

848 if font_style is None: 

849 font_style = font.FontStyle("", 64) 

850 self._parse_text_elem(element, style, group, style, font_style) 

851 

852 style.setdefault("fill", "none") 

853 self._add_style_shapes(style, group) 

854 

855 ## @todo text-anchor when it doesn't match text-align 

856 #if element.tag == self.qualified("svg", "text"): 

857 #dx = 0 

858 #dy = 0 

859 

860 #ta = style.get("text-anchor", style.get("text-align", "")) 

861 #if ta == "middle": 

862 #dx -= group.bounding_box().width / 2 

863 #elif ta == "end": 

864 #dx -= group.bounding_box().width 

865 

866 #if dx or dy: 

867 #ng = objects.Group() 

868 #ng.add_shape(group) 

869 #group.transform.position.value.x += dx 

870 #group.transform.position.value.y += dy 

871 #group = ng 

872 

873 shape_parent.shapes.insert(0, group) 

874 self.parse_transform(element, group, group.transform) 

875 return group 

876 

877 def parse_animations(self, lottie, element): 

878 animations = {} 

879 for child in element: 879 ↛ 880line 879 didn't jump to line 880, because the loop on line 879 never started

880 if self.unqualified(child.tag) == "animate": 

881 att = child.attrib["attributeName"] 

882 

883 from_val = child.attrib["from"] 

884 if att == "d": 

885 ## @todo 

886 continue 

887 else: 

888 from_val = float(from_val) 

889 if "to" in child.attrib: 

890 to_val = float(child.attrib["to"]) 

891 elif "by" in child.attrib: 

892 to_val = float(child.attrib["by"]) + from_val 

893 

894 begin = self.parse_animation_time(child.attrib.get("begin", 0)) or 0 

895 if "dur" in child.attrib: 

896 end = (self.parse_animation_time(child.attrib["dur"]) or 0) + begin 

897 elif "end" in child.attrib: 

898 end = self.parse_animation_time(child.attrib["dur"]) or 0 

899 else: 

900 continue 

901 

902 if att not in animations: 

903 animations[att] = {} 

904 animations[att][begin] = from_val 

905 animations[att][end] = to_val 

906 if self.max_time < end: 

907 self.max_time = end 

908 

909 tag = self.unqualified(element.tag) 

910 handler = getattr(self, "_parseshape_anim_" + tag, None) 

911 if handler: 911 ↛ 912line 911 didn't jump to line 912, because the condition on line 911 was never true

912 handler(lottie, element, animations) 

913 

914 def parse_animation_time(self, value): 

915 """! 

916 @see https://developer.mozilla.org/en-US/docs/Web/SVG/Content_type#Clock-value 

917 """ 

918 if not value: 

919 return None 

920 try: 

921 seconds = 0 

922 if ":" in value: 

923 mult = 1 

924 for elem in reversed(value.split(":")): 

925 seconds += float(elem) * mult 

926 mult *= 60 

927 elif value.endswith("s"): 

928 seconds = float(value[:-1]) 

929 elif value.endswith("ms"): 

930 seconds = float(value[:-2]) / 1000 

931 elif value.endswith("min"): 

932 seconds = float(value[:-3]) * 60 

933 elif value.endswith("h"): 

934 seconds = float(value[:-1]) * 60 * 60 

935 else: 

936 seconds = float(value) 

937 return seconds * self.animation.frame_rate 

938 except ValueError: 

939 pass 

940 return None 

941 

942 def _merge_animations(self, element, animations, val1, val2, dest, merge=NVector): 

943 if val1 not in animations and val2 not in animations: 

944 return 

945 

946 dict1 = list(sorted(animations.pop(val1, {}).items())) 

947 dict2 = list(sorted(animations.pop(val2, {}).items())) 

948 

949 x = float(element.attrib[val1]) 

950 y = float(element.attrib[val2]) 

951 values = {} 

952 while dict1 or dict2: 

953 if not dict1 or (dict2 and dict1[0][0] > dict2[0][0]): 

954 t, y = dict2.pop(0) 

955 elif not dict2 or dict1[0][0] < dict2[0][0]: 

956 t, x = dict1.pop(0) 

957 else: 

958 t, x = dict1.pop(0) 

959 t, y = dict2.pop(0) 

960 

961 values[t] = merge(x, y) 

962 

963 animations[dest] = values 

964 

965 def _apply_animations(self, animatable, name, animations, transform=lambda v: v): 965 ↛ exitline 965 didn't run the lambda on line 965

966 if name in animations: 

967 for t, v in animations[name].items(): 

968 animatable.add_keyframe(t, transform(v)) 

969 

970 

971class PathDParser: 

972 _re = re.compile("|".join(( 

973 r"[a-zA-Z]", 

974 r"[-+]?[0-9]*\.?[0-9]*[eE][-+]?[0-9]+", 

975 r"[-+]?[0-9]*\.?[0-9]+", 

976 ))) 

977 

978 def __init__(self, d_string): 

979 self.path = objects.properties.Bezier() 

980 self.paths = [] 

981 self.p = NVector(0, 0) 

982 self.la = None 

983 self.la_type = None 

984 self.tokens = list(map(self.d_subsplit, self._re.findall(d_string))) 

985 self.add_p = True 

986 self.implicit = "M" 

987 

988 def d_subsplit(self, tok): 

989 if tok.isalpha(): 

990 return tok 

991 return float(tok) 

992 

993 def next_token(self): 

994 if self.tokens: 

995 self.la = self.tokens.pop(0) 

996 if isinstance(self.la, str): 

997 self.la_type = 0 

998 else: 

999 self.la_type = 1 

1000 else: 

1001 self.la = None 

1002 return self.la 

1003 

1004 def next_vec(self): 

1005 x = self.next_token() 

1006 y = self.next_token() 

1007 return NVector(x, y) 

1008 

1009 def cur_vec(self): 

1010 x = self.la 

1011 y = self.next_token() 

1012 return NVector(x, y) 

1013 

1014 def parse(self): 

1015 self.next_token() 

1016 while self.la is not None: 

1017 if self.la_type == 0: 

1018 parser = "_parse_" + self.la 

1019 self.next_token() 

1020 getattr(self, parser)() 

1021 else: 

1022 parser = "_parse_" + self.implicit 

1023 getattr(self, parser)() 

1024 

1025 def _push_path(self): 

1026 self.path = objects.properties.Bezier() 

1027 self.add_p = True 

1028 

1029 def _parse_M(self): 

1030 if self.la_type != 1: 1030 ↛ 1031line 1030 didn't jump to line 1031, because the condition on line 1030 was never true

1031 self.next_token() 

1032 return 

1033 self.p = self.cur_vec() 

1034 self.implicit = "L" 

1035 if not self.add_p: 

1036 self._push_path() 

1037 self.next_token() 

1038 

1039 def _parse_m(self): 

1040 if self.la_type != 1: 1040 ↛ 1041line 1040 didn't jump to line 1041, because the condition on line 1040 was never true

1041 self.next_token() 

1042 return 

1043 self.p += self.cur_vec() 

1044 self.implicit = "l" 

1045 if not self.add_p: 

1046 self._push_path() 

1047 self.next_token() 

1048 

1049 def _rpoint(self, point, rel=None): 

1050 return (point - (rel or self.p)) if point is not None else NVector(0, 0) 

1051 

1052 def _do_add_p(self, outp=None): 

1053 if self.add_p: 

1054 self.paths.append(self.path) 

1055 self.path.add_point(self.p.clone(), NVector(0, 0), self._rpoint(outp)) 

1056 self.add_p = False 

1057 elif outp: 1057 ↛ 1058line 1057 didn't jump to line 1058, because the condition on line 1057 was never true

1058 rp = self.path.vertices[-1] 

1059 self.path.out_tangents[-1] = self._rpoint(outp, rp) 

1060 

1061 def _parse_L(self): 

1062 if self.la_type != 1: 1062 ↛ 1063line 1062 didn't jump to line 1063, because the condition on line 1062 was never true

1063 self.next_token() 

1064 return 

1065 self._do_add_p() 

1066 self.p = self.cur_vec() 

1067 self.path.add_point(self.p.clone(), NVector(0, 0), NVector(0, 0)) 

1068 self.implicit = "L" 

1069 self.next_token() 

1070 

1071 def _parse_l(self): 

1072 if self.la_type != 1: 1072 ↛ 1073line 1072 didn't jump to line 1073, because the condition on line 1072 was never true

1073 self.next_token() 

1074 return 

1075 self._do_add_p() 

1076 self.p += self.cur_vec() 

1077 self.path.add_point(self.p.clone(), NVector(0, 0), NVector(0, 0)) 

1078 self.implicit = "l" 

1079 self.next_token() 

1080 

1081 def _parse_H(self): 

1082 if self.la_type != 1: 1082 ↛ 1083line 1082 didn't jump to line 1083, because the condition on line 1082 was never true

1083 self.next_token() 

1084 return 

1085 self._do_add_p() 

1086 self.p[0] = self.la 

1087 self.path.add_point(self.p.clone(), NVector(0, 0), NVector(0, 0)) 

1088 self.implicit = "H" 

1089 self.next_token() 

1090 

1091 def _parse_h(self): 

1092 if self.la_type != 1: 1092 ↛ 1093line 1092 didn't jump to line 1093, because the condition on line 1092 was never true

1093 self.next_token() 

1094 return 

1095 self._do_add_p() 

1096 self.p[0] += self.la 

1097 self.path.add_point(self.p.clone(), NVector(0, 0), NVector(0, 0)) 

1098 self.implicit = "h" 

1099 self.next_token() 

1100 

1101 def _parse_V(self): 

1102 if self.la_type != 1: 1102 ↛ 1103line 1102 didn't jump to line 1103, because the condition on line 1102 was never true

1103 self.next_token() 

1104 return 

1105 self._do_add_p() 

1106 self.p[1] = self.la 

1107 self.path.add_point(self.p.clone(), NVector(0, 0), NVector(0, 0)) 

1108 self.implicit = "V" 

1109 self.next_token() 

1110 

1111 def _parse_v(self): 

1112 if self.la_type != 1: 1112 ↛ 1113line 1112 didn't jump to line 1113, because the condition on line 1112 was never true

1113 self.next_token() 

1114 return 

1115 self._do_add_p() 

1116 self.p[1] += self.la 

1117 self.path.add_point(self.p.clone(), NVector(0, 0), NVector(0, 0)) 

1118 self.implicit = "v" 

1119 self.next_token() 

1120 

1121 def _parse_C(self): 

1122 if self.la_type != 1: 1122 ↛ 1123line 1122 didn't jump to line 1123, because the condition on line 1122 was never true

1123 self.next_token() 

1124 return 

1125 pout = self.cur_vec() 

1126 self._do_add_p(pout) 

1127 pin = self.next_vec() 

1128 self.p = self.next_vec() 

1129 self.path.add_point( 

1130 self.p.clone(), 

1131 (pin - self.p), 

1132 NVector(0, 0) 

1133 ) 

1134 self.implicit = "C" 

1135 self.next_token() 

1136 

1137 def _parse_c(self): 

1138 if self.la_type != 1: 1138 ↛ 1139line 1138 didn't jump to line 1139, because the condition on line 1138 was never true

1139 self.next_token() 

1140 return 

1141 pout = self.p + self.cur_vec() 

1142 self._do_add_p(pout) 

1143 pin = self.p + self.next_vec() 

1144 self.p += self.next_vec() 

1145 self.path.add_point( 

1146 self.p.clone(), 

1147 (pin - self.p), 

1148 NVector(0, 0) 

1149 ) 

1150 self.implicit = "c" 

1151 self.next_token() 

1152 

1153 def _parse_S(self): 

1154 if self.la_type != 1: 1154 ↛ 1155line 1154 didn't jump to line 1155, because the condition on line 1154 was never true

1155 self.next_token() 

1156 return 

1157 pin = self.cur_vec() 

1158 self._do_add_p() 

1159 handle = self.path.in_tangents[-1] 

1160 self.path.out_tangents[-1] = (-handle) 

1161 self.p = self.next_vec() 

1162 self.path.add_point( 

1163 self.p.clone(), 

1164 (pin - self.p), 

1165 NVector(0, 0) 

1166 ) 

1167 self.implicit = "S" 

1168 self.next_token() 

1169 

1170 def _parse_s(self): 

1171 if self.la_type != 1: 1171 ↛ 1172line 1171 didn't jump to line 1172, because the condition on line 1171 was never true

1172 self.next_token() 

1173 return 

1174 pin = self.cur_vec() + self.p 

1175 self._do_add_p() 

1176 handle = self.path.in_tangents[-1] 

1177 self.path.out_tangents[-1] = (-handle) 

1178 self.p += self.next_vec() 

1179 self.path.add_point( 

1180 self.p.clone(), 

1181 (pin - self.p), 

1182 NVector(0, 0) 

1183 ) 

1184 self.implicit = "s" 

1185 self.next_token() 

1186 

1187 def _parse_Q(self): 

1188 if self.la_type != 1: 1188 ↛ 1189line 1188 didn't jump to line 1189, because the condition on line 1188 was never true

1189 self.next_token() 

1190 return 

1191 self._do_add_p() 

1192 pin = self.cur_vec() 

1193 self.p = self.next_vec() 

1194 self.path.add_point( 

1195 self.p.clone(), 

1196 (pin - self.p), 

1197 NVector(0, 0) 

1198 ) 

1199 self.implicit = "Q" 

1200 self.next_token() 

1201 

1202 def _parse_q(self): 

1203 if self.la_type != 1: 1203 ↛ 1204line 1203 didn't jump to line 1204, because the condition on line 1203 was never true

1204 self.next_token() 

1205 return 

1206 self._do_add_p() 

1207 pin = self.p + self.cur_vec() 

1208 self.p += self.next_vec() 

1209 self.path.add_point( 

1210 self.p.clone(), 

1211 (pin - self.p), 

1212 NVector(0, 0) 

1213 ) 

1214 self.implicit = "q" 

1215 self.next_token() 

1216 

1217 def _parse_T(self): 

1218 if self.la_type != 1: 1218 ↛ 1219line 1218 didn't jump to line 1219, because the condition on line 1218 was never true

1219 self.next_token() 

1220 return 

1221 self._do_add_p() 

1222 handle = self.p - self.path.in_tangents[-1] 

1223 self.p = self.cur_vec() 

1224 self.path.add_point( 

1225 self.p.clone(), 

1226 (handle - self.p), 

1227 NVector(0, 0) 

1228 ) 

1229 self.implicit = "T" 

1230 self.next_token() 

1231 

1232 def _parse_t(self): 

1233 if self.la_type != 1: 1233 ↛ 1234line 1233 didn't jump to line 1234, because the condition on line 1233 was never true

1234 self.next_token() 

1235 return 

1236 self._do_add_p() 

1237 handle = -self.path.in_tangents[-1] + self.p 

1238 self.p += self.cur_vec() 

1239 self.path.add_point( 

1240 self.p.clone(), 

1241 (handle - self.p), 

1242 NVector(0, 0) 

1243 ) 

1244 self.implicit = "t" 

1245 self.next_token() 

1246 

1247 def _parse_A(self): 

1248 if self.la_type != 1: 1248 ↛ 1249line 1248 didn't jump to line 1249, because the condition on line 1248 was never true

1249 self.next_token() 

1250 return 

1251 r = self.cur_vec() 

1252 xrot = self.next_token() 

1253 large = self.next_token() 

1254 sweep = self.next_token() 

1255 dest = self.next_vec() 

1256 self._do_arc(r[0], r[1], xrot, large, sweep, dest) 

1257 self.implicit = "A" 

1258 self.next_token() 

1259 

1260 def _do_arc(self, rx, ry, xrot, large, sweep, dest): 

1261 self._do_add_p() 

1262 if self.p == dest: 1262 ↛ 1263line 1262 didn't jump to line 1263, because the condition on line 1262 was never true

1263 return 

1264 

1265 if rx == 0 or ry == 0: 1265 ↛ 1267line 1265 didn't jump to line 1267, because the condition on line 1265 was never true

1266 # Straight line 

1267 self.p = dest 

1268 self.path.add_point( 

1269 self.p.clone(), 

1270 NVector(0, 0), 

1271 NVector(0, 0) 

1272 ) 

1273 return 

1274 

1275 ellipse, theta1, deltatheta = Ellipse.from_svg_arc(self.p, rx, ry, xrot, large, sweep, dest) 

1276 points = ellipse.to_bezier_points(theta1, deltatheta) 

1277 

1278 self._do_add_p() 

1279 self.path.out_tangents[-1] = points[0].out_tangent 

1280 for point in points[1:-1]: 

1281 self.path.add_point( 

1282 point.vertex, 

1283 point.in_tangent, 

1284 point.out_tangent, 

1285 ) 

1286 self.path.add_point( 

1287 dest.clone(), 

1288 points[-1].in_tangent, 

1289 NVector(0, 0), 

1290 ) 

1291 self.p = dest 

1292 

1293 def _parse_a(self): 

1294 if self.la_type != 1: 

1295 self.next_token() 

1296 return 

1297 r = self.cur_vec() 

1298 xrot = self.next_token() 

1299 large = self.next_token() 

1300 sweep = self.next_token() 

1301 dest = self.p + self.next_vec() 

1302 self._do_arc(r[0], r[1], xrot, large, sweep, dest) 

1303 self.implicit = "a" 

1304 self.next_token() 

1305 

1306 def _parse_Z(self): 

1307 if self.path.vertices: 1307 ↛ 1309line 1307 didn't jump to line 1309, because the condition on line 1307 was never false

1308 self.p = self.path.vertices[0].clone() 

1309 self.path.close() 

1310 self._push_path() 

1311 

1312 def _parse_z(self): 

1313 self._parse_Z() 

1314 

1315 

1316def parse_svg_etree(etree, layer_frames=0, *args, **kwargs): 

1317 parser = SvgParser() 

1318 return parser.parse_etree(etree, layer_frames, *args, **kwargs) 

1319 

1320 

1321def parse_svg_file(file, layer_frames=0, *args, **kwargs): 

1322 return parse_svg_etree(ElementTree.parse(file), layer_frames, *args, **kwargs)