Merge pull request #3301 from fonttools/faster-glyf
Faster `glyf` compile
This commit is contained in:
commit
807864872e
@ -210,6 +210,7 @@ ttLib.getTableClass("glyf").mergeMap = {
|
||||
"tableTag": equal,
|
||||
"glyphs": sumDicts,
|
||||
"glyphOrder": sumLists,
|
||||
"_reverseGlyphOrder": recalculate,
|
||||
"axisTags": equal,
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
Loading…
x
Reference in New Issue
Block a user