mirror of
https://github.com/OpenVGLab/OmniLottie.git
synced 2026-09-17 07:36:27 +00:00
Initial Commit
This commit is contained in:
@@ -0,0 +1,831 @@
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import fontTools.pens.basePen
|
||||
import fontTools.ttLib
|
||||
import fontTools.t1Lib
|
||||
from fontTools.pens.boundsPen import ControlBoundsPen
|
||||
import enum
|
||||
import math
|
||||
from xml.etree import ElementTree
|
||||
from ..nvector import NVector
|
||||
from ..objects.bezier import Bezier, BezierPoint
|
||||
from ..objects.shapes import Path, Group, Fill, Stroke
|
||||
from ..objects.text import TextJustify
|
||||
from ..objects.base import LottieProp, CustomObject
|
||||
from ..objects.layers import ShapeLayer
|
||||
|
||||
|
||||
class BezierPen(fontTools.pens.basePen.BasePen):
|
||||
def __init__(self, glyphSet, offset=NVector(0, 0)):
|
||||
super().__init__(glyphSet)
|
||||
self.beziers = []
|
||||
self.current = Bezier()
|
||||
self.offset = offset
|
||||
|
||||
def _point(self, pt):
|
||||
return self.offset + NVector(pt[0], -pt[1])
|
||||
|
||||
def _moveTo(self, pt):
|
||||
self._endPath()
|
||||
|
||||
def _endPath(self):
|
||||
if len(self.current.points):
|
||||
self.beziers.append(self.current)
|
||||
self.current = Bezier()
|
||||
|
||||
def _closePath(self):
|
||||
self.current.close()
|
||||
self._endPath()
|
||||
|
||||
def _lineTo(self, pt):
|
||||
if len(self.current.points) == 0:
|
||||
self.current.points.append(self._point(self._getCurrentPoint()))
|
||||
|
||||
self.current.points.append(self._point(pt))
|
||||
|
||||
def _curveToOne(self, pt1, pt2, pt3):
|
||||
if len(self.current.points) == 0:
|
||||
cp = self._point(self._getCurrentPoint())
|
||||
self.current.points.append(
|
||||
BezierPoint(
|
||||
cp,
|
||||
None,
|
||||
self._point(pt1) - cp
|
||||
)
|
||||
|
||||
)
|
||||
else:
|
||||
self.current.points[-1].out_tangent = self._point(pt1) - self.current.points[-1].vertex
|
||||
|
||||
dest = self._point(pt3)
|
||||
self.current.points.append(
|
||||
BezierPoint(
|
||||
dest,
|
||||
self._point(pt2) - dest,
|
||||
None,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class SystemFont:
|
||||
def __init__(self, family):
|
||||
self.family = family
|
||||
self.files = {}
|
||||
self.styles = set()
|
||||
self._renderers = {}
|
||||
|
||||
def add_file(self, styles, file):
|
||||
self.styles |= set(styles)
|
||||
key = self._key(styles)
|
||||
self.files.setdefault(key, file)
|
||||
|
||||
def filename(self, styles):
|
||||
return self.files[self._key(styles)]
|
||||
|
||||
def _key(self, styles):
|
||||
if isinstance(styles, str):
|
||||
return (styles,)
|
||||
return tuple(sorted(styles))
|
||||
|
||||
def __getitem__(self, styles):
|
||||
key = self._key(styles)
|
||||
if key in self._renderers:
|
||||
return self._renderers[key]
|
||||
fr = RawFontRenderer(self.files[key])
|
||||
self._renderers[key] = fr
|
||||
return fr
|
||||
|
||||
def __repr__(self):
|
||||
return "<SystemFont %s>" % self.family
|
||||
|
||||
|
||||
class FontQuery:
|
||||
"""!
|
||||
@see https://www.freedesktop.org/software/fontconfig/fontconfig-user.html#AEN21
|
||||
https://manpages.ubuntu.com/manpages/cosmic/man1/fc-pattern.1.html
|
||||
"""
|
||||
def __init__(self, str=""):
|
||||
self._query = {}
|
||||
if isinstance(str, FontQuery):
|
||||
self._query = str._query.copy()
|
||||
elif str:
|
||||
chunks = str.split(":")
|
||||
family = chunks.pop(0)
|
||||
self._query = dict(
|
||||
chunk.split("=")
|
||||
for chunk in chunks
|
||||
if chunk
|
||||
)
|
||||
self.family(family)
|
||||
|
||||
def family(self, name):
|
||||
self._query["family"] = name
|
||||
return self
|
||||
|
||||
def weight(self, weight):
|
||||
self._query["weight"] = weight
|
||||
return self
|
||||
|
||||
def css_weight(self, weight):
|
||||
"""!
|
||||
Weight from CSS weight value.
|
||||
|
||||
Weight is different between CSS and fontconfig
|
||||
This creates some interpolations to ensure known values are translated properly
|
||||
@see https://www.freedesktop.org/software/fontconfig/fontconfig-user.html#AEN178
|
||||
https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight#Common_weight_name_mapping
|
||||
"""
|
||||
if weight < 200:
|
||||
v = max(0, weight - 100) / 100 * 40
|
||||
elif weight < 500:
|
||||
v = -weight**3 / 200000 + weight**2 * 11/2000 - weight * 17/10 + 200
|
||||
elif weight < 700:
|
||||
v = -weight**2 * 3/1000 + weight * 41/10 - 1200
|
||||
else:
|
||||
v = (weight - 700) / 200 * 10 + 200
|
||||
return self.weight(int(round(v)))
|
||||
|
||||
def style(self, *styles):
|
||||
self._query["style"] = " ".join(styles)
|
||||
return self
|
||||
|
||||
def charset(self, *hex_ranges):
|
||||
self._query["charset"] = " ".join(hex_ranges)
|
||||
return self
|
||||
|
||||
def char(self, char):
|
||||
return self.charset("%x" % ord(char))
|
||||
|
||||
def custom(self, property, value):
|
||||
self._query[property] = value
|
||||
return self
|
||||
|
||||
def clone(self):
|
||||
return FontQuery(self)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._query.get(key, "")
|
||||
|
||||
def __contains__(self, item):
|
||||
return item in self._query
|
||||
|
||||
def get(self, key, default=None):
|
||||
return self._query.get(key, default)
|
||||
|
||||
def __str__(self):
|
||||
return self._query.get("family", "") + ":" + ":".join(
|
||||
"%s=%s" % (p, v)
|
||||
for p, v in self._query.items()
|
||||
if p != "family"
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return "<FontQuery %r>" % str(self)
|
||||
|
||||
def weight_to_css(self):
|
||||
x = int(self["weight"])
|
||||
if x < 40:
|
||||
v = x / 40 * 100 + 100
|
||||
elif x < 100:
|
||||
v = x**3/300 - x**2 * 11/15 + x*167/3 - 3200/3
|
||||
elif x < 200:
|
||||
v = (2050 - 10 * math.sqrt(5) * math.sqrt(1205 - 6 * x)) / 3
|
||||
else:
|
||||
v = (x - 200) * 200 / 10 + 700
|
||||
return int(round(v))
|
||||
|
||||
|
||||
class _SystemFontList:
|
||||
def __init__(self):
|
||||
self.fonts = None
|
||||
|
||||
def _lazy_load(self):
|
||||
if self.fonts is None:
|
||||
self.load()
|
||||
|
||||
def load(self):
|
||||
self.fonts = {}
|
||||
self.load_fc_list()
|
||||
|
||||
def cmd(self, *a):
|
||||
p = subprocess.Popen(a, stdout=subprocess.PIPE)
|
||||
out, err = p.communicate()
|
||||
out = out.decode("utf-8").strip()
|
||||
return out, p.returncode
|
||||
|
||||
def load_fc_list(self):
|
||||
out, returncode = self.cmd("fc-list", r'--format=%{file}\t%{family[0]}\t%{style[0]}\n')
|
||||
if returncode == 0:
|
||||
for line in out.splitlines():
|
||||
file, family, styles = line.split("\t")
|
||||
self._get(family).add_file(styles.split(" "), file)
|
||||
|
||||
def best(self, query):
|
||||
"""!
|
||||
Returns the renderer best matching the name
|
||||
"""
|
||||
out, returncode = self.cmd("fc-match", r"--format=%{family}\t%{style}", str(query))
|
||||
if returncode == 0:
|
||||
return self._font_from_match(out)
|
||||
|
||||
def _font_from_match(self, out):
|
||||
fam, style = out.split("\t")
|
||||
fam = fam.split(",")[0]
|
||||
style = style.split(",")[0].split()
|
||||
return self[fam][style]
|
||||
|
||||
def all(self, query):
|
||||
"""!
|
||||
Yields all the renderers matching a query
|
||||
"""
|
||||
out, returncode = self.cmd("fc-match", "-s", r"--format=%{family}\t%{style}\n", str(query))
|
||||
if returncode == 0:
|
||||
for line in out.splitlines():
|
||||
try:
|
||||
yield self._font_from_match(line)
|
||||
except (fontTools.ttLib.TTLibError, fontTools.t1Lib.T1Error):
|
||||
pass
|
||||
|
||||
def default(self):
|
||||
"""!
|
||||
Returns the default fornt renderer
|
||||
"""
|
||||
return self.best()
|
||||
|
||||
def _get(self, family):
|
||||
self._lazy_load()
|
||||
if family in self.fonts:
|
||||
return self.fonts[family]
|
||||
font = SystemFont(family)
|
||||
self.fonts[family] = font
|
||||
return font
|
||||
|
||||
def __getitem__(self, key):
|
||||
self._lazy_load()
|
||||
return self.fonts[key]
|
||||
|
||||
def __iter__(self):
|
||||
self._lazy_load()
|
||||
return iter(self.fonts.values())
|
||||
|
||||
def keys(self):
|
||||
self._lazy_load()
|
||||
return self.fonts.keys()
|
||||
|
||||
def __contains__(self, item):
|
||||
self._lazy_load()
|
||||
return item in self.fonts
|
||||
|
||||
|
||||
## Dictionary of system fonts
|
||||
fonts = _SystemFontList()
|
||||
|
||||
|
||||
def collect_kerning_pairs(font):
|
||||
if "GPOS" not in font:
|
||||
return {}
|
||||
|
||||
gpos_table = font["GPOS"].table
|
||||
|
||||
unique_kern_lookups = set()
|
||||
for item in gpos_table.FeatureList.FeatureRecord:
|
||||
if item.FeatureTag == "kern":
|
||||
feature = item.Feature
|
||||
unique_kern_lookups |= set(feature.LookupListIndex)
|
||||
|
||||
kerning_pairs = {}
|
||||
for kern_lookup_index in sorted(unique_kern_lookups):
|
||||
lookup = gpos_table.LookupList.Lookup[kern_lookup_index]
|
||||
if lookup.LookupType in {2, 9}:
|
||||
for pairPos in lookup.SubTable:
|
||||
if pairPos.LookupType == 9: # extension table
|
||||
if pairPos.ExtensionLookupType == 8: # contextual
|
||||
continue
|
||||
elif pairPos.ExtensionLookupType == 2:
|
||||
pairPos = pairPos.ExtSubTable
|
||||
|
||||
if pairPos.Format != 1:
|
||||
continue
|
||||
|
||||
firstGlyphsList = pairPos.Coverage.glyphs
|
||||
for ps_index, _ in enumerate(pairPos.PairSet):
|
||||
for pairValueRecordItem in pairPos.PairSet[ps_index].PairValueRecord:
|
||||
secondGlyph = pairValueRecordItem.SecondGlyph
|
||||
valueFormat = pairPos.ValueFormat1
|
||||
|
||||
if valueFormat == 5: # RTL kerning
|
||||
kernValue = "<%d 0 %d 0>" % (
|
||||
pairValueRecordItem.Value1.XPlacement,
|
||||
pairValueRecordItem.Value1.XAdvance)
|
||||
elif valueFormat == 0: # RTL pair with value <0 0 0 0>
|
||||
kernValue = "<0 0 0 0>"
|
||||
elif valueFormat == 4: # LTR kerning
|
||||
kernValue = pairValueRecordItem.Value1.XAdvance
|
||||
else:
|
||||
print(
|
||||
"\tValueFormat1 = %d" % valueFormat,
|
||||
file=sys.stdout)
|
||||
continue # skip the rest
|
||||
|
||||
kerning_pairs[(firstGlyphsList[ps_index], secondGlyph)] = kernValue
|
||||
return kerning_pairs
|
||||
|
||||
|
||||
class GlyphMetrics:
|
||||
def __init__(self, glyph, lsb, aw, xmin, xmax):
|
||||
self.glyph = glyph
|
||||
self.lsb = lsb
|
||||
self.advance = aw
|
||||
self.xmin = xmin
|
||||
self.xmax = xmax
|
||||
self.width = xmax - xmin
|
||||
self.advance = xmax
|
||||
|
||||
def draw(self, pen):
|
||||
return self.glyph.draw(pen)
|
||||
|
||||
|
||||
class Font:
|
||||
def __init__(self, wrapped):
|
||||
self.wrapped = wrapped
|
||||
if isinstance(self.wrapped, fontTools.ttLib.TTFont):
|
||||
self.cmap = self.wrapped.getBestCmap() or {}
|
||||
else:
|
||||
self.cmap = {}
|
||||
|
||||
self.glyphset = self.wrapped.getGlyphSet()
|
||||
|
||||
@classmethod
|
||||
def open(cls, filename):
|
||||
try:
|
||||
f = fontTools.ttLib.TTFont(filename)
|
||||
except fontTools.ttLib.TTLibError:
|
||||
f = fontTools.t1Lib.T1Font(filename)
|
||||
f.parse()
|
||||
|
||||
return cls(f)
|
||||
|
||||
def getGlyphSet(self):
|
||||
return self.wrapped.getGlyphSet()
|
||||
|
||||
def getBestCmap(self):
|
||||
return {}
|
||||
|
||||
def glyph_name(self, codepoint):
|
||||
if isinstance(codepoint, str):
|
||||
if len(codepoint) != 1:
|
||||
return ""
|
||||
codepoint = ord(codepoint)
|
||||
|
||||
if codepoint in self.cmap:
|
||||
return self.cmap[codepoint]
|
||||
|
||||
return self.calculated_glyph_name(codepoint)
|
||||
|
||||
@staticmethod
|
||||
def calculated_glyph_name(codepoint):
|
||||
from fontTools import agl # Adobe Glyph List
|
||||
if codepoint in agl.UV2AGL:
|
||||
return agl.UV2AGL[codepoint]
|
||||
elif codepoint <= 0xFFFF:
|
||||
return "uni%04X" % codepoint
|
||||
else:
|
||||
return "u%X" % codepoint
|
||||
|
||||
def scale(self):
|
||||
if isinstance(self.wrapped, fontTools.ttLib.TTFont):
|
||||
return 1 / self.wrapped["head"].unitsPerEm
|
||||
elif isinstance(self.wrapped, fontTools.t1Lib.T1Font):
|
||||
return self.wrapped["FontMatrix"][0]
|
||||
|
||||
def yMax(self):
|
||||
if isinstance(self.wrapped, fontTools.ttLib.TTFont):
|
||||
return self.wrapped["head"].yMax
|
||||
elif isinstance(self.wrapped, fontTools.t1Lib.T1Font):
|
||||
return self.wrapped["FontBBox"][3]
|
||||
|
||||
def glyph(self, glyph_name):
|
||||
if isinstance(self.wrapped, fontTools.ttLib.TTFont):
|
||||
glyph = self.glyphset[glyph_name]
|
||||
|
||||
xmin = getattr(glyph._glyph, "xMin", glyph.lsb)
|
||||
xmax = getattr(glyph._glyph, "xMax", glyph.width)
|
||||
return GlyphMetrics(glyph, glyph.lsb, glyph.width, xmin, xmax)
|
||||
elif isinstance(self.wrapped, fontTools.t1Lib.T1Font):
|
||||
glyph = self.glyphset[glyph_name]
|
||||
bounds_pen = ControlBoundsPen(self.glyphset)
|
||||
bounds = bounds_pen.bounds
|
||||
glyph.draw(bounds_pen)
|
||||
if not hasattr(glyph, "width"):
|
||||
advance = bounds[2]
|
||||
else:
|
||||
advance = glyph.width
|
||||
return GlyphMetrics(glyph, bounds[0], advance, bounds[0], bounds[2])
|
||||
|
||||
def __contains__(self, key):
|
||||
if isinstance(self.wrapped, fontTools.t1Lib.T1Font):
|
||||
return key in self.wrapped.font
|
||||
return key in self.wrapped
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.wrapped[key]
|
||||
|
||||
|
||||
class FontRenderer:
|
||||
tab_width = 4
|
||||
|
||||
@property
|
||||
def font(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_query(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def kerning(self, c1, c2):
|
||||
return 0
|
||||
|
||||
def text_to_chars(self, text):
|
||||
return text
|
||||
|
||||
def _on_missing(self, char, size, pos, group):
|
||||
"""!
|
||||
- Character as string
|
||||
- Font size
|
||||
- [in, out] Character position
|
||||
- Group shape
|
||||
"""
|
||||
|
||||
def glyph_name(self, ch):
|
||||
return self.font.glyph_name(ch)
|
||||
|
||||
def scale(self, size):
|
||||
return size * self.font.scale()
|
||||
|
||||
def line_height(self, size):
|
||||
return self.font.yMax() * self.scale(size)
|
||||
|
||||
def ex(self, size):
|
||||
return self.font.glyph("x").advance * self.scale(size)
|
||||
|
||||
def glyph_beziers(self, glyph, offset=NVector(0, 0)):
|
||||
pen = BezierPen(self.font.glyphset, offset)
|
||||
glyph.draw(pen)
|
||||
return pen.beziers
|
||||
|
||||
def glyph_shapes(self, glyph, offset=NVector(0, 0)):
|
||||
beziers = self.glyph_beziers(glyph, offset)
|
||||
return [
|
||||
Path(bez)
|
||||
for bez in beziers
|
||||
]
|
||||
|
||||
def _on_character(self, ch, size, pos, scale, line, use_kerning, chars, i):
|
||||
chname = self.glyph_name(ch)
|
||||
|
||||
if chname in self.font.glyphset:
|
||||
glyphdata = self.font.glyph(chname)
|
||||
#pos.x += glyphdata.lsb * scale
|
||||
glyph_shapes = self.glyph_shapes(glyphdata, pos / scale)
|
||||
|
||||
if glyph_shapes:
|
||||
if len(glyph_shapes) > 1:
|
||||
glyph_shape_group = line.add_shape(Group())
|
||||
glyph_shape = glyph_shape_group
|
||||
else:
|
||||
glyph_shape_group = line
|
||||
glyph_shape = glyph_shapes[0]
|
||||
|
||||
for sh in glyph_shapes:
|
||||
sh.shape.value.scale(scale)
|
||||
glyph_shape_group.add_shape(sh)
|
||||
|
||||
glyph_shape.name = ch
|
||||
|
||||
kerning = 0
|
||||
if use_kerning and i < len(chars) - 1:
|
||||
nextcname = chars[i+1]
|
||||
kerning = self.kerning(chname, nextcname)
|
||||
|
||||
pos.x += (glyphdata.advance + kerning) * scale
|
||||
return True
|
||||
return False
|
||||
|
||||
def render(self, text, size, pos=None, use_kerning=True):
|
||||
"""!
|
||||
Renders some text
|
||||
|
||||
@param text String to render
|
||||
@param size Font size (in pizels)
|
||||
@param[in,out] pos Text position
|
||||
@param use_kerning Whether to honour kerning info from the font file
|
||||
|
||||
@returns a Group shape, augmented with some extra attributes:
|
||||
- line_height Line height
|
||||
- next_x X position of the next character
|
||||
"""
|
||||
scale = self.scale(size)
|
||||
line_height = self.line_height(size)
|
||||
group = Group()
|
||||
group.name = text
|
||||
if pos is None:
|
||||
pos = NVector(0, 0)
|
||||
start_x = pos.x
|
||||
line = Group()
|
||||
group.add_shape(line)
|
||||
#group.transform.scale.value = NVector(100, 100) * scale
|
||||
|
||||
chars = self.text_to_chars(text)
|
||||
for i, ch in enumerate(chars):
|
||||
if ch == "\n":
|
||||
line.next_x = pos.x
|
||||
pos.x = start_x
|
||||
pos.y += line_height
|
||||
line = Group()
|
||||
group.add_shape(line)
|
||||
continue
|
||||
elif ch == "\t":
|
||||
chname = self.glyph_name(ch)
|
||||
if chname in self.font.glyphset:
|
||||
width = self.font.glyph(chname).advance
|
||||
else:
|
||||
width = self.ex(size)
|
||||
pos.x += width * scale * self.tab_width
|
||||
continue
|
||||
|
||||
self._on_character(ch, size, pos, scale, line, use_kerning, chars, i)
|
||||
|
||||
group.line_height = line_height
|
||||
group.next_x = line.next_x = pos.x
|
||||
return group
|
||||
|
||||
|
||||
class RawFontRenderer(FontRenderer):
|
||||
def __init__(self, filename):
|
||||
self.filename = filename
|
||||
self._font = Font.open(filename)
|
||||
self._kerning = None
|
||||
|
||||
@property
|
||||
def font(self):
|
||||
return self._font
|
||||
|
||||
def kerning(self, c1, c2):
|
||||
if self._kerning is None:
|
||||
self._kerning = collect_kerning_pairs(self.font)
|
||||
return self._kerning.get((c1, c2), 0)
|
||||
|
||||
def __repr__(self):
|
||||
return "<FontRenderer %r>" % self.filename
|
||||
|
||||
def get_query(self):
|
||||
return self.filename
|
||||
|
||||
|
||||
class FallbackFontRenderer(FontRenderer):
|
||||
def __init__(self, query, max_attempts=10):
|
||||
self.query = FontQuery(query)
|
||||
self._best = None
|
||||
self._bq = None
|
||||
self._fallback = {}
|
||||
self.max_attempts = max_attempts
|
||||
|
||||
@property
|
||||
def font(self):
|
||||
return self.best.font
|
||||
|
||||
def get_query(self):
|
||||
return self.query
|
||||
|
||||
def ex(self, size):
|
||||
best = self.best
|
||||
if "x" not in self.font.glyphset:
|
||||
best = fonts.best(self.query.clone().char("x"))
|
||||
return best.ex(size)
|
||||
|
||||
@property
|
||||
def best(self):
|
||||
cq = str(self.query)
|
||||
if self._best is None or self._bq != cq:
|
||||
self._best = fonts.best(self.query)
|
||||
self._bq = cq
|
||||
return self._best
|
||||
|
||||
def fallback_renderer(self, char):
|
||||
if char in self._fallback:
|
||||
return self._fallback[char]
|
||||
|
||||
if len(char) != 1:
|
||||
return None
|
||||
|
||||
codepoint = ord(char)
|
||||
name = Font.calculated_glyph_name(codepoint)
|
||||
for i, font in enumerate(fonts.all(self.query.clone().char(char))):
|
||||
# For some reason fontconfig sometimes returns a font that doesn't
|
||||
# actually contain the glyph
|
||||
if name in font.font.glyphset or codepoint in font.cmap:
|
||||
self._fallback[char] = font
|
||||
return font
|
||||
|
||||
if i > self.max_attempts:
|
||||
self._fallback[char] = None
|
||||
return None
|
||||
|
||||
def _on_character(self, char, size, pos, scale, group, use_kerning, chars, i):
|
||||
if self.best._on_character(char, size, pos, scale, group, use_kerning, chars, i):
|
||||
return True
|
||||
|
||||
font = self.fallback_renderer(char)
|
||||
if not font:
|
||||
return False
|
||||
|
||||
child = font.render(char, size, pos)
|
||||
if len(child.shapes) == 2:
|
||||
group.add_shape(child.shapes[0])
|
||||
else:
|
||||
group.add_shape(child)
|
||||
|
||||
def __repr__(self):
|
||||
return "<FallbackFontRenderer %s>" % self.query
|
||||
|
||||
|
||||
class EmojiRenderer(FontRenderer):
|
||||
_split = None
|
||||
|
||||
def __init__(self, wrapped, emoji_dir):
|
||||
if not os.path.isdir(emoji_dir):
|
||||
raise Exception("Not a valid directory: %s" % emoji_dir)
|
||||
self.wrapped = wrapped
|
||||
self.emoji_dir = emoji_dir
|
||||
self._svgs = {}
|
||||
|
||||
@property
|
||||
def font(self):
|
||||
return self.wrapped.font
|
||||
|
||||
def _get_svg(self, char):
|
||||
from ..parsers.svg import parse_svg_file
|
||||
|
||||
if char in self._svgs:
|
||||
return self._svgs[char]
|
||||
|
||||
basename = "-".join("%x" % ord(cp) for cp in char) + ".svg"
|
||||
filename = os.path.join(self.emoji_dir, basename)
|
||||
if not os.path.isfile(filename):
|
||||
self._svgs[char] = None
|
||||
return None
|
||||
|
||||
svga = parse_svg_file(filename)
|
||||
svgshape = Group()
|
||||
svgshape.name = basename
|
||||
for layer in svga.layers:
|
||||
if isinstance(layer, ShapeLayer):
|
||||
for shape in layer.shapes:
|
||||
svgshape.add_shape(shape)
|
||||
|
||||
self._svgs[char] = svgshape
|
||||
svgshape._bbox = svgshape.bounding_box()
|
||||
return svgshape
|
||||
|
||||
def _on_character(self, char, size, pos, scale, group, use_kerning, chars, i):
|
||||
svgshape = self._get_svg(char)
|
||||
if svgshape:
|
||||
target_height = self.line_height(size)
|
||||
scale = target_height / svgshape._bbox.height
|
||||
shape_group = Group()
|
||||
shape_group = svgshape.clone()
|
||||
shape_group.transform.scale.value *= scale
|
||||
offset = NVector(
|
||||
-svgshape._bbox.x1 + svgshape._bbox.width * 0.075,
|
||||
-svgshape._bbox.y2 + svgshape._bbox.height * 0.1
|
||||
)
|
||||
shape_group.transform.position.value = pos + offset * scale
|
||||
group.add_shape(shape_group)
|
||||
pos.x += svgshape._bbox.width * scale
|
||||
return True
|
||||
return self.wrapped._on_character(char, size, pos, scale, group, use_kerning, chars, i)
|
||||
|
||||
def get_query(self):
|
||||
return self.wrapped.get_query()
|
||||
|
||||
@staticmethod
|
||||
def _get_splitter():
|
||||
if EmojiRenderer._split is None:
|
||||
try:
|
||||
import grapheme
|
||||
EmojiRenderer._split = grapheme.graphemes
|
||||
except ImportError:
|
||||
sys.stderr.write("Install `grapheme` for better Emoji support\n")
|
||||
EmojiRenderer._split = lambda x: x
|
||||
return EmojiRenderer._split
|
||||
|
||||
@staticmethod
|
||||
def emoji_split(string):
|
||||
return EmojiRenderer._get_splitter()(string)
|
||||
|
||||
def text_to_chars(self, string):
|
||||
return list(self.emoji_split(string))
|
||||
|
||||
|
||||
class FontStyle:
|
||||
def __init__(self, query, size, justify=TextJustify.Left, position=None, use_kerning=True, emoji_svg=None):
|
||||
self.emoji_svg = emoji_svg
|
||||
self._set_query(query)
|
||||
self.size = size
|
||||
self.justify = justify
|
||||
self.position = position.clone() if position else NVector(0, 0)
|
||||
self.use_kerning = use_kerning
|
||||
|
||||
def _set_query(self, query):
|
||||
if isinstance(query, str) and os.path.isfile(query):
|
||||
self._renderer = RawFontRenderer(query)
|
||||
else:
|
||||
self._renderer = FallbackFontRenderer(query)
|
||||
|
||||
if self.emoji_svg:
|
||||
self._renderer = EmojiRenderer(self._renderer, self.emoji_svg)
|
||||
|
||||
@property
|
||||
def query(self):
|
||||
return self._renderer.get_query()
|
||||
|
||||
@query.setter
|
||||
def query(self, value):
|
||||
if str(value) != str(self.query):
|
||||
self._set_query(value)
|
||||
|
||||
@property
|
||||
def renderer(self):
|
||||
return self._renderer
|
||||
|
||||
def render(self, text, pos=NVector(0, 0)):
|
||||
group = self._renderer.render(text, self.size, self.position+pos, self.use_kerning)
|
||||
for subg in group.shapes[:-1]:
|
||||
width = subg.next_x - self.position.x - pos.x
|
||||
if self.justify == TextJustify.Center:
|
||||
subg.transform.position.value.x -= width / 2
|
||||
elif self.justify == TextJustify.Right:
|
||||
subg.transform.position.value.x -= width
|
||||
return group
|
||||
|
||||
def clone(self):
|
||||
return FontStyle(str(self._renderer.query), self.size, self.justify, NVector(*self.position), self.use_kerning)
|
||||
|
||||
@property
|
||||
def ex(self):
|
||||
return self._renderer.ex(self.size)
|
||||
|
||||
@property
|
||||
def line_height(self):
|
||||
return self._renderer.line_height(self.size)
|
||||
|
||||
|
||||
def _propfac(a):
|
||||
return property(lambda s: s._get(a), lambda s, v: s._set(a, v))
|
||||
|
||||
|
||||
class FontShape(CustomObject):
|
||||
_props = [
|
||||
LottieProp("query_string", "_query", str),
|
||||
LottieProp("size", "_size", float),
|
||||
LottieProp("justify", "_justify", TextJustify),
|
||||
LottieProp("text", "_text", str),
|
||||
LottieProp("position", "_position", NVector),
|
||||
]
|
||||
wrapped_lottie = Group
|
||||
|
||||
def __init__(self, text="", query="", size=64, justify=TextJustify.Left):
|
||||
CustomObject.__init__(self)
|
||||
if isinstance(query, FontStyle):
|
||||
self.style = query
|
||||
else:
|
||||
self.style = FontStyle(query, size, justify)
|
||||
self.text = text
|
||||
self.hidden = None
|
||||
|
||||
def _get(self, a):
|
||||
return getattr(self.style, a)
|
||||
|
||||
def _set(self, a, v):
|
||||
return setattr(self.style, a, v)
|
||||
|
||||
query = _propfac("query")
|
||||
size = _propfac("size")
|
||||
justify = _propfac("justify")
|
||||
position = _propfac("position")
|
||||
|
||||
@property
|
||||
def query_string(self):
|
||||
return str(self.query)
|
||||
|
||||
@query_string.setter
|
||||
def query_string(self, v):
|
||||
self.query = v
|
||||
|
||||
def _build_wrapped(self):
|
||||
g = self.style.render(self.text)
|
||||
self.line_height = g.line_height
|
||||
return g
|
||||
|
||||
def bounding_box(self, time=0):
|
||||
return self.wrapped.bounding_box(time)
|
||||
Reference in New Issue
Block a user