python-lottie  0.6.11+deved4e6c7
A framework to work with lottie files and telegram animated stickers (tgs)
font.py
Go to the documentation of this file.
1 import os
2 import sys
3 import subprocess
5 import fontTools.ttLib
6 import fontTools.t1Lib
7 from fontTools.pens.boundsPen import ControlBoundsPen
8 import enum
9 import math
10 from xml.etree import ElementTree
11 from ..nvector import NVector
12 from ..objects.bezier import Bezier, BezierPoint
13 from ..objects.shapes import Path, Group, Fill, Stroke
14 from ..objects.text import TextJustify
15 from ..objects.base import LottieProp, CustomObject
16 from ..objects.layers import ShapeLayer
17 
18 
19 class BezierPen(fontTools.pens.basePen.BasePen):
20  def __init__(self, glyphSet, offset=NVector(0, 0)):
21  super().__init__(glyphSet)
22  self.beziersbeziers = []
23  self.currentcurrent = Bezier()
24  self.offsetoffset = offset
25 
26  def _point(self, pt):
27  return self.offsetoffset + NVector(pt[0], -pt[1])
28 
29  def _moveTo(self, pt):
30  self._endPath_endPath()
31 
32  def _endPath(self):
33  if len(self.currentcurrent.points):
34  self.beziersbeziers.append(self.currentcurrent)
35  self.currentcurrent = Bezier()
36 
37  def _closePath(self):
38  self.currentcurrent.close()
39  self._endPath_endPath()
40 
41  def _lineTo(self, pt):
42  if len(self.currentcurrent.points) == 0:
43  self.currentcurrent.points.append(self._point_point(self._getCurrentPoint()))
44 
45  self.currentcurrent.points.append(self._point_point(pt))
46 
47  def _curveToOne(self, pt1, pt2, pt3):
48  if len(self.currentcurrent.points) == 0:
49  cp = self._point_point(self._getCurrentPoint())
50  self.currentcurrent.points.append(
52  cp,
53  None,
54  self._point_point(pt1) - cp
55  )
56 
57  )
58  else:
59  self.currentcurrent.points[-1].out_tangent = self._point_point(pt1) - self.currentcurrent.points[-1].vertex
60 
61  dest = self._point_point(pt3)
62  self.currentcurrent.points.append(
64  dest,
65  self._point_point(pt2) - dest,
66  None,
67  )
68  )
69 
70 
71 class SystemFont:
72  def __init__(self, family):
73  self.familyfamily = family
74  self.filesfiles = {}
75  self.stylesstyles = set()
76  self._renderers_renderers = {}
77 
78  def add_file(self, styles, file):
79  self.stylesstyles |= set(styles)
80  key = self._key_key(styles)
81  self.filesfiles.setdefault(key, file)
82 
83  def filename(self, styles):
84  return self.filesfiles[self._key_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_key(styles)
93  if key in self._renderers_renderers:
94  return self._renderers_renderers[key]
95  fr = RawFontRenderer(self.filesfiles[key])
96  self._renderers_renderers[key] = fr
97  return fr
98 
99  def __repr__(self):
100  return "<SystemFont %s>" % self.familyfamily
101 
102 
103 class 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_query = {}
110  if isinstance(str, FontQuery):
111  self._query_query = str._query.copy()
112  elif str:
113  chunks = str.split(":")
114  family = chunks.pop(0)
115  self._query_query = dict(
116  chunk.split("=")
117  for chunk in chunks
118  if chunk
119  )
120  self.familyfamily(family)
121 
122  def family(self, name):
123  self._query_query["family"] = name
124  return self
125 
126  def weight(self, weight):
127  self._query_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.weightweight(int(round(v)))
148 
149  def style(self, *styles):
150  self._query_query["style"] = " ".join(styles)
151  return self
152 
153  def charset(self, *hex_ranges):
154  self._query_query["charset"] = " ".join(hex_ranges)
155  return self
156 
157  def char(self, char):
158  return self.charsetcharset("%x" % ord(char))
159 
160  def custom(self, property, value):
161  self._query_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_query.get(key, "")
169 
170  def __contains__(self, item):
171  return item in self._query_query
172 
173  def get(self, key, default=None):
174  return self._query_query.get(key, default)
175 
176  def __str__(self):
177  return self._query_query.get("family", "") + ":" + ":".join(
178  "%s=%s" % (p, v)
179  for p, v in self._query_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 
200  def __init__(self):
201  self.fontsfonts = None
202 
203  def _lazy_load(self):
204  if self.fontsfonts is None:
205  self.loadload()
206 
207  def load(self):
208  self.fontsfonts = {}
209  self.load_fc_listload_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.cmdcmd("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_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.cmdcmd("fc-match", r"--format=%{family}\t%{style}", str(query))
229  if returncode == 0:
230  return self._font_from_match_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.cmdcmd("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_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.bestbest()
255 
256  def _get(self, family):
257  self._lazy_load_lazy_load()
258  if family in self.fontsfonts:
259  return self.fontsfonts[family]
260  font = SystemFont(family)
261  self.fontsfonts[family] = font
262  return font
263 
264  def __getitem__(self, key):
265  self._lazy_load_lazy_load()
266  return self.fontsfonts[key]
267 
268  def __iter__(self):
269  self._lazy_load_lazy_load()
270  return iter(self.fontsfonts.values())
271 
272  def keys(self):
273  self._lazy_load_lazy_load()
274  return self.fontsfonts.keys()
275 
276  def __contains__(self, item):
277  self._lazy_load_lazy_load()
278  return item in self.fontsfonts
279 
280 
281 ## Dictionary of system fonts
283 
284 
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 
336  def __init__(self, glyph, lsb, aw, xmin, xmax):
337  self.glyphglyph = glyph
338  self.lsblsb = lsb
339  self.advanceadvance = aw
340  self.xminxmin = xmin
341  self.xmaxxmax = xmax
342  self.widthwidth = xmax - xmin
343  self.advanceadvance = xmax
344 
345  def draw(self, pen):
346  return self.glyphglyph.draw(pen)
347 
348 
349 class Font:
350  def __init__(self, wrapped):
351  self.wrappedwrapped = wrapped
352  if isinstance(self.wrappedwrapped, fontTools.ttLib.TTFont):
353  self.cmapcmap = self.wrappedwrapped.getBestCmap() or {}
354  else:
355  self.cmapcmap = {}
356 
357  self.glyphsetglyphset = self.wrappedwrapped.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.wrappedwrapped.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.cmapcmap:
382  return self.cmapcmap[codepoint]
383 
384  return self.calculated_glyph_namecalculated_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.wrappedwrapped, fontTools.ttLib.TTFont):
398  return 1 / self.wrappedwrapped["head"].unitsPerEm
399  elif isinstance(self.wrappedwrapped, fontTools.t1Lib.T1Font):
400  return self.wrappedwrapped["FontMatrix"][0]
401 
402  def yMax(self):
403  if isinstance(self.wrappedwrapped, fontTools.ttLib.TTFont):
404  return self.wrappedwrapped["head"].yMax
405  elif isinstance(self.wrappedwrapped, fontTools.t1Lib.T1Font):
406  return self.wrappedwrapped["FontBBox"][3]
407 
408  def glyph(self, glyph_name):
409  if isinstance(self.wrappedwrapped, fontTools.ttLib.TTFont):
410  glyph = self.glyphsetglyphset[glyph_name]
411 
412  xmin = getattr(glyph._glyph, "xMin", glyph.lsb)
413  xmax = getattr(glyph._glyph, "xMax", glyph.width)
414  return GlyphMetrics(glyph, glyph.lsb, glyph.width, xmin, xmax)
415  elif isinstance(self.wrappedwrapped, fontTools.t1Lib.T1Font):
416  glyph = self.glyphsetglyphset[glyph_name]
417  bounds_pen = ControlBoundsPen(self.glyphsetglyphset)
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.wrappedwrapped, fontTools.t1Lib.T1Font):
428  return key in self.wrappedwrapped.font
429  return key in self.wrappedwrapped
430 
431  def __getitem__(self, key):
432  return self.wrappedwrapped[key]
433 
434 
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.fontfont.scale()
464 
465  def line_height(self, size):
466  return self.fontfont.yMax() * self.scalescale(size)
467 
468  def ex(self, size):
469  return self.fontfont.glyph("x").advance * self.scalescale(size)
470 
471  def glyph_beziers(self, glyph, offset=NVector(0, 0)):
472  pen = BezierPen(self.fontfont.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_beziersglyph_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_nameglyph_name(ch)
485 
486  if chname in self.fontfont.glyphset:
487  glyphdata = self.fontfont.glyph(chname)
488  #pos.x += glyphdata.lsb * scale
489  glyph_shapes = self.glyph_shapesglyph_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.kerningkerning(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.scalescale(size)
529  line_height = self.line_heightline_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_charstext_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_nameglyph_name(ch)
550  if chname in self.fontfont.glyphset:
551  width = self.fontfont.glyph(chname).advance
552  else:
553  width = self.exex(size)
554  pos.x += width * scale * self.tab_widthtab_width
555  continue
556 
557  self._on_character_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 
565  def __init__(self, filename):
566  self.filenamefilename = filename
567  self._font_font = Font.open(filename)
568  self._kerning_kerning = None
569 
570  @property
571  def font(self):
572  return self._font_font
573 
574  def kerning(self, c1, c2):
575  if self._kerning_kerning is None:
576  self._kerning_kerning = collect_kerning_pairs(self.fontfontfont)
577  return self._kerning_kerning.get((c1, c2), 0)
578 
579  def __repr__(self):
580  return "<FontRenderer %r>" % self.filenamefilename
581 
582  def get_query(self):
583  return self.filenamefilename
584 
585 
587  def __init__(self, query, max_attempts=10):
588  self.queryquery = FontQuery(query)
589  self._best_best = None
590  self._bq_bq = None
591  self._fallback_fallback = {}
592  self.max_attemptsmax_attempts = max_attempts
593 
594  @property
595  def font(self):
596  return self.bestbest.font
597 
598  def get_query(self):
599  return self.queryquery
600 
601  def ex(self, size):
602  best = self.bestbest
603  if "x" not in self.fontfontfont.glyphset:
604  best = fonts.best(self.queryquery.clone().char("x"))
605  return best.ex(size)
606 
607  @property
608  def best(self):
609  cq = str(self.queryquery)
610  if self._best_best is None or self._bq_bq != cq:
611  self._best_best = fonts.best(self.queryquery)
612  self._bq_bq = cq
613  return self._best_best
614 
615  def fallback_renderer(self, char):
616  if char in self._fallback_fallback:
617  return self._fallback_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.queryquery.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_fallback[char] = font
629  return font
630 
631  if i > self.max_attemptsmax_attempts:
632  self._fallback_fallback[char] = None
633  return None
634 
635  def _on_character(self, char, size, pos, scale, group, use_kerning, chars, i):
636  if self.bestbest._on_character(char, size, pos, scale, group, use_kerning, chars, i):
637  return True
638 
639  font = self.fallback_rendererfallback_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.queryquery
651 
652 
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.wrappedwrapped = wrapped
660  self.emoji_diremoji_dir = emoji_dir
661  self._svgs_svgs = {}
662 
663  @property
664  def font(self):
665  return self.wrappedwrapped.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_get_svg_filename(char)[1]
672 
673  def _get_svg_filename(self, char):
674  basename = self.emoji_basenameemoji_basename(char)
675  suffix = ".svg"
676 
677  filename = os.path.join(self.emoji_diremoji_dir, basename + suffix)
678  if os.path.isfile(filename):
679  return basename, filename
680 
681  filename = os.path.join(self.emoji_diremoji_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_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_svgs:
694  return self._svgs_svgs[char]
695 
696  basename, filename = self._get_svg_filename_get_svg_filename(char)
697  if filename is None:
698  self._svgs_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_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_get_svg(char)
715  if svgshape:
716  target_height = self.line_heightline_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.wrappedwrapped._on_character(char, size, pos, scale, group, use_kerning, chars, i)
730 
731  def get_query(self):
732  return self.wrappedwrapped.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_splitemoji_split(string))
751 
752 
753 class FontStyle:
754  def __init__(self, query, size, justify=TextJustify.Left, position=None, use_kerning=True, emoji_svg=None):
755  self.emoji_svgemoji_svg = emoji_svg
756  self._set_query_set_query(query)
757  self.sizesize = size
758  self.justifyjustify = justify
759  self.positionposition = position.clone() if position else NVector(0, 0)
760  self.use_kerninguse_kerning = use_kerning
761 
762  def _set_query(self, query):
763  if isinstance(query, str) and os.path.isfile(query):
764  self._renderer_renderer = RawFontRenderer(query)
765  else:
766  self._renderer_renderer = FallbackFontRenderer(query)
767 
768  if self.emoji_svgemoji_svg:
769  self._renderer_renderer = EmojiRenderer(self._renderer_renderer, self.emoji_svgemoji_svg)
770 
771  @property
772  def query(self):
773  return self._renderer_renderer.get_query()
774 
775  @query.setter
776  def query(self, value):
777  if str(value) != str(self.queryqueryquery):
778  self._set_query_set_query(value)
779 
780  @property
781  def renderer(self):
782  return self._renderer_renderer
783 
784  def render(self, text, pos=NVector(0, 0)):
785  group = self._renderer_renderer.render(text, self.sizesize, self.positionposition+pos, self.use_kerninguse_kerning)
786  for subg in group.shapes[:-1]:
787  width = subg.next_x - self.positionposition.x - pos.x
788  if self.justifyjustify == TextJustify.Center:
789  subg.transform.position.value.x -= width / 2
790  elif self.justifyjustify == TextJustify.Right:
791  subg.transform.position.value.x -= width
792  return group
793 
794  def clone(self):
795  return FontStyle(str(self.queryqueryquery), self.sizesize, self.justifyjustify, NVector(*self.positionposition), self.use_kerninguse_kerning)
796 
797  @property
798  def ex(self):
799  return self._renderer_renderer.ex(self.sizesize)
800 
801  @property
802  def line_height(self):
803  return self._renderer_renderer.line_height(self.sizesize)
804 
805 
806 def _propfac(a):
807  return property(lambda s: s._get(a), lambda s, v: s._set(a, v))
808 
809 
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.stylestyle = query
824  else:
825  self.stylestyle = FontStyle(query, size, justify)
826  self.texttext = text
827  self.hiddenhidden = None
828 
829  def _get(self, a):
830  return getattr(self.stylestyle, a)
831 
832  def _set(self, a, v):
833  return setattr(self.stylestyle, 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.queryquery)
843 
844  @query_string.setter
845  def query_string(self, v):
846  self.queryquery = v
847 
848  def _build_wrapped(self):
849  g = self.stylestyle.render(self.texttext)
850  self.line_heightline_height = g.line_height
851  return g
852 
853  def bounding_box(self, time=0):
854  return self.wrappedwrapped.bounding_box(time)
Allows extending the Lottie shapes with custom Python classes.
Definition: base.py:327
Lottie <-> Python property mapper.
Definition: base.py:88
Single bezier curve.
Definition: bezier.py:123
ShapeElement that can contain other shapes.
Definition: shapes.py:433
Animatable Bezier curve.
Definition: shapes.py:399
def _point(self, pt)
Definition: font.py:26
def __init__(self, glyphSet, offset=NVector(0, 0))
Definition: font.py:20
def _get_svg_filename(self, char)
Definition: font.py:673
def __init__(self, wrapped, emoji_dir)
Definition: font.py:656
def emoji_filename(self, char)
Definition: font.py:670
def text_to_chars(self, string)
Definition: font.py:749
def _get_svg(self, char)
Definition: font.py:690
def emoji_basename(self, char)
Definition: font.py:667
def __init__(self, query, max_attempts=10)
Definition: font.py:587
def fallback_renderer(self, char)
Definition: font.py:615
def char(self, char)
Definition: font.py:157
def charset(self, *hex_ranges)
Definition: font.py:153
def family(self, name)
Definition: font.py:122
def style(self, *styles)
Definition: font.py:149
def __init__(self, str="")
Definition: font.py:108
def get(self, key, default=None)
Definition: font.py:173
def weight_to_css(self)
Definition: font.py:186
def custom(self, property, value)
Definition: font.py:160
def css_weight(self, weight)
Weight from CSS weight value.
Definition: font.py:130
def __contains__(self, item)
Definition: font.py:170
def __getitem__(self, key)
Definition: font.py:167
def weight(self, weight)
Definition: font.py:126
def glyph_name(self, ch)
Definition: font.py:459
def text_to_chars(self, text)
Definition: font.py:448
def kerning(self, c1, c2)
Definition: font.py:445
def render(self, text, size, pos=None, use_kerning=True, start_x=None)
Renders some text.
Definition: font.py:514
def glyph_beziers(self, glyph, offset=NVector(0, 0))
Definition: font.py:471
def ex(self, size)
Definition: font.py:468
def scale(self, size)
Definition: font.py:462
def glyph_shapes(self, glyph, offset=NVector(0, 0))
Definition: font.py:476
def _on_character(self, ch, size, pos, scale, line, use_kerning, chars, i)
Definition: font.py:483
def line_height(self, size)
Definition: font.py:465
def query_string(self)
Definition: font.py:841
def __init__(self, text="", query="", size=64, justify=TextJustify.Left)
Definition: font.py:820
def bounding_box(self, time=0)
Definition: font.py:853
def query(self, value)
Definition: font.py:776
def _set_query(self, query)
Definition: font.py:762
def __init__(self, query, size, justify=TextJustify.Left, position=None, use_kerning=True, emoji_svg=None)
Definition: font.py:754
def render(self, text, pos=NVector(0, 0))
Definition: font.py:784
def open(cls, filename)
Definition: font.py:360
def yMax(self)
Definition: font.py:402
def __contains__(self, key)
Definition: font.py:426
def getBestCmap(self)
Definition: font.py:372
def glyph(self, glyph_name)
Definition: font.py:408
def calculated_glyph_name(codepoint)
Definition: font.py:387
def __getitem__(self, key)
Definition: font.py:431
def __init__(self, wrapped)
Definition: font.py:350
def glyph_name(self, codepoint)
Definition: font.py:375
def scale(self)
Definition: font.py:396
def getGlyphSet(self)
Definition: font.py:369
def __init__(self, glyph, lsb, aw, xmin, xmax)
Definition: font.py:336
def draw(self, pen)
Definition: font.py:345
def kerning(self, c1, c2)
Definition: font.py:574
def __init__(self, filename)
Definition: font.py:565
def filename(self, styles)
Definition: font.py:83
def __getitem__(self, styles)
Definition: font.py:91
def add_file(self, styles, file)
Definition: font.py:78
def _key(self, styles)
Definition: font.py:86
def __init__(self, family)
Definition: font.py:72
def __contains__(self, item)
Definition: font.py:276
def best(self, query)
Returns the renderer best matching the name.
Definition: font.py:224
def _get(self, family)
Definition: font.py:256
def default(self)
Returns the default fornt renderer.
Definition: font.py:250
def __getitem__(self, key)
Definition: font.py:264
def all(self, query)
Yields all the renderers matching a query.
Definition: font.py:238
def _font_from_match(self, out)
Definition: font.py:232
def parse_svg_file(file, layer_frames=0, *args, **kwargs)
Definition: importer.py:1311
def collect_kerning_pairs(font)
Definition: font.py:285