Coverage for lib/lottie/utils/funky_parser.py: 0%

964 statements  

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

1import re 

2import sys 

3import enum 

4import math 

5import typing 

6 

7from ..parsers.svg.svgdata import color_table 

8from ..parsers.svg.importer import parse_color, parse_svg_file 

9from ..objects.animation import Animation 

10from ..objects import layers 

11from ..objects import shapes 

12from ..nvector import NVector 

13from .font import FontStyle 

14 

15 

16color_words = { 

17 "alice": {"blue": {"_": 1}}, 

18 "antique": {"white": {"_": 1}}, 

19 "aqua": { 

20 "_": 1, 

21 "marine": {"_": 1} 

22 }, 

23 "azure": {"_": 1}, 

24 "beige": {"_": 1}, 

25 "bisque": {"_": 1}, 

26 "black": {"_": 1}, 

27 "blanched": {"almond": {"_": 1}}, 

28 "blue": { 

29 "_": 1, 

30 "navy": "navy", 

31 "violet": {"_": 1}, 

32 }, 

33 "brown": {"_": 1}, 

34 "burly": {"wood": {"_": 1}}, 

35 "cadet": {"blue": {"_": 1}}, 

36 "chartreuse": {"_": 1}, 

37 "chocolate": {"_": 1}, 

38 "coral": {"_": 1}, 

39 "cornflower": {"blue": {"_": 1}}, 

40 "corn": { 

41 "silk": {"_": 1}, 

42 "flower": {"blue": {"_": 1}}, 

43 }, 

44 "crimson": {"_": 1}, 

45 "cyan": {"_": 1}, 

46 "dark": { 

47 "blue": {"_": 1}, 

48 "cyan": {"_": 1}, 

49 "golden": {"rod": {"_": 1}}, 

50 "gray": {"_": 1}, 

51 "green": {"_": 1}, 

52 "grey": {"_": 1}, 

53 "khaki": {"_": 1}, 

54 "magenta": {"_": 1}, 

55 "olive": {"green": {"_": 1}}, 

56 "orange": {"_": 1}, 

57 "orchid": {"_": 1}, 

58 "red": {"_": 1}, 

59 "salmon": {"_": 1}, 

60 "sea": {"green": {"_": 1}}, 

61 "slate": { 

62 "blue": {"_": 1}, 

63 "gray": {"_": 1}, 

64 "grey": {"_": 1} 

65 }, 

66 "turquoise": {"_": 1}, 

67 "violet": {"_": 1} 

68 }, 

69 "deep": { 

70 "pink": {"_": 1}, 

71 "sky": {"blue": {"_": 1}}, 

72 }, 

73 "dim": { 

74 "gray": {"_": 1}, 

75 "grey": {"_": 1} 

76 }, 

77 "dodger": {"blue": {"_": 1}}, 

78 "fire": {"brick": {"_": 1}}, 

79 "floral": {"white": {"_": 1}}, 

80 "forest": {"green": {"_": 1}}, 

81 "fuchsia": {"_": 1}, 

82 "gainsboro": {"_": 1}, 

83 "ghost": {"white": {"_": 1}}, 

84 "gold": {"_": 1}, 

85 "golden": {"rod": {"_": 1}}, 

86 "gray": {"_": 1}, 

87 "green": { 

88 "_": 1, 

89 "yellow": {"_": 1} 

90 }, 

91 "grey": {"_": 1}, 

92 "honeydew": {"_": 1}, 

93 "hotpink": {"_": 1}, 

94 "indian": {"red": {"_": 1}}, 

95 "indigo": {"_": 1}, 

96 "ivory": {"_": 1}, 

97 "khaki": {"_": 1}, 

98 "lavender": { 

99 "_": 1, 

100 "blush": {"_": 1}, 

101 }, 

102 "lawn": {"green": {"_": 1}}, 

103 "lemon": {"chiffon": {"_": 1}}, 

104 "light": { 

105 "blue": {"_": 1}, 

106 "coral": {"_": 1}, 

107 "cyan": {"_": 1}, 

108 "golden": { 

109 "rod": { 

110 "_": 1, 

111 "yellow": {"_": 1}, 

112 } 

113 }, 

114 "gray": {"_": 1}, 

115 "green": {"_": 1}, 

116 "grey": {"_": 1}, 

117 "pink": {"_": 1}, 

118 "salmon": {"_": 1}, 

119 "sea": {"green": {"_": 1}}, 

120 "sky": {"blue": {"_": 1}}, 

121 "slate": { 

122 "gray": {"_": 1}, 

123 "grey": {"_": 1} 

124 }, 

125 "steel": {"blue": {"_": 1}}, 

126 "yellow": {"_": 1}, 

127 }, 

128 "lime": { 

129 "_": 1, 

130 "green": {"_": 1}, 

131 }, 

132 "linen": {"_": 1}, 

133 "magenta": {"_": 1}, 

134 "maroon": {"_": 1}, 

135 "medium": { 

136 "aquamarine": {"_": 1}, 

137 "blue": {"_": 1}, 

138 "orchid": {"_": 1}, 

139 "purple": {"_": 1}, 

140 "sea": {"green": {"_": 1}}, 

141 "slate": {"blue": {"_": 1}}, 

142 "spring": {"green": {"_": 1}}, 

143 "turquoise": {"_": 1}, 

144 "violet": {"red": {"_": 1}}, 

145 }, 

146 "midnight": {"blue": {"_": 1}}, 

147 "mint": {"cream": {"_": 1}}, 

148 "misty": {"rose": {"_": 1}}, 

149 "moccasin": {"_": 1}, 

150 "navajo": {"white": {"_": 1}}, 

151 "navy": { 

152 "_": 1, 

153 "blue": "navy", 

154 }, 

155 "old": {"lace": {"_": 1}}, 

156 "olive": { 

157 "_": 1, 

158 "drab": {"_": 1}, 

159 }, 

160 "orange": { 

161 "_": 1, 

162 "red": {"_": 1}, 

163 }, 

164 "orchid": {"_": 1}, 

165 "pale": { 

166 "golden": {"rod": {"_": 1}}, 

167 "green": {"_": 1}, 

168 "turquoise": {"_": 1}, 

169 "violet": {"red": {"_": 1}}, 

170 }, 

171 "papaya": {"whip": {"_": 1}}, 

172 "peach": {"puff": {"_": 1}}, 

173 "peru": {"_": 1}, 

174 "pink": {"_": 1}, 

175 "plum": {"_": 1}, 

176 "powder": {"blue": {"_": 1}}, 

177 "purple": {"_": 1}, 

178 "red": {"_": 1}, 

179 "rosy": {"brown": {"_": 1}}, 

180 "royal": {"blue": {"_": 1}}, 

181 "saddle": {"brown": {"_": 1}}, 

182 "salmon": {"_": 1}, 

183 "sandy": {"brown": {"_": 1}}, 

184 "sea": { 

185 "green": {"_": 1}, 

186 "shell": {"_": 1}, 

187 }, 

188 "seashell": {"_": 1}, 

189 "sienna": {"_": 1}, 

190 "silver": {"_": 1}, 

191 "sky": {"blue": {"_": 1}}, 

192 "slate": { 

193 "blue": {"_": 1}, 

194 "gray": {"_": 1}, 

195 "grey": {"_": 1}, 

196 }, 

197 "snow": {"_": 1}, 

198 "spring": {"green": {"_": 1}}, 

199 "steel": {"blue": {"_": 1}}, 

200 "tan": {"_": 1}, 

201 "teal": {"_": 1}, 

202 "thistle": {"_": 1}, 

203 "tomato": {"_": 1}, 

204 "turquoise": {"_": 1}, 

205 "violet": {"_": 1}, 

206 "wheat": {"_": 1}, 

207 "white": { 

208 "_": 1, 

209 "smoke": {"_": 1}, 

210 }, 

211 "yellow": { 

212 "_": 1, 

213 "green": {"_": 1}, 

214 }, 

215} 

216 

217 

218class TokenType(enum.Enum): 

219 Word = enum.auto() 

220 Number = enum.auto() 

221 Eof = enum.auto() 

222 String = enum.auto() 

223 Punctuation = enum.auto() 

224 Color = enum.auto() 

225 

226 

227class Token: 

228 def __init__( 

229 self, 

230 type: TokenType, 

231 line: int, 

232 col: int, 

233 start: int, 

234 end: int, 

235 value: typing.Any = None 

236 ): 

237 self.col = col 

238 self.line = line 

239 self.end = end 

240 self.start = start 

241 self.type = type 

242 self.value = value 

243 

244 def __repr__(self): 

245 if self.type == TokenType.Eof: 

246 return "End of file" 

247 return repr(self.value) 

248 

249 

250class AnimatableType: 

251 Position = enum.auto() 

252 Color = enum.auto() 

253 Angle = enum.auto() 

254 Size = enum.auto() 

255 Number = enum.auto() 

256 Integer = enum.auto() 

257 

258 

259class AnimatableProperty: 

260 def __init__(self, name, phrase, props, type: AnimatableType): 

261 self.name = name.split() 

262 self.phrase = phrase 

263 self.props = props 

264 self.type = type 

265 

266 def set_initial(self, value): 

267 for prop in self.props: 

268 prop.value = value 

269 

270 def get_initial(self): 

271 return self.props[0].get_value(0) 

272 

273 def add_keyframe(self, time, value): 

274 for prop in self.props: 

275 if not prop.animated and time > 0: 

276 prop.add_keyframe(0, prop.value) 

277 prop.add_keyframe(time, value) 

278 

279 def loop(self, time): 

280 for prop in self.props: 

281 if prop.animated: 

282 prop.add_keyframe(time, prop.get_value(0)) 

283 

284 def get_value(self, time): 

285 return self.props[0].get_value(time) 

286 

287 

288class ShapeData: 

289 def __init__(self, extent): 

290 self.color = [0, 0, 0] 

291 self.color_explicit = False 

292 self.extent = extent 

293 self.size_multiplitier = 1 

294 self.portrait = False 

295 self.roundness = 0 

296 self.opacity = 1 

297 self.count = 1 

298 self.stroke = None 

299 self.stroke_on_top = True 

300 self.properties = {} 

301 

302 def scale(self, multiplier): 

303 self.size_multiplitier *= multiplier 

304 self.extent *= multiplier 

305 

306 def define_property(self, name, type, props, phrase=None): 

307 self.properties[name] = AnimatableProperty(name, phrase, props, type) 

308 

309 def add_property(self, name, prop): 

310 self.properties[name].props.append(prop) 

311 

312 

313class Lexer: 

314 expression = re.compile( 

315 r'[\r\t ]*(?P<token>(?P<punc>[:,;.])|(?P<word>[a-zA-Z\']+)|(?P<color>#(?:[a-fA-F0-9]{3}){1,2})|' + 

316 r'(?P<number>-?[0-9]+(?P<fraction>\.[0-9]+)?)|(?P<string>"(?:[^"\\]|\\["\\nbt])+"))[\r\t ]*' 

317 ) 

318 

319 def __init__(self, text): 

320 self.text = text 

321 self.pos = 0 

322 self.token = None 

323 self.line = 1 

324 self.line_pos = 0 

325 self.tokens = [] 

326 self.token_index = 0 

327 

328 def _new_token(self, token: Token): 

329 self.token = token 

330 self.tokens.append(token) 

331 self.token_index = len(self.tokens) 

332 return token 

333 

334 def next(self): 

335 if self.token_index < len(self.tokens): 

336 self.token = self.tokens[self.token_index] 

337 self.token_index += 1 

338 return self.token 

339 

340 while True: 

341 if self.pos >= len(self.text): 

342 return self._new_token(Token(TokenType.Eof, self.line, self.pos - self.line_pos, self.pos, self.pos)) 

343 

344 if self.text[self.pos] == '\n': 

345 self.line += 1 

346 self.pos += 1 

347 self.line_pos = self.pos 

348 else: 

349 match = self.expression.match(self.text, self.pos) 

350 if match: 

351 break 

352 

353 self.pos += 1 

354 

355 if match.group("word"): 

356 type = TokenType.Word 

357 value = match.group("word").lower() 

358 elif match.group("number"): 

359 type = TokenType.Number 

360 if match.group("fraction"): 

361 value = float(match.group("number")) 

362 else: 

363 value = int(match.group("number")) 

364 elif match.group("string"): 

365 type = TokenType.String 

366 value = match.group("string")[1:-1] 

367 elif match.group("punc"): 

368 value = match.group("punc") 

369 type = TokenType.Punctuation 

370 elif match.group("color"): 

371 value = match.group("color") 

372 type = TokenType.Color 

373 

374 self.pos = match.end("token") 

375 return self._new_token(Token(type, self.line, self.pos - self.line_pos, match.start("token"), self.pos, value)) 

376 

377 def back(self): 

378 if self.token_index > 0: 

379 self.restore(self.token_index - 1) 

380 

381 def save(self): 

382 return self.token_index 

383 

384 def restore(self, index): 

385 self.token_index = index 

386 if index > 0: 

387 self.token = self.tokens[self.token_index-1] 

388 else: 

389 self.token = self.tokens[0] 

390 

391 

392class Logger: 

393 def warn(self, message): 

394 sys.stderr.write(message) 

395 sys.stderr.write("\n") 

396 

397 

398class DummyLogger(Logger): 

399 def warn(self, message): 

400 pass 

401 

402 

403class StorageLogger(Logger): 

404 def __init__(self): 

405 self.messages = [] 

406 

407 def warn(self, message): 

408 self.messages.append(message) 

409 

410 

411class Parser: 

412 sides = { 

413 "penta": 5, 

414 "hexa": 6, 

415 "hepta": 7, 

416 "octa": 8, 

417 "ennea": 9, 

418 "deca": 10, 

419 } 

420 

421 def __init__(self, text, logger: Logger): 

422 self.lexer = Lexer(text) 

423 self.lexer.next() 

424 self.logger = logger 

425 self.allow_resize = True 

426 self.max_duration = None 

427 self.svg_shapes = [] 

428 self.font = FontStyle("Ubuntu", 80) 

429 

430 def next(self): 

431 return self.lexer.next() 

432 

433 @property 

434 def token(self): 

435 return self.lexer.token 

436 

437 def color(self): 

438 return self.get_color(color_words, "") 

439 

440 def get_color_value(self, value, complete_word: str): 

441 if isinstance(value, str): 

442 return NVector(*color_table[value]) 

443 elif isinstance(value, (list, tuple)): 

444 return NVector(*value) 

445 elif isinstance(value, NVector): 

446 return value 

447 else: 

448 return NVector(*color_table[complete_word]) 

449 

450 def complete_color(self, word_dict: dict, complete_word: str): 

451 if "_" in word_dict: 

452 return self.get_color_value(complete_word) 

453 else: 

454 next_item = next(iter(word_dict.item())) 

455 return self.complete_color(next_item[1], complete_word + next_item[0]) 

456 

457 def get_color(self, word_dict: dict, complete_word: str): 

458 if self.token.type == TokenType.Color: 

459 color = parse_color(self.token.value) 

460 self.next() 

461 return color 

462 

463 if self.token.type != TokenType.Word: 

464 return None 

465 

466 value = word_dict.get(self.token.value, None) 

467 if not value: 

468 return None 

469 

470 if isinstance(value, dict): 

471 next_word = complete_word + self.token.value 

472 self.next() 

473 color = self.get_color(value, next_word) 

474 if color is not None: 

475 return color 

476 

477 if "_" in value: 

478 return self.get_color_value(value["_"], next_word) 

479 

480 self.warn("Incomplete color name") 

481 else: 

482 return self.get_color_value(value, complete_word) 

483 

484 def warn(self, message): 

485 token = self.token 

486 self.logger.warn("At line %s column %s, near %r: %s" % (token.line, token.col, token, message)) 

487 

488 def parse(self): 

489 self.lottie = Animation(180, 60) 

490 if self.article(): 

491 if self.check_words("animation", "composition"): 

492 self.next() 

493 self.animation() 

494 else: 

495 self.lexer.back() 

496 self.layers(self.lottie) 

497 if self.token.type != TokenType.Eof: 

498 self.warn("Extra tokens") 

499 return self.lottie 

500 

501 def article(self): 

502 if self.check_words("a", "an", "the"): 

503 self.next() 

504 return True 

505 return False 

506 

507 def check_words(self, *words): 

508 if self.token.type != TokenType.Word: 

509 return False 

510 

511 return self.token.value in words 

512 

513 def skip_words(self, *words): 

514 if self.check_words(*words): 

515 self.next() 

516 return True 

517 return False 

518 

519 def require_one_of(self, *words): 

520 if self.check_words(*words): 

521 return True 

522 

523 self.warn("Expected " + repr(words[0])) 

524 return False 

525 

526 def check_word_sequence(self, words): 

527 token = self.token 

528 index = self.lexer.save() 

529 for word in words: 

530 if token.type != TokenType.Word or token.value != word: 

531 break 

532 token = self.next() 

533 else: 

534 return True 

535 

536 self.lexer.restore(index) 

537 return False 

538 

539 def possesive(self): 

540 return self.skip_words("its", "his", "her", "their") 

541 

542 def properties(self, shape_data, callback, callback_args=[], words=["with"]): 

543 lexind_and = -1 

544 while True: 

545 must_have_property = self.skip_words(*words) 

546 

547 self.article() or self.possesive() 

548 

549 lexind = self.lexer.save() 

550 

551 if not callback(*callback_args): 

552 self.lexer.restore(lexind) 

553 

554 if shape_data and not self.shape_common_property(shape_data): 

555 if must_have_property: 

556 self.warn("Unknown property") 

557 break 

558 if lexind_and != -1: 

559 self.lexer.restore(lexind_and) 

560 break 

561 

562 lexind_and = self.lexer.save() 

563 if not self.skip_and(): 

564 break 

565 

566 def simple_properties_callback(self, object, properties): 

567 if self.check_words(*properties.keys()): 

568 prop = self.token.value 

569 self.next() 

570 if self.check_words("of"): 

571 self.next() 

572 

573 value = properties[prop](getattr(object, prop)) 

574 setattr(object, prop, value) 

575 return True 

576 return False 

577 

578 def animation(self): 

579 while True: 

580 if self.check_words("lasts", "lasting"): 

581 self.next() 

582 if self.check_words("for"): 

583 self.next() 

584 self.lottie.out_point = self.time(self.lottie.out_point) 

585 elif self.check_words("stops", "stopping", "loops", "looping"): 

586 if self.check_words("for", "after"): 

587 self.next() 

588 self.lottie.out_point = self.time(self.lottie.out_point) 

589 if self.max_duration and self.lottie.out_point > self.max_duration: 

590 self.lottie.out_point = self.max_duration 

591 elif self.check_words("with", "has"): 

592 props = { 

593 "width": self.integer, 

594 "height": self.integer, 

595 "name": self.string 

596 } 

597 if not self.allow_resize: 

598 props.pop("width") 

599 props.pop("height") 

600 self.properties(None, self.simple_properties_callback, [self.lottie, props], ["with", "has"]) 

601 elif self.skip_and(): 

602 pass 

603 else: 

604 return 

605 

606 def time(self, default): 

607 if self.token.type != TokenType.Number: 

608 self.warn("Expected time") 

609 return default 

610 

611 amount = self.token.value 

612 

613 self.next() 

614 if self.check_words("seconds", "second"): 

615 amount *= self.lottie.frame_rate 

616 elif self.check_words("milliseconds", "millisecond"): 

617 amount *= self.lottie.frame_rate / 1000 

618 elif not self.check_words("frames", "frame"): 

619 self.warn("Missing time unit") 

620 return amount 

621 

622 self.next() 

623 return amount 

624 

625 def integer(self, default, warn=True): 

626 if self.token.type != TokenType.Number or not isinstance(self.token.value, int): 

627 if warn: 

628 self.warn("Expected integer") 

629 return default 

630 

631 val = self.token.value 

632 self.next() 

633 return val 

634 

635 def number(self, default): 

636 if self.token.type != TokenType.Number: 

637 self.warn("Expected number") 

638 return default 

639 

640 val = self.token.value 

641 self.next() 

642 

643 return val 

644 

645 def string(self, default): 

646 if self.token.type != TokenType.String: 

647 self.warn("Expected string") 

648 return default 

649 

650 val = self.token.value 

651 self.next() 

652 return val 

653 

654 def layers(self, composition): 

655 while True: 

656 if self.token.type == TokenType.Punctuation and self.token.value in ";.": 

657 self.next() 

658 self.skip_and() 

659 self.skip_words("then") or self.skip_words("finally") 

660 

661 if self.check_words("there's"): 

662 self.next() 

663 self.layer(composition) 

664 elif self.check_words("there"): 

665 self.next() 

666 if self.check_words("is", "are"): 

667 self.next() 

668 self.layer(composition) 

669 elif self.article(): 

670 self.lexer.back() 

671 self.layer(composition) 

672 elif self.token.type == TokenType.Number and isinstance(self.token.value, int): 

673 self.layer(composition) 

674 else: 

675 break 

676 

677 def count(self, default=1): 

678 if self.article(): 

679 return 1 

680 return self.integer(default) 

681 

682 def layer(self, composition): 

683 if self.token.type != TokenType.Word: 

684 self.warn("Expected shape") 

685 return 

686 

687 layer = layers.ShapeLayer() 

688 layer.in_point = self.lottie.in_point 

689 layer.out_point = self.lottie.out_point 

690 composition.insert_layer(0, layer) 

691 

692 self.shape_list(layer) 

693 

694 if self.token.type == TokenType.Punctuation and self.token.value in ",;.": 

695 self.next() 

696 

697 def skip_and(self): 

698 if self.token.type == TokenType.Punctuation and self.token.value == ",": 

699 self.next() 

700 self.skip_words("and") 

701 return True 

702 return self.skip_words("and") 

703 

704 def shape_list(self, parent): 

705 extent = min(self.lottie.width, self.lottie.height) * 0.4 

706 shape = ShapeData(extent) 

707 shape.count = self.count() 

708 

709 while True: 

710 ok = False 

711 

712 color = self.color() 

713 if color: 

714 ok = True 

715 shape.color = color 

716 shape.color_explicit = True 

717 

718 if self.check_words("transparent", "invisible"): 

719 self.next() 

720 shape.color = None 

721 ok = True 

722 

723 size_mult = self.size_multiplitier() 

724 if size_mult: 

725 ok = True 

726 shape.scale(size_mult) 

727 

728 if self.check_words("portrait"): 

729 self.next() 

730 shape.portrait = True 

731 ok = True 

732 

733 if self.check_words("landscape"): 

734 self.next() 

735 shape.portrait = False 

736 ok = True 

737 

738 lexind = self.lexer.save() 

739 qualifier = self.size_qualifier() 

740 if self.check_words("rounded"): 

741 shape.roundness = qualifier 

742 self.next() 

743 ok = True 

744 elif self.check_words("transparent"): 

745 shape.opacity = (1 / qualifier) 

746 self.next() 

747 ok = True 

748 elif lexind < self.lexer.token_index: 

749 self.lexer.restore(lexind) 

750 

751 if self.check_words("star", "polygon", "ellipse", "rectangle", "circle", "square", "text"): 

752 shape_type = self.token.value 

753 function = getattr(self, "shape_" + shape_type) 

754 self.next() 

755 shape_object = function(shape) 

756 self.add_shape(parent, shape_object, shape) 

757 return 

758 

759 if self.token.type == TokenType.Word: 

760 for name, sides in self.sides.items(): 

761 if self.token.value.startswith(name): 

762 if self.token.value.endswith("gon"): 

763 self.next() 

764 shape_object = self.shape_polygon(shape, sides) 

765 self.add_shape(parent, shape_object, shape) 

766 return 

767 elif self.token.value.endswith("gram"): 

768 self.next() 

769 shape_object = self.shape_star(shape, sides) 

770 self.add_shape(parent, shape_object, shape) 

771 return 

772 

773 if self.check_words("triangle"): 

774 self.next() 

775 shape_object = self.shape_polygon(shape, 3) 

776 self.add_shape(parent, shape_object, shape) 

777 return 

778 

779 sides = self.integer(None, False) 

780 if sides is not None: 

781 if self.check_words("sided", "pointed"): 

782 self.next() 

783 if self.check_words("polygon", "star"): 

784 shape_type = self.token.value 

785 function = getattr(self, "shape_" + shape_type) 

786 self.next() 

787 shape_object = function(shape, sides) 

788 self.add_shape(parent, shape_object, shape) 

789 return 

790 else: 

791 self.warn("Expected 'star' or 'polygon'") 

792 return 

793 else: 

794 continue 

795 

796 for svg_shape in self.svg_shapes: 

797 if svg_shape.match(parent, self, shape): 

798 return 

799 

800 if not ok: 

801 self.next() 

802 break 

803 

804 self.warn("Expected shape") 

805 

806 def size_qualifier(self): 

807 base = 1 

808 

809 while True: 

810 if self.check_words("very", "much"): 

811 self.next() 

812 base *= 1.33 

813 elif self.check_words("extremely"): 

814 self.next() 

815 base *= 1.5 

816 elif self.check_words("incredibly"): 

817 self.next() 

818 base *= 2 

819 else: 

820 break 

821 

822 return base 

823 

824 def size_multiplitier(self): 

825 lexind = self.lexer.save() 

826 base = self.size_qualifier() 

827 

828 if self.check_words("small"): 

829 self.next() 

830 return 0.8 / base 

831 elif self.check_words("large", "big"): 

832 self.next() 

833 return 1.2 * base 

834 elif self.check_words("tiny"): 

835 self.next() 

836 return 0.5 / base 

837 elif self.check_words("huge"): 

838 self.next() 

839 return 1.6 * base 

840 else: 

841 self.lexer.restore(lexind) 

842 return None 

843 

844 def add_shape(self, parent, shape_object, shape_data): 

845 g = shapes.Group() 

846 g.add_shape(shape_object) 

847 

848 if shape_data.stroke and shape_data.stroke_on_top: 

849 g.add_shape(shape_data.stroke) 

850 

851 if shape_data.color: 

852 fill = shapes.Fill(shape_data.color) 

853 g.add_shape(fill) 

854 shape_data.define_property("color", AnimatableType.Color, [fill.color]) 

855 

856 if shape_data.stroke and not shape_data.stroke_on_top: 

857 g.add_shape(shape_data.stroke) 

858 

859 if shape_data.opacity != 1: 

860 g.transform.opacity.value = 100 * shape_data.opacity 

861 

862 if "position" in shape_data.properties: 

863 center = shape_data.properties["position"].get_initial() 

864 else: 

865 center = g.bounding_box(0).center() 

866 g.transform.position.value = self.position(g, 0) + center 

867 g.transform.anchor_point.value = NVector(*center) 

868 shape_data.define_property("position", AnimatableType.Position, [g.transform.position], ["moves"]) 

869 shape_data.define_property("rotation", AnimatableType.Angle, [g.transform.rotation], ["rotates"]) 

870 

871 if self.check_words("rotated"): 

872 self.next() 

873 g.transform.rotation.value = self.angle(0) 

874 

875 parent.insert_shape(0, g) 

876 

877 if self.skip_words("that"): 

878 self.animated_properties(shape_data) 

879 

880 return g 

881 

882 def position(self, shape: shapes.Group, time: float): 

883 px = 0 

884 py = 0 

885 

886 qual = self.size_qualifier() 

887 

888 if self.check_words("to", "in", "towards", "on", "at"): 

889 self.next() 

890 if self.check_words("the"): 

891 self.next() 

892 if self.token.type != TokenType.Word: 

893 self.warn("Expected position") 

894 return 

895 

896 while True: 

897 if self.check_words("left"): 

898 px = -1 

899 elif self.check_words("right"): 

900 px = 1 

901 elif self.check_words("top"): 

902 py = -1 

903 elif self.check_words("bottom"): 

904 py = 1 

905 elif self.check_words("center", "middle"): 

906 pass 

907 else: 

908 break 

909 

910 self.next() 

911 

912 if self.check_words("side", "corner"): 

913 self.next() 

914 

915 if px == 0 and py == 0: 

916 return shape.transform.position.get_value(time) 

917 

918 box = shape.bounding_box(time) 

919 center = box.center() 

920 left = box.width / 2 

921 right = self.lottie.width - box.width / 2 

922 top = box.height / 2 

923 bottom = self.lottie.height - box.height / 2 

924 

925 pos = shape.transform.position.get_value(time) 

926 x = pos.x 

927 y = pos.y 

928 dx = dy = 0 

929 

930 if px < 0: 

931 dx = left - center.x 

932 elif px > 0: 

933 dx = right - center.y 

934 

935 if py < 0: 

936 dy = top - center.y 

937 elif py > 0: 

938 dy = bottom - center.y 

939 

940 return NVector( 

941 x + dx * qual, 

942 y + dy * qual, 

943 ) 

944 

945 def animation_time(self, time, required): 

946 if self.skip_words("at"): 

947 if self.skip_words("the"): 

948 self.require_one_of("end") 

949 self.next() 

950 return self.lottie.out_point 

951 return self.time(time) 

952 if self.skip_words("after"): 

953 return self.time(0) + time 

954 if required: 

955 return time 

956 return None 

957 

958 def animated_properties(self, shape_data: ShapeData): 

959 time = 0 

960 

961 while True: 

962 prop_time = self.animation_time(time, False) 

963 changing = self.skip_words("changing", "changes") 

964 possesive = self.possesive() 

965 found_property = None 

966 value = None 

967 loop = False 

968 

969 if possesive or changing: 

970 for property in shape_data.properties.values(): 

971 if self.check_word_sequence(property.name): 

972 found_property = property 

973 if possesive and not changing: 

974 if self.skip_words("loops"): 

975 self.skip_words("back") 

976 loop = True 

977 break 

978 else: 

979 self.skip_words("changes") 

980 value = self.animated_property(shape_data, property, time) 

981 break 

982 else: 

983 for property in shape_data.properties.values(): 

984 if property.phrase and self.check_word_sequence(property.phrase): 

985 found_property = property 

986 value = self.animated_property(shape_data, property, time) 

987 break 

988 

989 if not found_property: 

990 self.warn("Unknown property") 

991 break 

992 

993 if prop_time is None: 

994 self.prop_time = self.animation_time(time, True) 

995 time = prop_time 

996 

997 if loop: 

998 found_property.loop(time) 

999 else: 

1000 if value is None: 

1001 break 

1002 

1003 found_property.add_keyframe(time, value) 

1004 

1005 cont = self.skip_and() 

1006 cont = self.skip_words("then") or cont 

1007 if not cont: 

1008 break 

1009 

1010 def animated_property_value(self, property: AnimatableProperty, time): 

1011 if property.type == AnimatableType.Angle: 

1012 return self.angle(None) 

1013 elif property.type == AnimatableType.Number: 

1014 return self.number(None) 

1015 elif property.type == AnimatableType.Integer: 

1016 return self.integer(None) 

1017 elif property.type == AnimatableType.Color: 

1018 value = self.color() 

1019 if not value: 

1020 self.warn("Expected color") 

1021 return value 

1022 elif property.type == AnimatableType.Position: 

1023 return self.position_value(property.get_value(time)) 

1024 elif property.type == AnimatableType.Size: 

1025 return self.vector_value() 

1026 return None 

1027 

1028 def animated_property(self, shape_data: ShapeData, property: AnimatableProperty, time): 

1029 relative = False 

1030 if not self.skip_words("to"): 

1031 relative = self.skip_words("by") 

1032 

1033 value = self.animated_property_value(property, time) 

1034 

1035 if value is not None and relative: 

1036 value += property.get_value(time) 

1037 

1038 return value 

1039 

1040 def vector_value(self): 

1041 x = self.number(0) 

1042 if self.token.type == TokenType.Punctuation and self.token.value == ",": 

1043 self.next() 

1044 y = self.number(0) 

1045 

1046 return NVector(x, y) 

1047 

1048 def position_value(self, start: NVector): 

1049 

1050 if self.token.type == TokenType.Word: 

1051 direction = None 

1052 

1053 if self.check_words("left") or self.check_word_sequence("the", "left"): 

1054 direction = NVector(-1, 0) 

1055 elif self.check_words("right") or self.check_word_sequence("the", "right"): 

1056 direction = NVector(1, 0) 

1057 elif self.check_words("up", "upwards", "upward"): 

1058 direction = NVector(0, -1) 

1059 elif self.check_words("down", "downwards", "downward"): 

1060 direction = NVector(0, 1) 

1061 

1062 if direction is None: 

1063 self.warn("Expected direction or position") 

1064 return start 

1065 

1066 self.skip_words("by") 

1067 

1068 return start + direction * self.number() 

1069 

1070 return self.vector_value() 

1071 

1072 def shape_common_property(self, shape_data: ShapeData): 

1073 color = NVector(0, 0, 0) 

1074 width = 4 

1075 

1076 while True: 

1077 got_color = self.color() 

1078 if got_color: 

1079 color = got_color 

1080 continue 

1081 

1082 quant = self.size_qualifier() 

1083 if self.check_words("thick"): 

1084 self.next() 

1085 width *= 1.5 * quant 

1086 continue 

1087 elif self.check_words("thin"): 

1088 self.next() 

1089 width *= 0.6 / quant 

1090 continue 

1091 else: 

1092 break 

1093 

1094 if self.check_words("stroke", "border", "outline", "edge", "borders", "edges"): 

1095 self.next() 

1096 shape_data.stroke = shapes.Stroke(color, width) 

1097 return True 

1098 

1099 return False 

1100 

1101 def animated_properties_callback(self, shape_data: ShapeData): 

1102 for property in shape_data.properties.values(): 

1103 if self.check_word_sequence(property.name): 

1104 self.skip_words("to", "is", "are") 

1105 property.set_initial(self.animated_property_value(property, 0)) 

1106 return True 

1107 return False 

1108 

1109 def shape_square(self, shape_data: ShapeData): 

1110 pos = NVector(self.lottie.width / 2, self.lottie.height / 2) 

1111 size = NVector(shape_data.extent, shape_data.extent) 

1112 round_base = shape_data.extent / 5 

1113 shape = shapes.Rect(pos, size, shape_data.roundness * round_base) 

1114 shape_data.define_property("position", AnimatableType.Position, [shape.position]) 

1115 shape_data.define_property("size", AnimatableType.Size, [shape.position]) 

1116 self.properties(shape_data, self.animated_properties_callback, [shape_data], ["with", "of"]) 

1117 return shape 

1118 

1119 def shape_circle(self, shape_data: ShapeData): 

1120 pos = NVector(self.lottie.width / 2, self.lottie.height / 2) 

1121 size = NVector(shape_data.extent, shape_data.extent) 

1122 shape = shapes.Ellipse(pos, size) 

1123 shape_data.define_property("position", AnimatableType.Position, [shape.position]) 

1124 shape_data.define_property("size", AnimatableType.Size, [shape.position]) 

1125 self.properties(shape_data, self.animated_properties_callback, [shape_data], ["with", "of"]) 

1126 return shape 

1127 

1128 def shape_star(self, shape_data: ShapeData, sides: int = None): 

1129 pos = NVector(self.lottie.width / 2, self.lottie.height / 2) 

1130 round_base = shape_data.extent / 5 

1131 roundness = shape_data.roundness * round_base 

1132 shape = shapes.Star() 

1133 shape.position.value = pos 

1134 shape.inner_roundness.value = roundness 

1135 shape.outer_radius.value = shape_data.extent / 2 

1136 shape.outer_roundness.value = roundness 

1137 shape.star_type = shapes.StarType.Star 

1138 shape.points.value = sides or 5 

1139 

1140 def callback(): 

1141 if self.animated_properties_callback(shape_data): 

1142 return True 

1143 

1144 if self.check_words("diameter"): 

1145 shape.outer_radius.value = self.number(shape.outer_radius.value) / 2 

1146 return True 

1147 elif self.token.type == TokenType.Number: 

1148 if sides: 

1149 self.warn("Number of sides already specified") 

1150 shape.points.value = self.integer(shape.points.value) 

1151 if self.require_one_of("points", "sides", "point", "side"): 

1152 self.next() 

1153 return True 

1154 return False 

1155 

1156 shape.inner_radius.value = shape.outer_radius.value / 2 

1157 

1158 shape_data.define_property("position", AnimatableType.Position, [shape.position]) 

1159 shape_data.define_property("outer radius", AnimatableType.Number, [shape.outer_radius]) 

1160 shape_data.define_property("radius", AnimatableType.Number, [shape.outer_radius]) 

1161 shape_data.define_property("inner radius", AnimatableType.Number, [shape.inner_radius]) 

1162 self.properties(shape_data, callback, [], ["with", "of", "has"]) 

1163 

1164 return shape 

1165 

1166 def shape_polygon(self, shape_data: ShapeData, sides: int = None): 

1167 shape = self.shape_star(shape_data, sides) 

1168 shape.inner_radius = None 

1169 shape.inner_roundness = None 

1170 shape.star_type = shapes.StarType.Polygon 

1171 return shape 

1172 

1173 def angle_direction(self): 

1174 if self.check_words("clockwise"): 

1175 self.next() 

1176 return 1 

1177 elif self.check_words("counter"): 

1178 self.next() 

1179 if self.check_words("clockwise"): 

1180 self.next() 

1181 return -1 

1182 return 0 

1183 

1184 def fraction(self): 

1185 if self.article(): 

1186 amount = 1 

1187 else: 

1188 amount = self.number(1) 

1189 

1190 if self.check_words("full", "entire"): 

1191 self.next() 

1192 return amount, True 

1193 elif self.check_words("half", "halfs"): 

1194 self.next() 

1195 return amount / 2, True 

1196 elif self.check_words("third", "thirds"): 

1197 self.next() 

1198 return amount / 3, True 

1199 elif self.check_words("quarter", "quarters"): 

1200 self.next() 

1201 return amount / 3, True 

1202 

1203 return amount, False 

1204 

1205 def angle(self, default): 

1206 direction = self.angle_direction() 

1207 

1208 amount, has_fraction = self.fraction() 

1209 

1210 if self.skip_and(): 

1211 more_frac = self.fraction()[0] 

1212 if has_fraction: 

1213 amount += amount * more_frac 

1214 else: 

1215 amount += more_frac 

1216 

1217 if self.check_words("turns"): 

1218 self.next() 

1219 amount *= 360 

1220 elif self.require_one_of("degrees"): 

1221 self.next() 

1222 elif self.check_word_sequence(["pi", "radians"]): 

1223 amount *= 180 

1224 if direction == 0: 

1225 direction = -1 

1226 

1227 if direction == 0: 

1228 direction = 1 

1229 return amount * direction 

1230 

1231 def rect_properties(self, shape_data: ShapeData, shape): 

1232 extent = shape_data.extent 

1233 parse_data = { 

1234 "width": None, 

1235 "height": None, 

1236 "ratio": math.sqrt(2), 

1237 "size_specified": False 

1238 } 

1239 handle_orientation = True 

1240 

1241 shape_data.define_property("position", AnimatableType.Position, [shape.position]) 

1242 shape_data.define_property("size", AnimatableType.Size, [shape.size]) 

1243 

1244 def callback(): 

1245 if self.animated_properties_callback(shape_data): 

1246 return True 

1247 

1248 if self.check_words("ratio"): 

1249 self.next() 

1250 ratio = self.fraction()[0] 

1251 if ratio <= 0: 

1252 self.warn("Ratio must be positive") 

1253 else: 

1254 parse_data["ratio"] = ratio 

1255 return True 

1256 elif self.check_words("width"): 

1257 self.next() 

1258 parse_data["width"] = self.number(0) 

1259 return True 

1260 elif self.check_words("height"): 

1261 self.next() 

1262 parse_data["height"] = self.number(0) 

1263 return True 

1264 elif self.check_words("size"): 

1265 parse_data["size_specified"] = True 

1266 

1267 return False 

1268 

1269 self.properties(shape_data, callback, [], ["with", "of", "has"]) 

1270 

1271 if not parse_data["size_specified"]: 

1272 width = parse_data["width"] 

1273 height = parse_data["height"] 

1274 ratio = parse_data["ratio"] 

1275 

1276 if width is None and height is None: 

1277 width = extent 

1278 height = width / ratio 

1279 elif width is None: 

1280 width = height * ratio 

1281 elif height is None: 

1282 height = width / ratio 

1283 else: 

1284 handle_orientation = False 

1285 

1286 if handle_orientation: 

1287 if (width > height and shape_data.portrait) or (width < height and not shape_data.portrait): 

1288 width, height = height, width 

1289 

1290 shape.size.value = NVector(width, height) 

1291 

1292 def shape_rectangle(self, shape_data: ShapeData): 

1293 pos = NVector(self.lottie.width / 2, self.lottie.height / 2) 

1294 round_base = shape_data.extent / 5 

1295 

1296 shape = shapes.Rect(pos, NVector(0, 0), shape_data.roundness * round_base) 

1297 shape_data.define_property("roundness", AnimatableType.Number, [shape.rounded]) 

1298 self.rect_properties(shape_data, shape) 

1299 return shape 

1300 

1301 def shape_ellipse(self, shape_data: ShapeData): 

1302 pos = NVector(self.lottie.width / 2, self.lottie.height / 2) 

1303 shape = shapes.Ellipse(pos, NVector(0, 0)) 

1304 self.rect_properties(shape_data, shape) 

1305 return shape 

1306 

1307 def shape_text(self, shape_data: ShapeData): 

1308 text = self.string("") 

1309 font = self.font.clone() 

1310 shape = font.render(text) 

1311 

1312 box = shape.bounding_box() 

1313 

1314 center = box.center() 

1315 anim_center = NVector(self.lottie.width / 2, self.lottie.height / 2) 

1316 shape.transform.anchor_point.value = anim_center 

1317 shape.transform.position.value.y = self.lottie.height - center.y 

1318 shape.transform.position.value.x = self.lottie.width - center.x 

1319 

1320 scale = shape_data.size_multiplitier 

1321 if box.width > self.lottie.width > 0: 

1322 scale *= self.lottie.width / box.width 

1323 

1324 if scale != 1: 

1325 wrapper = shapes.Group() 

1326 wrapper.transform.position.value = wrapper.transform.anchor_point.value = anim_center 

1327 wrapper.transform.scale.value *= scale 

1328 wrapper.add_shape(shape) 

1329 return wrapper 

1330 

1331 return shape 

1332 

1333 

1334class SvgLoader: 

1335 def __init__(self): 

1336 self.cache = {} 

1337 

1338 def load(self, filename): 

1339 if filename in self.cache: 

1340 return self.cache[filename] 

1341 

1342 anim = parse_svg_file(filename) 

1343 self.cache[filename] = anim 

1344 return anim 

1345 

1346 

1347class SvgFeature: 

1348 def __init__(self, layer_names, colors): 

1349 self.layer_names = layer_names 

1350 self.colors = [parse_color(color) for color in colors] 

1351 

1352 def iter_stylers(self, lottie_object): 

1353 objects = [] 

1354 if self.layer_names: 

1355 for layer_name in self.layer_names: 

1356 found = lottie_object.find(layer_name) 

1357 if found: 

1358 objects.append(found) 

1359 else: 

1360 objects = [lottie_object] 

1361 

1362 for object in objects: 

1363 for styler in object.find_all((shapes.Fill, shapes.Stroke)): 

1364 if len(self.colors) == 0 or styler.color.value in self.colors: 

1365 yield styler 

1366 

1367 def process(self, lottie_object, new_color): 

1368 for styler in self.iter_stylers(lottie_object): 

1369 styler.color.value = new_color 

1370 

1371 

1372class SvgShape: 

1373 def __init__(self, file, phrase, feature_map, main_feature, facing_direction, svg_loader: SvgLoader): 

1374 self.file = file 

1375 self.phrase = phrase 

1376 self.feature_map = feature_map 

1377 if isinstance(main_feature, str): 

1378 self.main_feature = feature_map[main_feature] 

1379 else: 

1380 self.main_feature = main_feature 

1381 self.facing_direction = facing_direction 

1382 self.svg_loader = svg_loader 

1383 

1384 def callback(self, parser: Parser, shape_data: ShapeData): 

1385 lexind = parser.lexer.save() 

1386 color = parser.color() 

1387 if color: 

1388 if parser.check_words(*self.feature_map.keys()): 

1389 feature_name = parser.lexer.token.value 

1390 shape_data.properties[feature_name].set_initial(color) 

1391 parser.next() 

1392 return True 

1393 else: 

1394 parser.lexer.restore(lexind) 

1395 

1396 return False 

1397 

1398 def match(self, parent, parser: Parser, shape_data: ShapeData): 

1399 if not parser.check_word_sequence(self.phrase): 

1400 return False 

1401 

1402 shape_data.stroke_on_top = False 

1403 svg_anim = parse_svg_file(self.file, 0, parser.lottie.out_point, parser.lottie.frame_rate) 

1404 layer = svg_anim.layers[0].clone() 

1405 group = shapes.Group() 

1406 group.name = layer.name 

1407 group.shapes = layer.shapes + group.shapes 

1408 layer.transform.clone_into(group.transform) 

1409 

1410 wrapper = shapes.Group() 

1411 wrapper.add_shape(group) 

1412 

1413 delta_ap = group.bounding_box(0).center() 

1414 wrapper.transform.anchor_point.value += delta_ap 

1415 wrapper.transform.position.value += delta_ap 

1416 wrapper.transform.scale.value *= shape_data.size_multiplitier 

1417 

1418 for name, feature in self.feature_map.items(): 

1419 singular = name[:-1] if name.endswith("s") else name 

1420 shape_data.properties[name] = AnimatableProperty(singular + " color", None, [], AnimatableType.Color) 

1421 for styler in feature.iter_stylers(group): 

1422 shape_data.add_property(name, styler.color) 

1423 

1424 if self.main_feature: 

1425 shape_data.define_property("color", AnimatableType.Color, []) 

1426 

1427 for styler in self.main_feature.iter_stylers(group): 

1428 shape_data.add_property("color", styler.color) 

1429 

1430 if shape_data.color and shape_data.color_explicit: 

1431 shape_data.properties["color"].set_initial(shape_data.color) 

1432 

1433 parser.properties(shape_data, self.callback, [parser, shape_data], ["with"]) 

1434 

1435 if self.facing_direction != 0 and parser.check_words("facing", "looking"): 

1436 parser.next() 

1437 if parser.skip_words("to"): 

1438 parser.skip_words("the") 

1439 

1440 if parser.check_words("left", "right"): 

1441 direction = -1 if parser.token.value == "left" else 1 

1442 if direction != self.facing_direction: 

1443 wrapper.transform.scale.value.x *= -1 

1444 else: 

1445 parser.warn("Missing facing direction") 

1446 

1447 parser.next() 

1448 

1449 shape_data.color = None 

1450 parser.add_shape(parent, wrapper, shape_data) 

1451 return True