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)