File indexing completed on 2025-01-19 03:59:54
0001 import os 0002 import sys 0003 import subprocess 0004 import fontTools.pens.basePen 0005 import fontTools.ttLib 0006 import fontTools.t1Lib 0007 from fontTools.pens.boundsPen import ControlBoundsPen 0008 import enum 0009 import math 0010 from xml.etree import ElementTree 0011 from ..nvector import NVector 0012 from ..objects.bezier import Bezier, BezierPoint 0013 from ..objects.shapes import Path, Group, Fill, Stroke 0014 from ..objects.text import TextJustify 0015 from ..objects.base import LottieProp, CustomObject 0016 from ..objects.layers import ShapeLayer 0017 0018 0019 class BezierPen(fontTools.pens.basePen.BasePen): 0020 def __init__(self, glyphSet, offset=NVector(0, 0)): 0021 super().__init__(glyphSet) 0022 self.beziers = [] 0023 self.current = Bezier() 0024 self.offset = offset 0025 0026 def _point(self, pt): 0027 return self.offset + NVector(pt[0], -pt[1]) 0028 0029 def _moveTo(self, pt): 0030 self._endPath() 0031 0032 def _endPath(self): 0033 if len(self.current.points): 0034 self.beziers.append(self.current) 0035 self.current = Bezier() 0036 0037 def _closePath(self): 0038 self.current.close() 0039 self._endPath() 0040 0041 def _lineTo(self, pt): 0042 if len(self.current.points) == 0: 0043 self.current.points.append(self._point(self._getCurrentPoint())) 0044 0045 self.current.points.append(self._point(pt)) 0046 0047 def _curveToOne(self, pt1, pt2, pt3): 0048 if len(self.current.points) == 0: 0049 cp = self._point(self._getCurrentPoint()) 0050 self.current.points.append( 0051 BezierPoint( 0052 cp, 0053 None, 0054 self._point(pt1) - cp 0055 ) 0056 0057 ) 0058 else: 0059 self.current.points[-1].out_tangent = self._point(pt1) - self.current.points[-1].vertex 0060 0061 dest = self._point(pt3) 0062 self.current.points.append( 0063 BezierPoint( 0064 dest, 0065 self._point(pt2) - dest, 0066 None, 0067 ) 0068 ) 0069 0070 0071 class SystemFont: 0072 def __init__(self, family): 0073 self.family = family 0074 self.files = {} 0075 self.styles = set() 0076 self._renderers = {} 0077 0078 def add_file(self, styles, file): 0079 self.styles |= set(styles) 0080 key = self._key(styles) 0081 self.files.setdefault(key, file) 0082 0083 def filename(self, styles): 0084 return self.files[self._key(styles)] 0085 0086 def _key(self, styles): 0087 if isinstance(styles, str): 0088 return (styles,) 0089 return tuple(sorted(styles)) 0090 0091 def __getitem__(self, styles): 0092 key = self._key(styles) 0093 if key in self._renderers: 0094 return self._renderers[key] 0095 fr = RawFontRenderer(self.files[key]) 0096 self._renderers[key] = fr 0097 return fr 0098 0099 def __repr__(self): 0100 return "<SystemFont %s>" % self.family 0101 0102 0103 class FontQuery: 0104 """! 0105 @see https://www.freedesktop.org/software/fontconfig/fontconfig-user.html#AEN21 0106 https://manpages.ubuntu.com/manpages/cosmic/man1/fc-pattern.1.html 0107 """ 0108 def __init__(self, str=""): 0109 self._query = {} 0110 if isinstance(str, FontQuery): 0111 self._query = str._query.copy() 0112 elif str: 0113 chunks = str.split(":") 0114 family = chunks.pop(0) 0115 self._query = dict( 0116 chunk.split("=") 0117 for chunk in chunks 0118 if chunk 0119 ) 0120 self.family(family) 0121 0122 def family(self, name): 0123 self._query["family"] = name 0124 return self 0125 0126 def weight(self, weight): 0127 self._query["weight"] = weight 0128 return self 0129 0130 def css_weight(self, weight): 0131 """! 0132 Weight from CSS weight value. 0133 0134 Weight is different between CSS and fontconfig 0135 This creates some interpolations to ensure known values are translated properly 0136 @see https://www.freedesktop.org/software/fontconfig/fontconfig-user.html#AEN178 0137 https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight#Common_weight_name_mapping 0138 """ 0139 if weight < 200: 0140 v = max(0, weight - 100) / 100 * 40 0141 elif weight < 500: 0142 v = -weight**3 / 200000 + weight**2 * 11/2000 - weight * 17/10 + 200 0143 elif weight < 700: 0144 v = -weight**2 * 3/1000 + weight * 41/10 - 1200 0145 else: 0146 v = (weight - 700) / 200 * 10 + 200 0147 return self.weight(int(round(v))) 0148 0149 def style(self, *styles): 0150 self._query["style"] = " ".join(styles) 0151 return self 0152 0153 def charset(self, *hex_ranges): 0154 self._query["charset"] = " ".join(hex_ranges) 0155 return self 0156 0157 def char(self, char): 0158 return self.charset("%x" % ord(char)) 0159 0160 def custom(self, property, value): 0161 self._query[property] = value 0162 return self 0163 0164 def clone(self): 0165 return FontQuery(self) 0166 0167 def __getitem__(self, key): 0168 return self._query.get(key, "") 0169 0170 def __contains__(self, item): 0171 return item in self._query 0172 0173 def get(self, key, default=None): 0174 return self._query.get(key, default) 0175 0176 def __str__(self): 0177 return self._query.get("family", "") + ":" + ":".join( 0178 "%s=%s" % (p, v) 0179 for p, v in self._query.items() 0180 if p != "family" 0181 ) 0182 0183 def __repr__(self): 0184 return "<FontQuery %r>" % str(self) 0185 0186 def weight_to_css(self): 0187 x = int(self["weight"]) 0188 if x < 40: 0189 v = x / 40 * 100 + 100 0190 elif x < 100: 0191 v = x**3/300 - x**2 * 11/15 + x*167/3 - 3200/3 0192 elif x < 200: 0193 v = (2050 - 10 * math.sqrt(5) * math.sqrt(1205 - 6 * x)) / 3 0194 else: 0195 v = (x - 200) * 200 / 10 + 700 0196 return int(round(v)) 0197 0198 0199 class _SystemFontList: 0200 def __init__(self): 0201 self.fonts = None 0202 0203 def _lazy_load(self): 0204 if self.fonts is None: 0205 self.load() 0206 0207 def load(self): 0208 self.fonts = {} 0209 self.load_fc_list() 0210 0211 def cmd(self, *a): 0212 p = subprocess.Popen(a, stdout=subprocess.PIPE) 0213 out, err = p.communicate() 0214 out = out.decode("utf-8").strip() 0215 return out, p.returncode 0216 0217 def load_fc_list(self): 0218 out, returncode = self.cmd("fc-list", r'--format=%{file}\t%{family[0]}\t%{style[0]}\n') 0219 if returncode == 0: 0220 for line in out.splitlines(): 0221 file, family, styles = line.split("\t") 0222 self._get(family).add_file(styles.split(" "), file) 0223 0224 def best(self, query): 0225 """! 0226 Returns the renderer best matching the name 0227 """ 0228 out, returncode = self.cmd("fc-match", r"--format=%{family}\t%{style}", str(query)) 0229 if returncode == 0: 0230 return self._font_from_match(out) 0231 0232 def _font_from_match(self, out): 0233 fam, style = out.split("\t") 0234 fam = fam.split(",")[0] 0235 style = style.split(",")[0].split() 0236 return self[fam][style] 0237 0238 def all(self, query): 0239 """! 0240 Yields all the renderers matching a query 0241 """ 0242 out, returncode = self.cmd("fc-match", "-s", r"--format=%{family}\t%{style}\n", str(query)) 0243 if returncode == 0: 0244 for line in out.splitlines(): 0245 try: 0246 yield self._font_from_match(line) 0247 except (fontTools.ttLib.TTLibError, fontTools.t1Lib.T1Error): 0248 pass 0249 0250 def default(self): 0251 """! 0252 Returns the default fornt renderer 0253 """ 0254 return self.best() 0255 0256 def _get(self, family): 0257 self._lazy_load() 0258 if family in self.fonts: 0259 return self.fonts[family] 0260 font = SystemFont(family) 0261 self.fonts[family] = font 0262 return font 0263 0264 def __getitem__(self, key): 0265 self._lazy_load() 0266 return self.fonts[key] 0267 0268 def __iter__(self): 0269 self._lazy_load() 0270 return iter(self.fonts.values()) 0271 0272 def keys(self): 0273 self._lazy_load() 0274 return self.fonts.keys() 0275 0276 def __contains__(self, item): 0277 self._lazy_load() 0278 return item in self.fonts 0279 0280 0281 ## Dictionary of system fonts 0282 fonts = _SystemFontList() 0283 0284 0285 def collect_kerning_pairs(font): 0286 if "GPOS" not in font: 0287 return {} 0288 0289 gpos_table = font["GPOS"].table 0290 0291 unique_kern_lookups = set() 0292 for item in gpos_table.FeatureList.FeatureRecord: 0293 if item.FeatureTag == "kern": 0294 feature = item.Feature 0295 unique_kern_lookups |= set(feature.LookupListIndex) 0296 0297 kerning_pairs = {} 0298 for kern_lookup_index in sorted(unique_kern_lookups): 0299 lookup = gpos_table.LookupList.Lookup[kern_lookup_index] 0300 if lookup.LookupType in {2, 9}: 0301 for pairPos in lookup.SubTable: 0302 if pairPos.LookupType == 9: # extension table 0303 if pairPos.ExtensionLookupType == 8: # contextual 0304 continue 0305 elif pairPos.ExtensionLookupType == 2: 0306 pairPos = pairPos.ExtSubTable 0307 0308 if pairPos.Format != 1: 0309 continue 0310 0311 firstGlyphsList = pairPos.Coverage.glyphs 0312 for ps_index, _ in enumerate(pairPos.PairSet): 0313 for pairValueRecordItem in pairPos.PairSet[ps_index].PairValueRecord: 0314 secondGlyph = pairValueRecordItem.SecondGlyph 0315 valueFormat = pairPos.ValueFormat1 0316 0317 if valueFormat == 5: # RTL kerning 0318 kernValue = "<%d 0 %d 0>" % ( 0319 pairValueRecordItem.Value1.XPlacement, 0320 pairValueRecordItem.Value1.XAdvance) 0321 elif valueFormat == 0: # RTL pair with value <0 0 0 0> 0322 kernValue = "<0 0 0 0>" 0323 elif valueFormat == 4: # LTR kerning 0324 kernValue = pairValueRecordItem.Value1.XAdvance 0325 else: 0326 print( 0327 "\tValueFormat1 = %d" % valueFormat, 0328 file=sys.stdout) 0329 continue # skip the rest 0330 0331 kerning_pairs[(firstGlyphsList[ps_index], secondGlyph)] = kernValue 0332 return kerning_pairs 0333 0334 0335 class GlyphMetrics: 0336 def __init__(self, glyph, lsb, aw, xmin, xmax): 0337 self.glyph = glyph 0338 self.lsb = lsb 0339 self.advance = aw 0340 self.xmin = xmin 0341 self.xmax = xmax 0342 self.width = xmax - xmin 0343 self.advance = xmax 0344 0345 def draw(self, pen): 0346 return self.glyph.draw(pen) 0347 0348 0349 class Font: 0350 def __init__(self, wrapped): 0351 self.wrapped = wrapped 0352 if isinstance(self.wrapped, fontTools.ttLib.TTFont): 0353 self.cmap = self.wrapped.getBestCmap() or {} 0354 else: 0355 self.cmap = {} 0356 0357 self.glyphset = self.wrapped.getGlyphSet() 0358 0359 @classmethod 0360 def open(cls, filename): 0361 try: 0362 f = fontTools.ttLib.TTFont(filename) 0363 except fontTools.ttLib.TTLibError: 0364 f = fontTools.t1Lib.T1Font(filename) 0365 f.parse() 0366 0367 return cls(f) 0368 0369 def getGlyphSet(self): 0370 return self.wrapped.getGlyphSet() 0371 0372 def getBestCmap(self): 0373 return {} 0374 0375 def glyph_name(self, codepoint): 0376 if isinstance(codepoint, str): 0377 if len(codepoint) != 1: 0378 return "" 0379 codepoint = ord(codepoint) 0380 0381 if codepoint in self.cmap: 0382 return self.cmap[codepoint] 0383 0384 return self.calculated_glyph_name(codepoint) 0385 0386 @staticmethod 0387 def calculated_glyph_name(codepoint): 0388 from fontTools import agl # Adobe Glyph List 0389 if codepoint in agl.UV2AGL: 0390 return agl.UV2AGL[codepoint] 0391 elif codepoint <= 0xFFFF: 0392 return "uni%04X" % codepoint 0393 else: 0394 return "u%X" % codepoint 0395 0396 def scale(self): 0397 if isinstance(self.wrapped, fontTools.ttLib.TTFont): 0398 return 1 / self.wrapped["head"].unitsPerEm 0399 elif isinstance(self.wrapped, fontTools.t1Lib.T1Font): 0400 return self.wrapped["FontMatrix"][0] 0401 0402 def yMax(self): 0403 if isinstance(self.wrapped, fontTools.ttLib.TTFont): 0404 return self.wrapped["head"].yMax 0405 elif isinstance(self.wrapped, fontTools.t1Lib.T1Font): 0406 return self.wrapped["FontBBox"][3] 0407 0408 def glyph(self, glyph_name): 0409 if isinstance(self.wrapped, fontTools.ttLib.TTFont): 0410 glyph = self.glyphset[glyph_name] 0411 0412 xmin = getattr(glyph._glyph, "xMin", glyph.lsb) 0413 xmax = getattr(glyph._glyph, "xMax", glyph.width) 0414 return GlyphMetrics(glyph, glyph.lsb, glyph.width, xmin, xmax) 0415 elif isinstance(self.wrapped, fontTools.t1Lib.T1Font): 0416 glyph = self.glyphset[glyph_name] 0417 bounds_pen = ControlBoundsPen(self.glyphset) 0418 bounds = bounds_pen.bounds 0419 glyph.draw(bounds_pen) 0420 if not hasattr(glyph, "width"): 0421 advance = bounds[2] 0422 else: 0423 advance = glyph.width 0424 return GlyphMetrics(glyph, bounds[0], advance, bounds[0], bounds[2]) 0425 0426 def __contains__(self, key): 0427 if isinstance(self.wrapped, fontTools.t1Lib.T1Font): 0428 return key in self.wrapped.font 0429 return key in self.wrapped 0430 0431 def __getitem__(self, key): 0432 return self.wrapped[key] 0433 0434 0435 class FontRenderer: 0436 tab_width = 4 0437 0438 @property 0439 def font(self): 0440 raise NotImplementedError 0441 0442 def get_query(self): 0443 raise NotImplementedError 0444 0445 def kerning(self, c1, c2): 0446 return 0 0447 0448 def text_to_chars(self, text): 0449 return text 0450 0451 def _on_missing(self, char, size, pos, group): 0452 """! 0453 - Character as string 0454 - Font size 0455 - [in, out] Character position 0456 - Group shape 0457 """ 0458 0459 def glyph_name(self, ch): 0460 return self.font.glyph_name(ch) 0461 0462 def scale(self, size): 0463 return size * self.font.scale() 0464 0465 def line_height(self, size): 0466 return self.font.yMax() * self.scale(size) 0467 0468 def ex(self, size): 0469 return self.font.glyph("x").advance * self.scale(size) 0470 0471 def glyph_beziers(self, glyph, offset=NVector(0, 0)): 0472 pen = BezierPen(self.font.glyphset, offset) 0473 glyph.draw(pen) 0474 return pen.beziers 0475 0476 def glyph_shapes(self, glyph, offset=NVector(0, 0)): 0477 beziers = self.glyph_beziers(glyph, offset) 0478 return [ 0479 Path(bez) 0480 for bez in beziers 0481 ] 0482 0483 def _on_character(self, ch, size, pos, scale, line, use_kerning, chars, i): 0484 chname = self.glyph_name(ch) 0485 0486 if chname in self.font.glyphset: 0487 glyphdata = self.font.glyph(chname) 0488 #pos.x += glyphdata.lsb * scale 0489 glyph_shapes = self.glyph_shapes(glyphdata, pos / scale) 0490 0491 if glyph_shapes: 0492 if len(glyph_shapes) > 1: 0493 glyph_shape_group = line.add_shape(Group()) 0494 glyph_shape = glyph_shape_group 0495 else: 0496 glyph_shape_group = line 0497 glyph_shape = glyph_shapes[0] 0498 0499 for sh in glyph_shapes: 0500 sh.shape.value.scale(scale) 0501 glyph_shape_group.add_shape(sh) 0502 0503 glyph_shape.name = ch 0504 0505 kerning = 0 0506 if use_kerning and i < len(chars) - 1: 0507 nextcname = chars[i+1] 0508 kerning = self.kerning(chname, nextcname) 0509 0510 pos.x += (glyphdata.advance + kerning) * scale 0511 return True 0512 return False 0513 0514 def render(self, text, size, pos=None, use_kerning=True): 0515 """! 0516 Renders some text 0517 0518 @param text String to render 0519 @param size Font size (in pizels) 0520 @param[in,out] pos Text position 0521 @param use_kerning Whether to honour kerning info from the font file 0522 0523 @returns a Group shape, augmented with some extra attributes: 0524 - line_height Line height 0525 - next_x X position of the next character 0526 """ 0527 scale = self.scale(size) 0528 line_height = self.line_height(size) 0529 group = Group() 0530 group.name = text 0531 if pos is None: 0532 pos = NVector(0, 0) 0533 start_x = pos.x 0534 line = Group() 0535 group.add_shape(line) 0536 #group.transform.scale.value = NVector(100, 100) * scale 0537 0538 chars = self.text_to_chars(text) 0539 for i, ch in enumerate(chars): 0540 if ch == "\n": 0541 line.next_x = pos.x 0542 pos.x = start_x 0543 pos.y += line_height 0544 line = Group() 0545 group.add_shape(line) 0546 continue 0547 elif ch == "\t": 0548 chname = self.glyph_name(ch) 0549 if chname in self.font.glyphset: 0550 width = self.font.glyph(chname).advance 0551 else: 0552 width = self.ex(size) 0553 pos.x += width * scale * self.tab_width 0554 continue 0555 0556 self._on_character(ch, size, pos, scale, line, use_kerning, chars, i) 0557 0558 group.line_height = line_height 0559 group.next_x = line.next_x = pos.x 0560 return group 0561 0562 0563 class RawFontRenderer(FontRenderer): 0564 def __init__(self, filename): 0565 self.filename = filename 0566 self._font = Font.open(filename) 0567 self._kerning = None 0568 0569 @property 0570 def font(self): 0571 return self._font 0572 0573 def kerning(self, c1, c2): 0574 if self._kerning is None: 0575 self._kerning = collect_kerning_pairs(self.font) 0576 return self._kerning.get((c1, c2), 0) 0577 0578 def __repr__(self): 0579 return "<FontRenderer %r>" % self.filename 0580 0581 def get_query(self): 0582 return self.filename 0583 0584 0585 class FallbackFontRenderer(FontRenderer): 0586 def __init__(self, query, max_attempts=10): 0587 self.query = FontQuery(query) 0588 self._best = None 0589 self._bq = None 0590 self._fallback = {} 0591 self.max_attempts = max_attempts 0592 0593 @property 0594 def font(self): 0595 return self.best.font 0596 0597 def get_query(self): 0598 return self.query 0599 0600 def ex(self, size): 0601 best = self.best 0602 if "x" not in self.font.glyphset: 0603 best = fonts.best(self.query.clone().char("x")) 0604 return best.ex(size) 0605 0606 @property 0607 def best(self): 0608 cq = str(self.query) 0609 if self._best is None or self._bq != cq: 0610 self._best = fonts.best(self.query) 0611 self._bq = cq 0612 return self._best 0613 0614 def fallback_renderer(self, char): 0615 if char in self._fallback: 0616 return self._fallback[char] 0617 0618 if len(char) != 1: 0619 return None 0620 0621 codepoint = ord(char) 0622 name = Font.calculated_glyph_name(codepoint) 0623 for i, font in enumerate(fonts.all(self.query.clone().char(char))): 0624 # For some reason fontconfig sometimes returns a font that doesn't 0625 # actually contain the glyph 0626 if name in font.font.glyphset or codepoint in font.cmap: 0627 self._fallback[char] = font 0628 return font 0629 0630 if i > self.max_attempts: 0631 self._fallback[char] = None 0632 return None 0633 0634 def _on_character(self, char, size, pos, scale, group, use_kerning, chars, i): 0635 if self.best._on_character(char, size, pos, scale, group, use_kerning, chars, i): 0636 return True 0637 0638 font = self.fallback_renderer(char) 0639 if not font: 0640 return False 0641 0642 child = font.render(char, size, pos) 0643 if len(child.shapes) == 2: 0644 group.add_shape(child.shapes[0]) 0645 else: 0646 group.add_shape(child) 0647 0648 def __repr__(self): 0649 return "<FallbackFontRenderer %s>" % self.query 0650 0651 0652 class EmojiRenderer(FontRenderer): 0653 _split = None 0654 0655 def __init__(self, wrapped, emoji_dir): 0656 if not os.path.isdir(emoji_dir): 0657 raise Exception("Not a valid directory: %s" % emoji_dir) 0658 self.wrapped = wrapped 0659 self.emoji_dir = emoji_dir 0660 self._svgs = {} 0661 0662 @property 0663 def font(self): 0664 return self.wrapped.font 0665 0666 def _get_svg(self, char): 0667 from ..parsers.svg import parse_svg_file 0668 0669 if char in self._svgs: 0670 return self._svgs[char] 0671 0672 basename = "-".join("%x" % ord(cp) for cp in char) + ".svg" 0673 filename = os.path.join(self.emoji_dir, basename) 0674 if not os.path.isfile(filename): 0675 self._svgs[char] = None 0676 return None 0677 0678 svga = parse_svg_file(filename) 0679 svgshape = Group() 0680 svgshape.name = basename 0681 for layer in svga.layers: 0682 if isinstance(layer, ShapeLayer): 0683 for shape in layer.shapes: 0684 svgshape.add_shape(shape) 0685 0686 self._svgs[char] = svgshape 0687 svgshape._bbox = svgshape.bounding_box() 0688 return svgshape 0689 0690 def _on_character(self, char, size, pos, scale, group, use_kerning, chars, i): 0691 svgshape = self._get_svg(char) 0692 if svgshape: 0693 target_height = self.line_height(size) 0694 scale = target_height / svgshape._bbox.height 0695 shape_group = Group() 0696 shape_group = svgshape.clone() 0697 shape_group.transform.scale.value *= scale 0698 offset = NVector( 0699 -svgshape._bbox.x1 + svgshape._bbox.width * 0.075, 0700 -svgshape._bbox.y2 + svgshape._bbox.height * 0.1 0701 ) 0702 shape_group.transform.position.value = pos + offset * scale 0703 group.add_shape(shape_group) 0704 pos.x += svgshape._bbox.width * scale 0705 return True 0706 return self.wrapped._on_character(char, size, pos, scale, group, use_kerning, chars, i) 0707 0708 def get_query(self): 0709 return self.wrapped.get_query() 0710 0711 @staticmethod 0712 def _get_splitter(): 0713 if EmojiRenderer._split is None: 0714 try: 0715 import grapheme 0716 EmojiRenderer._split = grapheme.graphemes 0717 except ImportError: 0718 sys.stderr.write("Install `grapheme` for better Emoji support\n") 0719 EmojiRenderer._split = lambda x: x 0720 return EmojiRenderer._split 0721 0722 @staticmethod 0723 def emoji_split(string): 0724 return EmojiRenderer._get_splitter()(string) 0725 0726 def text_to_chars(self, string): 0727 return list(self.emoji_split(string)) 0728 0729 0730 class FontStyle: 0731 def __init__(self, query, size, justify=TextJustify.Left, position=None, use_kerning=True, emoji_svg=None): 0732 self.emoji_svg = emoji_svg 0733 self._set_query(query) 0734 self.size = size 0735 self.justify = justify 0736 self.position = position.clone() if position else NVector(0, 0) 0737 self.use_kerning = use_kerning 0738 0739 def _set_query(self, query): 0740 if isinstance(query, str) and os.path.isfile(query): 0741 self._renderer = RawFontRenderer(query) 0742 else: 0743 self._renderer = FallbackFontRenderer(query) 0744 0745 if self.emoji_svg: 0746 self._renderer = EmojiRenderer(self._renderer, self.emoji_svg) 0747 0748 @property 0749 def query(self): 0750 return self._renderer.get_query() 0751 0752 @query.setter 0753 def query(self, value): 0754 if str(value) != str(self.query): 0755 self._set_query(value) 0756 0757 @property 0758 def renderer(self): 0759 return self._renderer 0760 0761 def render(self, text, pos=NVector(0, 0)): 0762 group = self._renderer.render(text, self.size, self.position+pos, self.use_kerning) 0763 for subg in group.shapes[:-1]: 0764 width = subg.next_x - self.position.x - pos.x 0765 if self.justify == TextJustify.Center: 0766 subg.transform.position.value.x -= width / 2 0767 elif self.justify == TextJustify.Right: 0768 subg.transform.position.value.x -= width 0769 return group 0770 0771 def clone(self): 0772 return FontStyle(str(self._renderer.query), self.size, self.justify, NVector(*self.position), self.use_kerning) 0773 0774 @property 0775 def ex(self): 0776 return self._renderer.ex(self.size) 0777 0778 @property 0779 def line_height(self): 0780 return self._renderer.line_height(self.size) 0781 0782 0783 def _propfac(a): 0784 return property(lambda s: s._get(a), lambda s, v: s._set(a, v)) 0785 0786 0787 class FontShape(CustomObject): 0788 _props = [ 0789 LottieProp("query_string", "_query", str), 0790 LottieProp("size", "_size", float), 0791 LottieProp("justify", "_justify", TextJustify), 0792 LottieProp("text", "_text", str), 0793 LottieProp("position", "_position", NVector), 0794 ] 0795 wrapped_lottie = Group 0796 0797 def __init__(self, text="", query="", size=64, justify=TextJustify.Left): 0798 CustomObject.__init__(self) 0799 if isinstance(query, FontStyle): 0800 self.style = query 0801 else: 0802 self.style = FontStyle(query, size, justify) 0803 self.text = text 0804 self.hidden = None 0805 0806 def _get(self, a): 0807 return getattr(self.style, a) 0808 0809 def _set(self, a, v): 0810 return setattr(self.style, a, v) 0811 0812 query = _propfac("query") 0813 size = _propfac("size") 0814 justify = _propfac("justify") 0815 position = _propfac("position") 0816 0817 @property 0818 def query_string(self): 0819 return str(self.query) 0820 0821 @query_string.setter 0822 def query_string(self, v): 0823 self.query = v 0824 0825 def _build_wrapped(self): 0826 g = self.style.render(self.text) 0827 self.line_height = g.line_height 0828 return g 0829 0830 def bounding_box(self, time=0): 0831 return self.wrapped.bounding_box(time)