[glyf] Add optimizeSize option

Set to True by default. Can be turned to False on the table,
or at Glyph() compile time.

Also fixes Glyph's draw() to expand the glyph first. Otherwise
it was failing.
This commit is contained in:
Behdad Esfahbod 2024-10-09 11:04:25 -06:00
parent afceebcda5
commit e8146a6d07
2 changed files with 102 additions and 6 deletions

View File

@ -713,7 +713,9 @@ class Glyph(object):
else:
self.decompileCoordinates(data)
def compile(self, glyfTable, recalcBBoxes=True, *, boundsDone=None):
def compile(
self, glyfTable, recalcBBoxes=True, *, boundsDone=None, optimizeSize=None
):
if hasattr(self, "data"):
if recalcBBoxes:
# must unpack glyph in order to recalculate bounding box
@ -730,7 +732,9 @@ class Glyph(object):
if self.isComposite():
data = data + self.compileComponents(glyfTable)
else:
data = data + self.compileCoordinates()
if optimizeSize is None:
optimizeSize = getattr(glyfTable, "optimizeSize", True)
data = data + self.compileCoordinates(optimizeSize=optimizeSize)
return data
def toXML(self, writer, ttFont):
@ -976,7 +980,7 @@ class Glyph(object):
data = data + struct.pack(">h", len(instructions)) + instructions
return data
def compileCoordinates(self):
def compileCoordinates(self, *, optimizeSize=True):
assert len(self.coordinates) == len(self.flags)
data = []
endPtsOfContours = array.array("H", self.endPtsOfContours)
@ -991,9 +995,12 @@ class Glyph(object):
deltas.toInt()
deltas.absoluteToRelative()
# TODO(behdad): Add a configuration option for this?
deltas = self.compileDeltasGreedy(self.flags, deltas)
# deltas = self.compileDeltasOptimal(self.flags, deltas)
if optimizeSize:
# TODO(behdad): Add a configuration option for this?
deltas = self.compileDeltasGreedy(self.flags, deltas)
# deltas = self.compileDeltasOptimal(self.flags, deltas)
else:
deltas = self.compileDeltasForSpeed(self.flags, deltas)
data.extend(deltas)
return b"".join(data)
@ -1110,6 +1117,63 @@ class Glyph(object):
return (compressedFlags, compressedXs, compressedYs)
def compileDeltasForSpeed(self, flags, deltas):
# uses widest representation needed, for all deltas.
compressedFlags = bytearray()
compressedXs = bytearray()
compressedYs = bytearray()
# Compute the necessary width for each axis
xs = [d[0] for d in deltas]
ys = [d[1] for d in deltas]
minX, minY, maxX, maxY = min(xs), min(ys), max(xs), max(ys)
xZero = minX == 0 and maxX == 0
yZero = minY == 0 and maxY == 0
xShort = -255 <= minX <= maxX <= 255
yShort = -255 <= minY <= maxY <= 255
lastflag = None
repeat = 0
for flag, (x, y) in zip(flags, deltas):
# Oh, the horrors of TrueType
# do x
if xZero:
flag = flag | flagXsame
elif xShort:
flag = flag | flagXShort
if x > 0:
flag = flag | flagXsame
else:
x = -x
compressedXs.append(x)
else:
compressedXs.extend(struct.pack(">h", x))
# do y
if yZero:
flag = flag | flagYsame
elif yShort:
flag = flag | flagYShort
if y > 0:
flag = flag | flagYsame
else:
y = -y
compressedYs.append(y)
else:
compressedYs.extend(struct.pack(">h", y))
# handle repeating flags
if flag == lastflag and repeat != 255:
repeat = repeat + 1
if repeat == 1:
compressedFlags.append(flag)
else:
compressedFlags[-2] = flag | flagRepeat
compressedFlags[-1] = repeat
else:
repeat = 0
compressedFlags.append(flag)
lastflag = flag
return (compressedFlags, compressedXs, compressedYs)
def recalcBounds(self, glyfTable, *, boundsDone=None):
"""Recalculates the bounds of the glyph.
@ -1404,6 +1468,7 @@ class Glyph(object):
pen.addComponent(glyphName, transform)
return
self.expand(glyfTable)
coordinates, endPts, flags = self.getCoordinates(glyfTable)
if offset:
coordinates = coordinates.copy()

View File

@ -707,6 +707,37 @@ class GlyphComponentTest:
'<component glyphName="a" firstPt="1" secondPt="2" flags="0x0"/>'
]
def test_compile_for_speed(self):
glyph = Glyph()
glyph.numberOfContours = 1
glyph.coordinates = GlyphCoordinates(
[(0, 0), (1, 0), (1, 0), (1, 1), (1, 1), (0, 1), (0, 1)]
)
glyph.flags = array.array("B", [flagOnCurve] + [flagCubic] * 6)
glyph.endPtsOfContours = [6]
glyph.program = ttProgram.Program()
glyph.expand(None)
sizeBytes = glyph.compile(None, optimizeSize=True)
glyph.expand(None)
speedBytes = glyph.compile(None, optimizeSize=False)
assert len(sizeBytes) < len(speedBytes)
for data in sizeBytes, speedBytes:
glyph = Glyph(data)
pen = RecordingPen()
glyph.draw(pen, None)
assert pen.value == [
("moveTo", ((0, 0),)),
("curveTo", ((1, 0), (1, 0), (1.0, 0.5))),
("curveTo", ((1, 1), (1, 1), (0.5, 1.0))),
("curveTo", ((0, 1), (0, 1), (0, 0))),
("closePath", ()),
]
def test_fromXML_reference_points(self):
comp = GlyphComponent()
for name, attrs, content in parseXML(