7 from fontTools.pens.boundsPen
import ControlBoundsPen
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
20 def __init__(self, glyphSet, offset=NVector(0, 0)):
29 def _moveTo(self, pt):
33 if len(self.
currentcurrent.points):
41 def _lineTo(self, pt):
42 if len(self.
currentcurrent.points) == 0:
43 self.
currentcurrent.points.append(self.
_point_point(self._getCurrentPoint()))
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(
54 self.
_point_point(pt1) - cp
59 self.
currentcurrent.points[-1].out_tangent = self.
_point_point(pt1) - self.
currentcurrent.points[-1].vertex
61 dest = self.
_point_point(pt3)
62 self.
currentcurrent.points.append(
65 self.
_point_point(pt2) - dest,
79 self.
stylesstyles |= set(styles)
80 key = self.
_key_key(styles)
81 self.
filesfiles.setdefault(key, file)
84 return self.
filesfiles[self.
_key_key(styles)]
86 def _key(self, styles):
87 if isinstance(styles, str):
89 return tuple(sorted(styles))
92 key = self.
_key_key(styles)
100 return "<SystemFont %s>" % self.
familyfamily
105 @see https://www.freedesktop.org/software/fontconfig/fontconfig-user.html#AEN21
106 https://manpages.ubuntu.com/manpages/cosmic/man1/fc-pattern.1.html
110 if isinstance(str, FontQuery):
111 self.
_query_query = str._query.copy()
113 chunks = str.split(
":")
114 family = chunks.pop(0)
123 self.
_query_query[
"family"] = name
127 self.
_query_query[
"weight"] = weight
132 Weight from CSS weight value.
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
140 v = max(0, weight - 100) / 100 * 40
142 v = -weight**3 / 200000 + weight**2 * 11/2000 - weight * 17/10 + 200
144 v = -weight**2 * 3/1000 + weight * 41/10 - 1200
146 v = (weight - 700) / 200 * 10 + 200
150 self.
_query_query[
"style"] =
" ".join(styles)
154 self.
_query_query[
"charset"] =
" ".join(hex_ranges)
158 return self.
charsetcharset(
"%x" % ord(char))
161 self.
_query_query[property] = value
171 return item
in self.
_query_query
173 def get(self, key, default=None):
174 return self.
_query_query.
get(key, default)
177 return self.
_query_query.
get(
"family",
"") +
":" +
":".join(
179 for p, v
in self.
_query_query.items()
184 return "<FontQuery %r>" %
str(self)
187 x =
int(self[
"weight"])
189 v = x / 40 * 100 + 100
191 v = x**3/300 - x**2 * 11/15 + x*167/3 - 3200/3
193 v = (2050 - 10 * math.sqrt(5) * math.sqrt(1205 - 6 * x)) / 3
195 v = (x - 200) * 200 / 10 + 700
203 def _lazy_load(self):
204 if self.
fontsfonts
is None:
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
218 out, returncode = self.
cmdcmd(
"fc-list",
r'--format=%{file}\t%{family[0]}\t%{style[0]}\n')
220 for line
in out.splitlines():
221 file, family, styles = line.split(
"\t")
222 self.
_get_get(family).add_file(styles.split(
" "), file)
226 Returns the renderer best matching the name
228 out, returncode = self.
cmdcmd(
"fc-match",
r"--format=%{family}\t%{style}",
str(query))
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]
240 Yields all the renderers matching a query
242 out, returncode = self.
cmdcmd(
"fc-match",
"-s",
r"--format=%{family}\t%{style}\n",
str(query))
244 for line
in out.splitlines():
247 except (fontTools.ttLib.TTLibError, fontTools.t1Lib.T1Error):
252 Returns the default fornt renderer
254 return self.
bestbest()
256 def _get(self, family):
258 if family
in self.
fontsfonts:
259 return self.
fontsfonts[family]
261 self.
fontsfonts[family] = font
266 return self.
fontsfonts[key]
270 return iter(self.
fontsfonts.values())
278 return item
in self.
fontsfonts
286 if "GPOS" not in font:
289 gpos_table = font[
"GPOS"].table
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)
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:
303 if pairPos.ExtensionLookupType == 8:
305 elif pairPos.ExtensionLookupType == 2:
306 pairPos = pairPos.ExtSubTable
308 if pairPos.Format != 1:
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
318 kernValue =
"<%d 0 %d 0>" % (
319 pairValueRecordItem.Value1.XPlacement,
320 pairValueRecordItem.Value1.XAdvance)
321 elif valueFormat == 0:
322 kernValue =
"<0 0 0 0>"
323 elif valueFormat == 4:
324 kernValue = pairValueRecordItem.Value1.XAdvance
327 "\tValueFormat1 = %d" % valueFormat,
331 kerning_pairs[(firstGlyphsList[ps_index], secondGlyph)] = kernValue
352 if isinstance(self.
wrappedwrapped, fontTools.ttLib.TTFont):
362 f = fontTools.ttLib.TTFont(filename)
363 except fontTools.ttLib.TTLibError:
364 f = fontTools.t1Lib.T1Font(filename)
376 if isinstance(codepoint, str):
377 if len(codepoint) != 1:
379 codepoint = ord(codepoint)
381 if codepoint
in self.
cmapcmap:
382 return self.
cmapcmap[codepoint]
388 from fontTools
import agl
389 if codepoint
in agl.UV2AGL:
390 return agl.UV2AGL[codepoint]
391 elif codepoint <= 0xFFFF:
392 return "uni%04X" % codepoint
394 return "u%X" % codepoint
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]
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]
409 if isinstance(self.
wrappedwrapped, fontTools.ttLib.TTFont):
410 glyph = self.
glyphsetglyphset[glyph_name]
411 table = self.
glyphsetglyphset.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.
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"):
423 advance = glyph.width
424 return GlyphMetrics(glyph, bounds[0], advance, bounds[0], bounds[2])
427 if isinstance(self.
wrappedwrapped, fontTools.t1Lib.T1Font):
428 return key
in self.
wrappedwrapped.font
429 return key
in self.
wrappedwrapped
432 return self.
wrappedwrapped[key]
440 raise NotImplementedError
443 raise NotImplementedError
451 def _on_missing(self, char, size, pos, group):
453 - Character as string
455 - [in, out] Character position
466 return self.
fontfont.yMax() * self.
scalescale(size)
469 return self.
fontfont.glyph(
"x").advance * self.
scalescale(size)
483 def _on_character(self, ch, size, pos, scale, line, use_kerning, chars, i):
486 if chname
in self.
fontfont.glyphset:
487 glyphdata = self.
fontfont.glyph(chname)
489 glyph_shapes = self.
glyph_shapesglyph_shapes(glyphdata, pos / scale)
492 if len(glyph_shapes) > 1:
493 glyph_shape_group = line.add_shape(
Group())
494 glyph_shape = glyph_shape_group
496 glyph_shape_group = line
497 glyph_shape = glyph_shapes[0]
499 for sh
in glyph_shapes:
500 sh.shape.value.scale(scale)
501 glyph_shape_group.add_shape(sh)
503 glyph_shape.name = ch
506 if use_kerning
and i < len(chars) - 1:
507 nextcname = chars[i+1]
508 kerning = self.
kerningkerning(chname, nextcname)
510 pos.x += (glyphdata.advance + kerning) * scale
514 def render(self, text, size, pos=None, use_kerning=True, start_x=None):
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
524 @returns a Group shape, augmented with some extra attributes:
525 - line_height Line height
526 - next_x X position of the next character
528 scale = self.
scalescale(size)
534 start_x = pos.x
if start_x
is None else start_x
536 group.add_shape(line)
540 for i, ch
in enumerate(chars):
546 group.add_shape(line)
550 if chname
in self.
fontfont.glyphset:
551 width = self.
fontfont.glyph(chname).advance
553 width = self.
exex(size)
554 pos.x += width * scale * self.
tab_widthtab_width
557 self.
_on_character_on_character(ch, size, pos, scale, line, use_kerning, chars, i)
559 group.line_height = line_height
560 group.next_x = line.next_x = pos.x
567 self.
_font_font = Font.open(filename)
572 return self.
_font_font
577 return self.
_kerning_kerning.get((c1, c2), 0)
580 return "<FontRenderer %r>" % self.
filenamefilename
589 self.
_best_best =
None
596 return self.
bestbest.font
599 return self.
queryquery
603 if "x" not in self.
fontfontfont.glyphset:
604 best = fonts.best(self.
queryquery.clone().char(
"x"))
610 if self.
_best_best
is None or self.
_bq_bq != cq:
613 return self.
_best_best
622 codepoint = ord(char)
623 name = Font.calculated_glyph_name(codepoint)
624 for i, font
in enumerate(fonts.all(self.
queryquery.clone().char(char))):
627 if name
in font.font.glyphset
or codepoint
in font.font.cmap:
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):
643 child = font.render(char, size, pos)
644 if len(child.shapes) == 2:
645 group.add_shape(child.shapes[0])
647 group.add_shape(child)
650 return "<FallbackFontRenderer %s>" % self.
queryquery
657 if not os.path.isdir(emoji_dir):
658 raise Exception(
"Not a valid directory: %s" % emoji_dir)
665 return self.
wrappedwrapped.font
668 return "-".join(
"%x" % ord(cp)
for cp
in char)
673 def _get_svg_filename(self, char):
677 filename = os.path.join(self.
emoji_diremoji_dir, basename + suffix)
678 if os.path.isfile(filename):
679 return basename, filename
681 filename = os.path.join(self.
emoji_diremoji_dir, basename.upper() + suffix)
682 if os.path.isfile(filename):
683 return basename, filename
685 if char
and char[-1] ==
'\ufe0f':
690 def _get_svg(self, char):
691 from ..parsers.svg
import parse_svg_file
693 if char
in self.
_svgs_svgs:
694 return self.
_svgs_svgs[char]
698 self.
_svgs_svgs[char] =
None
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)
709 self.
_svgs_svgs[char] = svgshape
710 svgshape._bbox = svgshape.bounding_box()
713 def _on_character(self, char, size, pos, scale, group, use_kerning, chars, i):
714 svgshape = self.
_get_svg_get_svg(char)
717 scale = target_height / svgshape._bbox.height
718 shape_group =
Group()
719 shape_group = svgshape.clone()
720 shape_group.transform.scale.value *= scale
722 -svgshape._bbox.x1 + svgshape._bbox.width * 0.075,
723 -svgshape._bbox.y2 + svgshape._bbox.height * 0.1
725 shape_group.transform.position.value = pos + offset * scale
726 group.add_shape(shape_group)
727 pos.x += svgshape._bbox.width * scale
729 return self.
wrappedwrapped._on_character(char, size, pos, scale, group, use_kerning, chars, i)
736 if EmojiRenderer._split
is None:
739 EmojiRenderer._split = grapheme.graphemes
741 sys.stderr.write(
"Install `grapheme` for better Emoji support\n")
742 EmojiRenderer._split =
lambda x: x
743 return EmojiRenderer._split
747 return EmojiRenderer._get_splitter()(string)
754 def __init__(self, query, size, justify=TextJustify.Left, position=None, use_kerning=True, emoji_svg=None):
762 def _set_query(self, query):
763 if isinstance(query, str)
and os.path.isfile(query):
773 return self.
_renderer_renderer.get_query()
784 def render(self, text, pos=NVector(0, 0)):
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
807 return property(
lambda s: s._get(a),
lambda s, v: s._set(a, v))
814 LottieProp(
"justify",
"_justify", TextJustify),
818 wrapped_lottie = Group
820 def __init__(self, text="", query="", size=64, justify=TextJustify.Left):
821 CustomObject.__init__(self)
822 if isinstance(query, FontStyle):
830 return getattr(self.
stylestyle, a)
832 def _set(self, a, v):
833 return setattr(self.
stylestyle, a, v)
835 query = _propfac(
"query")
836 size = _propfac(
"size")
837 justify = _propfac(
"justify")
838 position = _propfac(
"position")
848 def _build_wrapped(self):
849 g = self.
stylestyle.render(self.
texttext)
Allows extending the Lottie shapes with custom Python classes.
Lottie <-> Python property mapper.
ShapeElement that can contain other shapes.
def __init__(self, glyphSet, offset=NVector(0, 0))
def _get_svg_filename(self, char)
def __init__(self, wrapped, emoji_dir)
def emoji_filename(self, char)
def text_to_chars(self, string)
def emoji_basename(self, char)
def __init__(self, query, max_attempts=10)
def fallback_renderer(self, char)
def charset(self, *hex_ranges)
def __init__(self, str="")
def get(self, key, default=None)
def custom(self, property, value)
def css_weight(self, weight)
Weight from CSS weight value.
def __contains__(self, item)
def __getitem__(self, key)
def text_to_chars(self, text)
def kerning(self, c1, c2)
def render(self, text, size, pos=None, use_kerning=True, start_x=None)
Renders some text.
def glyph_beziers(self, glyph, offset=NVector(0, 0))
def glyph_shapes(self, glyph, offset=NVector(0, 0))
def _on_character(self, ch, size, pos, scale, line, use_kerning, chars, i)
def line_height(self, size)
def __init__(self, text="", query="", size=64, justify=TextJustify.Left)
def bounding_box(self, time=0)
def _set_query(self, query)
def __init__(self, query, size, justify=TextJustify.Left, position=None, use_kerning=True, emoji_svg=None)
def render(self, text, pos=NVector(0, 0))
def __contains__(self, key)
def glyph(self, glyph_name)
def calculated_glyph_name(codepoint)
def __getitem__(self, key)
def __init__(self, wrapped)
def glyph_name(self, codepoint)
def __init__(self, glyph, lsb, aw, xmin, xmax)
def kerning(self, c1, c2)
def __init__(self, filename)
def filename(self, styles)
def __getitem__(self, styles)
def add_file(self, styles, file)
def __init__(self, family)
def __contains__(self, item)
def best(self, query)
Returns the renderer best matching the name.
def default(self)
Returns the default fornt renderer.
def __getitem__(self, key)
def all(self, query)
Yields all the renderers matching a query.
def _font_from_match(self, out)
def parse_svg_file(file, layer_frames=0, *args, **kwargs)
def collect_kerning_pairs(font)