Merge pull request #3301 from fonttools/faster-glyf

Faster `glyf` compile
This commit is contained in:
Behdad Esfahbod 2023-10-16 12:25:35 -06:00 committed by GitHub
commit 807864872e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 130 additions and 22 deletions

View File

@ -210,6 +210,7 @@ ttLib.getTableClass("glyf").mergeMap = {
"tableTag": equal,
"glyphs": sumDicts,
"glyphOrder": sumLists,
"_reverseGlyphOrder": recalculate,
"axisTags": equal,
}

View File

@ -47,7 +47,7 @@ def updateBounds(bounds, p, min=min, max=max):
Args:
bounds: A bounding rectangle expressed as a tuple
``(xMin, yMin, xMax, yMax)``.
``(xMin, yMin, xMax, yMax), or None``.
p: A 2D tuple representing a point.
min,max: functions to compute the minimum and maximum.
@ -55,6 +55,8 @@ def updateBounds(bounds, p, min=min, max=max):
The updated bounding rectangle ``(xMin, yMin, xMax, yMax)``.
"""
(x, y) = p
if bounds is None:
return x, y, x, y
xMin, yMin, xMax, yMax = bounds
return min(xMin, x), min(yMin, y), max(xMax, x), max(yMax, y)

View File

@ -50,6 +50,9 @@ def main(args=None):
""",
)
parser.add_argument("font", metavar="font", nargs="*", help="Font file.")
parser.add_argument(
"-t", "--table", metavar="table", nargs="*", help="Tables to decompile."
)
parser.add_argument(
"-o", "--output", metavar="FILE", default=None, help="Output file."
)
@ -74,6 +77,7 @@ def main(args=None):
outFile = options.output
lazy = options.lazy
flavor = options.flavor
tables = options.table if options.table is not None else []
fonts = []
for f in options.font:
@ -84,6 +88,10 @@ def main(args=None):
collection = TTCollection(f, lazy=lazy)
fonts.extend(collection.fonts)
for font in fonts:
for table in tables if "*" not in tables else font.keys():
font[table] # Decompiles
if outFile is not None:
if len(fonts) == 1:
fonts[0].flavor = flavor

View File

@ -166,15 +166,15 @@ class TupleVariation(object):
return b"".join(tupleData), auxData
def compileCoord(self, axisTags):
result = bytearray()
result = []
axes = self.axes
for axis in axisTags:
triple = axes.get(axis)
if triple is None:
result.extend(b"\0\0")
result.append(b"\0\0")
else:
result.extend(struct.pack(">h", fl2fi(triple[1], 14)))
return bytes(result)
result.append(struct.pack(">h", fl2fi(triple[1], 14)))
return b"".join(result)
def compileIntermediateCoord(self, axisTags):
needed = False
@ -187,13 +187,13 @@ class TupleVariation(object):
break
if not needed:
return None
minCoords = bytearray()
maxCoords = bytearray()
minCoords = []
maxCoords = []
for axis in axisTags:
minValue, value, maxValue = self.axes.get(axis, (0.0, 0.0, 0.0))
minCoords.extend(struct.pack(">h", fl2fi(minValue, 14)))
maxCoords.extend(struct.pack(">h", fl2fi(maxValue, 14)))
return minCoords + maxCoords
minCoords.append(struct.pack(">h", fl2fi(minValue, 14)))
maxCoords.append(struct.pack(">h", fl2fi(maxValue, 14)))
return b"".join(minCoords + maxCoords)
@staticmethod
def decompileCoord_(axisTags, data, offset):
@ -802,7 +802,7 @@ def inferRegion_(peak):
intermediateEndTuple fields.
"""
start, end = {}, {}
for (axis, value) in peak.items():
for axis, value in peak.items():
start[axis] = min(value, 0.0) # -0.3 --> -0.3; 0.7 --> 0.0
end[axis] = max(value, 0.0) # -0.3 --> 0.0; 0.7 --> 0.7
return (start, end)

View File

@ -6,7 +6,7 @@ from fontTools import ttLib
from fontTools import version
from fontTools.misc.transform import DecomposedTransform
from fontTools.misc.textTools import tostr, safeEval, pad
from fontTools.misc.arrayTools import calcIntBounds, pointInRect
from fontTools.misc.arrayTools import updateBounds, pointInRect
from fontTools.misc.bezierTools import calcQuadraticBounds
from fontTools.misc.fixedTools import (
fixedToFloat as fi2fl,
@ -102,6 +102,7 @@ class table__g_l_y_f(DefaultTable.DefaultTable):
noname = 0
self.glyphs = {}
self.glyphOrder = glyphOrder = ttFont.getGlyphOrder()
self._reverseGlyphOrder = {}
for i in range(0, len(loca) - 1):
try:
glyphName = glyphOrder[i]
@ -144,9 +145,10 @@ class table__g_l_y_f(DefaultTable.DefaultTable):
currentLocation = 0
dataList = []
recalcBBoxes = ttFont.recalcBBoxes
boundsDone = set()
for glyphName in self.glyphOrder:
glyph = self.glyphs[glyphName]
glyphData = glyph.compile(self, recalcBBoxes)
glyphData = glyph.compile(self, recalcBBoxes, boundsDone=boundsDone)
if padding > 1:
glyphData = pad(glyphData, size=padding)
locations.append(currentLocation)
@ -281,6 +283,7 @@ class table__g_l_y_f(DefaultTable.DefaultTable):
glyphOrder ([str]): List of glyph names in order.
"""
self.glyphOrder = glyphOrder
self._reverseGlyphOrder = {}
def getGlyphName(self, glyphID):
"""Returns the name for the glyph with the given ID.
@ -289,13 +292,24 @@ class table__g_l_y_f(DefaultTable.DefaultTable):
"""
return self.glyphOrder[glyphID]
def _buildReverseGlyphOrderDict(self):
self._reverseGlyphOrder = d = {}
for glyphID, glyphName in enumerate(self.glyphOrder):
d[glyphName] = glyphID
def getGlyphID(self, glyphName):
"""Returns the ID of the glyph with the given name.
Raises a ``ValueError`` if the glyph is not found in the font.
"""
# XXX optimize with reverse dict!!!
return self.glyphOrder.index(glyphName)
glyphOrder = self.glyphOrder
id = getattr(self, "_reverseGlyphOrder", {}).get(glyphName)
if id is None or id >= len(glyphOrder) or glyphOrder[id] != glyphName:
self._buildReverseGlyphOrderDict()
id = self._reverseGlyphOrder.get(glyphName)
if id is None:
raise ValueError(glyphName)
return id
def removeHinting(self):
"""Removes TrueType hints from all glyphs in the glyphset.
@ -488,7 +502,7 @@ class table__g_l_y_f(DefaultTable.DefaultTable):
assert len(coord) == len(glyph.coordinates)
glyph.coordinates = GlyphCoordinates(coord)
glyph.recalcBounds(self)
glyph.recalcBounds(self, boundsDone=set())
horizontalAdvanceWidth = otRound(rightSideX - leftSideX)
if horizontalAdvanceWidth < 0:
@ -728,7 +742,7 @@ class Glyph(object):
else:
self.decompileCoordinates(data)
def compile(self, glyfTable, recalcBBoxes=True):
def compile(self, glyfTable, recalcBBoxes=True, *, boundsDone=None):
if hasattr(self, "data"):
if recalcBBoxes:
# must unpack glyph in order to recalculate bounding box
@ -737,8 +751,10 @@ class Glyph(object):
return self.data
if self.numberOfContours == 0:
return b""
if recalcBBoxes:
self.recalcBounds(glyfTable)
self.recalcBounds(glyfTable, boundsDone=boundsDone)
data = sstruct.pack(glyphHeaderFormat, self)
if self.isComposite():
data = data + self.compileComponents(glyfTable)
@ -1148,7 +1164,7 @@ class Glyph(object):
return (compressedFlags, compressedXs, compressedYs)
def recalcBounds(self, glyfTable):
def recalcBounds(self, glyfTable, *, boundsDone=None):
"""Recalculates the bounds of the glyph.
Each glyph object stores its bounding box in the
@ -1156,12 +1172,55 @@ class Glyph(object):
recomputed when the ``coordinates`` change. The ``table__g_l_y_f`` bounds
must be provided to resolve component bounds.
"""
if self.isComposite() and self.tryRecalcBoundsComposite(
glyfTable, boundsDone=boundsDone
):
return
try:
coords, endPts, flags = self.getCoordinates(glyfTable)
self.xMin, self.yMin, self.xMax, self.yMax = calcIntBounds(coords)
self.xMin, self.yMin, self.xMax, self.yMax = coords.calcIntBounds()
except NotImplementedError:
pass
def tryRecalcBoundsComposite(self, glyfTable, *, boundsDone=None):
"""Try recalculating the bounds of a composite glyph that has
certain constrained properties. Namely, none of the components
have a transform other than an integer translate, and none
uses the anchor points.
Each glyph object stores its bounding box in the
``xMin``/``yMin``/``xMax``/``yMax`` attributes. These bounds must be
recomputed when the ``coordinates`` change. The ``table__g_l_y_f`` bounds
must be provided to resolve component bounds.
Return True if bounds were calculated, False otherwise.
"""
for compo in self.components:
if hasattr(compo, "firstPt") or hasattr(compo, "transform"):
return False
if not float(compo.x).is_integer() or not float(compo.y).is_integer():
return False
# All components are untransformed and have an integer x/y translate
bounds = None
for compo in self.components:
glyphName = compo.glyphName
g = glyfTable[glyphName]
if boundsDone is None or glyphName not in boundsDone:
g.recalcBounds(glyfTable, boundsDone=boundsDone)
if boundsDone is not None:
boundsDone.add(glyphName)
x, y = compo.x, compo.y
bounds = updateBounds(bounds, (g.xMin + x, g.yMin + y))
bounds = updateBounds(bounds, (g.xMax + x, g.yMax + y))
if bounds is None:
bounds = (0, 0, 0, 0)
self.xMin, self.yMin, self.xMax, self.yMax = bounds
return True
def isComposite(self):
"""Test whether a glyph has components"""
if hasattr(self, "data"):
@ -2300,10 +2359,18 @@ class GlyphCoordinates(object):
def __getitem__(self, k):
"""Returns a two element tuple (x,y)"""
a = self._a
if isinstance(k, slice):
indices = range(*k.indices(len(self)))
return [self[i] for i in indices]
a = self._a
# Instead of calling ourselves recursively, duplicate code; faster
ret = []
for k in indices:
x = a[2 * k]
y = a[2 * k + 1]
ret.append(
(int(x) if x.is_integer() else x, int(y) if y.is_integer() else y)
)
return ret
x = a[2 * k]
y = a[2 * k + 1]
return (int(x) if x.is_integer() else x, int(y) if y.is_integer() else y)
@ -2341,6 +2408,17 @@ class GlyphCoordinates(object):
for i in range(len(a)):
a[i] = round(a[i])
def calcBounds(self):
a = self._a
if not a:
return 0, 0, 0, 0
xs = a[0::2]
ys = a[1::2]
return min(xs), min(ys), max(xs), max(ys)
def calcIntBounds(self, round=otRound):
return tuple(round(v) for v in self.calcBounds())
def relativeToAbsolute(self):
a = self._a
x, y = 0, 0

View File

@ -400,6 +400,25 @@ class GlyfTableTest(unittest.TestCase):
[(0, 0), (100, 0), (0, 0), (0, -1000)],
)
def test_getGlyphID(self):
# https://github.com/fonttools/fonttools/pull/3301#discussion_r1360405861
glyf = newTable("glyf")
glyf.setGlyphOrder([".notdef", "a", "b"])
glyf.glyphs = {}
for glyphName in glyf.glyphOrder:
glyf[glyphName] = Glyph()
assert glyf.getGlyphID("a") == 1
with pytest.raises(ValueError):
glyf.getGlyphID("c")
glyf["c"] = Glyph()
assert glyf.getGlyphID("c") == 3
del glyf["b"]
assert glyf.getGlyphID("c") == 2
class GlyphTest:
def test_getCoordinates(self):