Coverage for lib/lottie/utils/font.py: 20%

578 statements  

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

1import os 

2import sys 

3import subprocess 

4import fontTools.pens.basePen 

5import fontTools.ttLib 

6import fontTools.t1Lib 

7from fontTools.pens.boundsPen import ControlBoundsPen 

8import enum 

9import math 

10from xml.etree import ElementTree 

11from ..nvector import NVector 

12from ..objects.bezier import Bezier, BezierPoint 

13from ..objects.shapes import Path, Group, Fill, Stroke 

14from ..objects.text import TextJustify 

15from ..objects.base import LottieProp, CustomObject 

16from ..objects.layers import ShapeLayer 

17 

18 

19class BezierPen(fontTools.pens.basePen.BasePen): 

20 def __init__(self, glyphSet, offset=NVector(0, 0)): 

21 super().__init__(glyphSet) 

22 self.beziers = [] 

23 self.current = Bezier() 

24 self.offset = offset 

25 

26 def _point(self, pt): 

27 return self.offset + NVector(pt[0], -pt[1]) 

28 

29 def _moveTo(self, pt): 

30 self._endPath() 

31 

32 def _endPath(self): 

33 if len(self.current.points): 

34 self.beziers.append(self.current) 

35 self.current = Bezier() 

36 

37 def _closePath(self): 

38 self.current.close() 

39 self._endPath() 

40 

41 def _lineTo(self, pt): 

42 if len(self.current.points) == 0: 

43 self.current.points.append(self._point(self._getCurrentPoint())) 

44 

45 self.current.points.append(self._point(pt)) 

46 

47 def _curveToOne(self, pt1, pt2, pt3): 

48 if len(self.current.points) == 0: 

49 cp = self._point(self._getCurrentPoint()) 

50 self.current.points.append( 

51 BezierPoint( 

52 cp, 

53 None, 

54 self._point(pt1) - cp 

55 ) 

56 

57 ) 

58 else: 

59 self.current.points[-1].out_tangent = self._point(pt1) - self.current.points[-1].vertex 

60 

61 dest = self._point(pt3) 

62 self.current.points.append( 

63 BezierPoint( 

64 dest, 

65 self._point(pt2) - dest, 

66 None, 

67 ) 

68 ) 

69 

70 

71class SystemFont: 

72 def __init__(self, family): 

73 self.family = family 

74 self.files = {} 

75 self.styles = set() 

76 self._renderers = {} 

77 

78 def add_file(self, styles, file): 

79 self.styles |= set(styles) 

80 key = self._key(styles) 

81 self.files.setdefault(key, file) 

82 

83 def filename(self, styles): 

84 return self.files[self._key(styles)] 

85 

86 def _key(self, styles): 

87 if isinstance(styles, str): 

88 return (styles,) 

89 return tuple(sorted(styles)) 

90 

91 def __getitem__(self, styles): 

92 key = self._key(styles) 

93 if key in self._renderers: 

94 return self._renderers[key] 

95 fr = RawFontRenderer(self.files[key]) 

96 self._renderers[key] = fr 

97 return fr 

98 

99 def __repr__(self): 

100 return "<SystemFont %s>" % self.family 

101 

102 

103class FontQuery: 

104 """! 

105 @see https://www.freedesktop.org/software/fontconfig/fontconfig-user.html#AEN21 

106 https://manpages.ubuntu.com/manpages/cosmic/man1/fc-pattern.1.html 

107 """ 

108 def __init__(self, str=""): 

109 self._query = {} 

110 if isinstance(str, FontQuery): 

111 self._query = str._query.copy() 

112 elif str: 

113 chunks = str.split(":") 

114 family = chunks.pop(0) 

115 self._query = dict( 

116 chunk.split("=") 

117 for chunk in chunks 

118 if chunk 

119 ) 

120 self.family(family) 

121 

122 def family(self, name): 

123 self._query["family"] = name 

124 return self 

125 

126 def weight(self, weight): 

127 self._query["weight"] = weight 

128 return self 

129 

130 def css_weight(self, weight): 

131 """! 

132 Weight from CSS weight value. 

133 

134 Weight is different between CSS and fontconfig 

135 This creates some interpolations to ensure known values are translated properly 

136 @see https://www.freedesktop.org/software/fontconfig/fontconfig-user.html#AEN178 

137 https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight#Common_weight_name_mapping 

138 """ 

139 if weight < 200: 

140 v = max(0, weight - 100) / 100 * 40 

141 elif weight < 500: 

142 v = -weight**3 / 200000 + weight**2 * 11/2000 - weight * 17/10 + 200 

143 elif weight < 700: 

144 v = -weight**2 * 3/1000 + weight * 41/10 - 1200 

145 else: 

146 v = (weight - 700) / 200 * 10 + 200 

147 return self.weight(int(round(v))) 

148 

149 def style(self, *styles): 

150 self._query["style"] = " ".join(styles) 

151 return self 

152 

153 def charset(self, *hex_ranges): 

154 self._query["charset"] = " ".join(hex_ranges) 

155 return self 

156 

157 def char(self, char): 

158 return self.charset("%x" % ord(char)) 

159 

160 def custom(self, property, value): 

161 self._query[property] = value 

162 return self 

163 

164 def clone(self): 

165 return FontQuery(self) 

166 

167 def __getitem__(self, key): 

168 return self._query.get(key, "") 

169 

170 def __contains__(self, item): 

171 return item in self._query 

172 

173 def get(self, key, default=None): 

174 return self._query.get(key, default) 

175 

176 def __str__(self): 

177 return self._query.get("family", "") + ":" + ":".join( 

178 "%s=%s" % (p, v) 

179 for p, v in self._query.items() 

180 if p != "family" 

181 ) 

182 

183 def __repr__(self): 

184 return "<FontQuery %r>" % str(self) 

185 

186 def weight_to_css(self): 

187 x = int(self["weight"]) 

188 if x < 40: 

189 v = x / 40 * 100 + 100 

190 elif x < 100: 

191 v = x**3/300 - x**2 * 11/15 + x*167/3 - 3200/3 

192 elif x < 200: 

193 v = (2050 - 10 * math.sqrt(5) * math.sqrt(1205 - 6 * x)) / 3 

194 else: 

195 v = (x - 200) * 200 / 10 + 700 

196 return int(round(v)) 

197 

198 

199class _SystemFontList: 

200 def __init__(self): 

201 self.fonts = None 

202 

203 def _lazy_load(self): 

204 if self.fonts is None: 

205 self.load() 

206 

207 def load(self): 

208 self.fonts = {} 

209 self.load_fc_list() 

210 

211 def cmd(self, *a): 

212 p = subprocess.Popen(a, stdout=subprocess.PIPE) 

213 out, err = p.communicate() 

214 out = out.decode("utf-8").strip() 

215 return out, p.returncode 

216 

217 def load_fc_list(self): 

218 out, returncode = self.cmd("fc-list", r'--format=%{file}\t%{family[0]}\t%{style[0]}\n') 

219 if returncode == 0: 

220 for line in out.splitlines(): 

221 file, family, styles = line.split("\t") 

222 self._get(family).add_file(styles.split(" "), file) 

223 

224 def best(self, query): 

225 """! 

226 Returns the renderer best matching the name 

227 """ 

228 out, returncode = self.cmd("fc-match", r"--format=%{family}\t%{style}", str(query)) 

229 if returncode == 0: 

230 return self._font_from_match(out) 

231 

232 def _font_from_match(self, out): 

233 fam, style = out.split("\t") 

234 fam = fam.split(",")[0] 

235 style = style.split(",")[0].split() 

236 return self[fam][style] 

237 

238 def all(self, query): 

239 """! 

240 Yields all the renderers matching a query 

241 """ 

242 out, returncode = self.cmd("fc-match", "-s", r"--format=%{family}\t%{style}\n", str(query)) 

243 if returncode == 0: 

244 for line in out.splitlines(): 

245 try: 

246 yield self._font_from_match(line) 

247 except (fontTools.ttLib.TTLibError, fontTools.t1Lib.T1Error): 

248 pass 

249 

250 def default(self): 

251 """! 

252 Returns the default fornt renderer 

253 """ 

254 return self.best() 

255 

256 def _get(self, family): 

257 self._lazy_load() 

258 if family in self.fonts: 

259 return self.fonts[family] 

260 font = SystemFont(family) 

261 self.fonts[family] = font 

262 return font 

263 

264 def __getitem__(self, key): 

265 self._lazy_load() 

266 return self.fonts[key] 

267 

268 def __iter__(self): 

269 self._lazy_load() 

270 return iter(self.fonts.values()) 

271 

272 def keys(self): 

273 self._lazy_load() 

274 return self.fonts.keys() 

275 

276 def __contains__(self, item): 

277 self._lazy_load() 

278 return item in self.fonts 

279 

280 

281## Dictionary of system fonts 

282fonts = _SystemFontList() 

283 

284 

285def collect_kerning_pairs(font): 

286 if "GPOS" not in font: 

287 return {} 

288 

289 gpos_table = font["GPOS"].table 

290 

291 unique_kern_lookups = set() 

292 for item in gpos_table.FeatureList.FeatureRecord: 

293 if item.FeatureTag == "kern": 

294 feature = item.Feature 

295 unique_kern_lookups |= set(feature.LookupListIndex) 

296 

297 kerning_pairs = {} 

298 for kern_lookup_index in sorted(unique_kern_lookups): 

299 lookup = gpos_table.LookupList.Lookup[kern_lookup_index] 

300 if lookup.LookupType in {2, 9}: 

301 for pairPos in lookup.SubTable: 

302 if pairPos.LookupType == 9: # extension table 

303 if pairPos.ExtensionLookupType == 8: # contextual 

304 continue 

305 elif pairPos.ExtensionLookupType == 2: 

306 pairPos = pairPos.ExtSubTable 

307 

308 if pairPos.Format != 1: 

309 continue 

310 

311 firstGlyphsList = pairPos.Coverage.glyphs 

312 for ps_index, _ in enumerate(pairPos.PairSet): 

313 for pairValueRecordItem in pairPos.PairSet[ps_index].PairValueRecord: 

314 secondGlyph = pairValueRecordItem.SecondGlyph 

315 valueFormat = pairPos.ValueFormat1 

316 

317 if valueFormat == 5: # RTL kerning 

318 kernValue = "<%d 0 %d 0>" % ( 

319 pairValueRecordItem.Value1.XPlacement, 

320 pairValueRecordItem.Value1.XAdvance) 

321 elif valueFormat == 0: # RTL pair with value <0 0 0 0> 

322 kernValue = "<0 0 0 0>" 

323 elif valueFormat == 4: # LTR kerning 

324 kernValue = pairValueRecordItem.Value1.XAdvance 

325 else: 

326 print( 

327 "\tValueFormat1 = %d" % valueFormat, 

328 file=sys.stdout) 

329 continue # skip the rest 

330 

331 kerning_pairs[(firstGlyphsList[ps_index], secondGlyph)] = kernValue 

332 return kerning_pairs 

333 

334 

335class GlyphMetrics: 

336 def __init__(self, glyph, lsb, aw, xmin, xmax): 

337 self.glyph = glyph 

338 self.lsb = lsb 

339 self.advance = aw 

340 self.xmin = xmin 

341 self.xmax = xmax 

342 self.width = xmax - xmin 

343 self.advance = xmax 

344 

345 def draw(self, pen): 

346 return self.glyph.draw(pen) 

347 

348 

349class Font: 

350 def __init__(self, wrapped): 

351 self.wrapped = wrapped 

352 if isinstance(self.wrapped, fontTools.ttLib.TTFont): 

353 self.cmap = self.wrapped.getBestCmap() or {} 

354 else: 

355 self.cmap = {} 

356 

357 self.glyphset = self.wrapped.getGlyphSet() 

358 

359 @classmethod 

360 def open(cls, filename): 

361 try: 

362 f = fontTools.ttLib.TTFont(filename) 

363 except fontTools.ttLib.TTLibError: 

364 f = fontTools.t1Lib.T1Font(filename) 

365 f.parse() 

366 

367 return cls(f) 

368 

369 def getGlyphSet(self): 

370 return self.wrapped.getGlyphSet() 

371 

372 def getBestCmap(self): 

373 return {} 

374 

375 def glyph_name(self, codepoint): 

376 if isinstance(codepoint, str): 

377 if len(codepoint) != 1: 

378 return "" 

379 codepoint = ord(codepoint) 

380 

381 if codepoint in self.cmap: 

382 return self.cmap[codepoint] 

383 

384 return self.calculated_glyph_name(codepoint) 

385 

386 @staticmethod 

387 def calculated_glyph_name(codepoint): 

388 from fontTools import agl # Adobe Glyph List 

389 if codepoint in agl.UV2AGL: 

390 return agl.UV2AGL[codepoint] 

391 elif codepoint <= 0xFFFF: 

392 return "uni%04X" % codepoint 

393 else: 

394 return "u%X" % codepoint 

395 

396 def scale(self): 

397 if isinstance(self.wrapped, fontTools.ttLib.TTFont): 

398 return 1 / self.wrapped["head"].unitsPerEm 

399 elif isinstance(self.wrapped, fontTools.t1Lib.T1Font): 

400 return self.wrapped["FontMatrix"][0] 

401 

402 def yMax(self): 

403 if isinstance(self.wrapped, fontTools.ttLib.TTFont): 

404 return self.wrapped["head"].yMax 

405 elif isinstance(self.wrapped, fontTools.t1Lib.T1Font): 

406 return self.wrapped["FontBBox"][3] 

407 

408 def glyph(self, glyph_name): 

409 if isinstance(self.wrapped, fontTools.ttLib.TTFont): 

410 glyph = self.glyphset[glyph_name] 

411 table = self.glyphset.glyfTable[glyph_name] 

412 xmin = getattr(table, "xMin", glyph.lsb) 

413 xmax = getattr(table, "xMax", glyph.width) 

414 return GlyphMetrics(glyph, glyph.lsb, glyph.width, xmin, xmax) 

415 elif isinstance(self.wrapped, fontTools.t1Lib.T1Font): 

416 glyph = self.glyphset[glyph_name] 

417 bounds_pen = ControlBoundsPen(self.glyphset) 

418 bounds = bounds_pen.bounds 

419 glyph.draw(bounds_pen) 

420 if not hasattr(glyph, "width"): 

421 advance = bounds[2] 

422 else: 

423 advance = glyph.width 

424 return GlyphMetrics(glyph, bounds[0], advance, bounds[0], bounds[2]) 

425 

426 def __contains__(self, key): 

427 if isinstance(self.wrapped, fontTools.t1Lib.T1Font): 

428 return key in self.wrapped.font 

429 return key in self.wrapped 

430 

431 def __getitem__(self, key): 

432 return self.wrapped[key] 

433 

434 

435class FontRenderer: 

436 tab_width = 4 

437 

438 @property 

439 def font(self): 

440 raise NotImplementedError 

441 

442 def get_query(self): 

443 raise NotImplementedError 

444 

445 def kerning(self, c1, c2): 

446 return 0 

447 

448 def text_to_chars(self, text): 

449 return text 

450 

451 def _on_missing(self, char, size, pos, group): 

452 """! 

453 - Character as string 

454 - Font size 

455 - [in, out] Character position 

456 - Group shape 

457 """ 

458 

459 def glyph_name(self, ch): 

460 return self.font.glyph_name(ch) 

461 

462 def scale(self, size): 

463 return size * self.font.scale() 

464 

465 def line_height(self, size): 

466 return self.font.yMax() * self.scale(size) 

467 

468 def ex(self, size): 

469 return self.font.glyph("x").advance * self.scale(size) 

470 

471 def glyph_beziers(self, glyph, offset=NVector(0, 0)): 

472 pen = BezierPen(self.font.glyphset, offset) 

473 glyph.draw(pen) 

474 return pen.beziers 

475 

476 def glyph_shapes(self, glyph, offset=NVector(0, 0)): 

477 beziers = self.glyph_beziers(glyph, offset) 

478 return [ 

479 Path(bez) 

480 for bez in beziers 

481 ] 

482 

483 def _on_character(self, ch, size, pos, scale, line, use_kerning, chars, i): 

484 chname = self.glyph_name(ch) 

485 

486 if chname in self.font.glyphset: 

487 glyphdata = self.font.glyph(chname) 

488 #pos.x += glyphdata.lsb * scale 

489 glyph_shapes = self.glyph_shapes(glyphdata, pos / scale) 

490 

491 if glyph_shapes: 

492 if len(glyph_shapes) > 1: 

493 glyph_shape_group = line.add_shape(Group()) 

494 glyph_shape = glyph_shape_group 

495 else: 

496 glyph_shape_group = line 

497 glyph_shape = glyph_shapes[0] 

498 

499 for sh in glyph_shapes: 

500 sh.shape.value.scale(scale) 

501 glyph_shape_group.add_shape(sh) 

502 

503 glyph_shape.name = ch 

504 

505 kerning = 0 

506 if use_kerning and i < len(chars) - 1: 

507 nextcname = chars[i+1] 

508 kerning = self.kerning(chname, nextcname) 

509 

510 pos.x += (glyphdata.advance + kerning) * scale 

511 return True 

512 return False 

513 

514 def render(self, text, size, pos=None, use_kerning=True, start_x=None): 

515 """! 

516 Renders some text 

517 

518 @param text String to render 

519 @param size Font size (in pizels) 

520 @param[in,out] pos Text position 

521 @param use_kerning Whether to honour kerning info from the font file 

522 @param start_x x-position of the start of a line 

523 

524 @returns a Group shape, augmented with some extra attributes: 

525 - line_height Line height 

526 - next_x X position of the next character 

527 """ 

528 scale = self.scale(size) 

529 line_height = self.line_height(size) 

530 group = Group() 

531 group.name = text 

532 if pos is None: 

533 pos = NVector(0, 0) 

534 start_x = pos.x if start_x is None else start_x 

535 line = Group() 

536 group.add_shape(line) 

537 #group.transform.scale.value = NVector(100, 100) * scale 

538 

539 chars = self.text_to_chars(text) 

540 for i, ch in enumerate(chars): 

541 if ch == "\n": 

542 line.next_x = pos.x 

543 pos.x = start_x 

544 pos.y += line_height 

545 line = Group() 

546 group.add_shape(line) 

547 continue 

548 elif ch == "\t": 

549 chname = self.glyph_name(ch) 

550 if chname in self.font.glyphset: 

551 width = self.font.glyph(chname).advance 

552 else: 

553 width = self.ex(size) 

554 pos.x += width * scale * self.tab_width 

555 continue 

556 

557 self._on_character(ch, size, pos, scale, line, use_kerning, chars, i) 

558 

559 group.line_height = line_height 

560 group.next_x = line.next_x = pos.x 

561 return group 

562 

563 

564class RawFontRenderer(FontRenderer): 

565 def __init__(self, filename): 

566 self.filename = filename 

567 self._font = Font.open(filename) 

568 self._kerning = None 

569 

570 @property 

571 def font(self): 

572 return self._font 

573 

574 def kerning(self, c1, c2): 

575 if self._kerning is None: 

576 self._kerning = collect_kerning_pairs(self.font) 

577 return self._kerning.get((c1, c2), 0) 

578 

579 def __repr__(self): 

580 return "<FontRenderer %r>" % self.filename 

581 

582 def get_query(self): 

583 return self.filename 

584 

585 

586class FallbackFontRenderer(FontRenderer): 

587 def __init__(self, query, max_attempts=10): 

588 self.query = FontQuery(query) 

589 self._best = None 

590 self._bq = None 

591 self._fallback = {} 

592 self.max_attempts = max_attempts 

593 

594 @property 

595 def font(self): 

596 return self.best.font 

597 

598 def get_query(self): 

599 return self.query 

600 

601 def ex(self, size): 

602 best = self.best 

603 if "x" not in self.font.glyphset: 

604 best = fonts.best(self.query.clone().char("x")) 

605 return best.ex(size) 

606 

607 @property 

608 def best(self): 

609 cq = str(self.query) 

610 if self._best is None or self._bq != cq: 

611 self._best = fonts.best(self.query) 

612 self._bq = cq 

613 return self._best 

614 

615 def fallback_renderer(self, char): 

616 if char in self._fallback: 

617 return self._fallback[char] 

618 

619 if len(char) != 1: 

620 return None 

621 

622 codepoint = ord(char) 

623 name = Font.calculated_glyph_name(codepoint) 

624 for i, font in enumerate(fonts.all(self.query.clone().char(char))): 

625 # For some reason fontconfig sometimes returns a font that doesn't 

626 # actually contain the glyph 

627 if name in font.font.glyphset or codepoint in font.font.cmap: 

628 self._fallback[char] = font 

629 return font 

630 

631 if i > self.max_attempts: 

632 self._fallback[char] = None 

633 return None 

634 

635 def _on_character(self, char, size, pos, scale, group, use_kerning, chars, i): 

636 if self.best._on_character(char, size, pos, scale, group, use_kerning, chars, i): 

637 return True 

638 

639 font = self.fallback_renderer(char) 

640 if not font: 

641 return False 

642 

643 child = font.render(char, size, pos) 

644 if len(child.shapes) == 2: 

645 group.add_shape(child.shapes[0]) 

646 else: 

647 group.add_shape(child) 

648 

649 def __repr__(self): 

650 return "<FallbackFontRenderer %s>" % self.query 

651 

652 

653class EmojiRenderer(FontRenderer): 

654 _split = None 

655 

656 def __init__(self, wrapped, emoji_dir): 

657 if not os.path.isdir(emoji_dir): 

658 raise Exception("Not a valid directory: %s" % emoji_dir) 

659 self.wrapped = wrapped 

660 self.emoji_dir = emoji_dir 

661 self._svgs = {} 

662 

663 @property 

664 def font(self): 

665 return self.wrapped.font 

666 

667 def emoji_basename(self, char): 

668 return "-".join("%x" % ord(cp) for cp in char) 

669 

670 def emoji_filename(self, char): 

671 return self._get_svg_filename(char)[1] 

672 

673 def _get_svg_filename(self, char): 

674 basename = self.emoji_basename(char) 

675 suffix = ".svg" 

676 

677 filename = os.path.join(self.emoji_dir, basename + suffix) 

678 if os.path.isfile(filename): 

679 return basename, filename 

680 

681 filename = os.path.join(self.emoji_dir, basename.upper() + suffix) 

682 if os.path.isfile(filename): 

683 return basename, filename 

684 

685 if char and char[-1] == '\ufe0f': 

686 return self._get_svg_filename(char[:-1]) 

687 

688 return None, None 

689 

690 def _get_svg(self, char): 

691 from ..parsers.svg import parse_svg_file 

692 

693 if char in self._svgs: 

694 return self._svgs[char] 

695 

696 basename, filename = self._get_svg_filename(char) 

697 if filename is None: 

698 self._svgs[char] = None 

699 return None 

700 

701 svga = parse_svg_file(filename) 

702 svgshape = Group() 

703 svgshape.name = basename 

704 for layer in svga.layers: 

705 if isinstance(layer, ShapeLayer): 

706 for shape in layer.shapes: 

707 svgshape.add_shape(shape) 

708 

709 self._svgs[char] = svgshape 

710 svgshape._bbox = svgshape.bounding_box() 

711 return svgshape 

712 

713 def _on_character(self, char, size, pos, scale, group, use_kerning, chars, i): 

714 svgshape = self._get_svg(char) 

715 if svgshape: 

716 target_height = self.line_height(size) 

717 scale = target_height / svgshape._bbox.height 

718 shape_group = Group() 

719 shape_group = svgshape.clone() 

720 shape_group.transform.scale.value *= scale 

721 offset = NVector( 

722 -svgshape._bbox.x1 + svgshape._bbox.width * 0.075, 

723 -svgshape._bbox.y2 + svgshape._bbox.height * 0.1 

724 ) 

725 shape_group.transform.position.value = pos + offset * scale 

726 group.add_shape(shape_group) 

727 pos.x += svgshape._bbox.width * scale 

728 return True 

729 return self.wrapped._on_character(char, size, pos, scale, group, use_kerning, chars, i) 

730 

731 def get_query(self): 

732 return self.wrapped.get_query() 

733 

734 @staticmethod 

735 def _get_splitter(): 

736 if EmojiRenderer._split is None: 

737 try: 

738 import grapheme 

739 EmojiRenderer._split = grapheme.graphemes 

740 except ImportError: 

741 sys.stderr.write("Install `grapheme` for better Emoji support\n") 

742 EmojiRenderer._split = lambda x: x 

743 return EmojiRenderer._split 

744 

745 @staticmethod 

746 def emoji_split(string): 

747 return EmojiRenderer._get_splitter()(string) 

748 

749 def text_to_chars(self, string): 

750 return list(self.emoji_split(string)) 

751 

752 

753class FontStyle: 

754 def __init__(self, query, size, justify=TextJustify.Left, position=None, use_kerning=True, emoji_svg=None): 

755 self.emoji_svg = emoji_svg 

756 self._set_query(query) 

757 self.size = size 

758 self.justify = justify 

759 self.position = position.clone() if position else NVector(0, 0) 

760 self.use_kerning = use_kerning 

761 

762 def _set_query(self, query): 

763 if isinstance(query, str) and os.path.isfile(query): 

764 self._renderer = RawFontRenderer(query) 

765 else: 

766 self._renderer = FallbackFontRenderer(query) 

767 

768 if self.emoji_svg: 

769 self._renderer = EmojiRenderer(self._renderer, self.emoji_svg) 

770 

771 @property 

772 def query(self): 

773 return self._renderer.get_query() 

774 

775 @query.setter 

776 def query(self, value): 

777 if str(value) != str(self.query): 

778 self._set_query(value) 

779 

780 @property 

781 def renderer(self): 

782 return self._renderer 

783 

784 def render(self, text, pos=NVector(0, 0)): 

785 group = self._renderer.render(text, self.size, self.position+pos, self.use_kerning) 

786 for subg in group.shapes[:-1]: 

787 width = subg.next_x - self.position.x - pos.x 

788 if self.justify == TextJustify.Center: 

789 subg.transform.position.value.x -= width / 2 

790 elif self.justify == TextJustify.Right: 

791 subg.transform.position.value.x -= width 

792 return group 

793 

794 def clone(self): 

795 return FontStyle(str(self.query), self.size, self.justify, NVector(*self.position), self.use_kerning) 

796 

797 @property 

798 def ex(self): 

799 return self._renderer.ex(self.size) 

800 

801 @property 

802 def line_height(self): 

803 return self._renderer.line_height(self.size) 

804 

805 

806def _propfac(a): 

807 return property(lambda s: s._get(a), lambda s, v: s._set(a, v)) 807 ↛ exitline 807 didn't run the lambda on line 807 or line 807 didn't run the lambda on line 807

808 

809 

810class FontShape(CustomObject): 

811 _props = [ 

812 LottieProp("query_string", "_query", str), 

813 LottieProp("size", "_size", float), 

814 LottieProp("justify", "_justify", TextJustify), 

815 LottieProp("text", "_text", str), 

816 LottieProp("position", "_position", NVector), 

817 ] 

818 wrapped_lottie = Group 

819 

820 def __init__(self, text="", query="", size=64, justify=TextJustify.Left): 

821 CustomObject.__init__(self) 

822 if isinstance(query, FontStyle): 

823 self.style = query 

824 else: 

825 self.style = FontStyle(query, size, justify) 

826 self.text = text 

827 self.hidden = None 

828 

829 def _get(self, a): 

830 return getattr(self.style, a) 

831 

832 def _set(self, a, v): 

833 return setattr(self.style, a, v) 

834 

835 query = _propfac("query") 

836 size = _propfac("size") 

837 justify = _propfac("justify") 

838 position = _propfac("position") 

839 

840 @property 

841 def query_string(self): 

842 return str(self.query) 

843 

844 @query_string.setter 

845 def query_string(self, v): 

846 self.query = v 

847 

848 def _build_wrapped(self): 

849 g = self.style.render(self.text) 

850 self.line_height = g.line_height 

851 return g 

852 

853 def bounding_box(self, time=0): 

854 return self.wrapped.bounding_box(time)