Move LerpGlyphSet and lerp_recordings to more prominent places

Fixes https://github.com/fonttools/fonttools/issues/3361
This commit is contained in:
Behdad Esfahbod 2023-12-06 08:34:51 -07:00
parent dcf3f0c4b1
commit 4f6c739181
6 changed files with 151 additions and 53 deletions

View File

@ -172,6 +172,36 @@ class RecordingPointPen(AbstractPointPen):
drawPoints = replay drawPoints = replay
def lerp_recordings(recording1, recording2, factor=0.5):
"""Linearly interpolate between two recordings. The recordings
must be decomposed, i.e. they must not contain any components.
Factor is typically between 0 and 1. 0 means the first recording,
1 means the second recording, and 0.5 means the average of the
two recordings. Other values are possible, and can be useful to
extrapolate. Defaults to 0.5.
Returns the new recording.
"""
value = []
if len(recording1) != len(recording2):
raise ValueError(
"Mismatched lengths: %d and %d" % (len(recording1), len(recording2))
)
for (op1, args1), (op2, args2) in zip(recording1, recording2):
if op1 != op2:
raise ValueError("Mismatched operations: %s, %s" % (op1, op2))
if op1 == "addComponent":
raise ValueError("Cannot interpolate components")
else:
mid_args = [
(x1 + (x2 - x1) * factor, y1 + (y2 - y1) * factor)
for (x1, y1), (x2, y2) in zip(args1, args2)
]
value.append((op1, mid_args))
return value
if __name__ == "__main__": if __name__ == "__main__":
pen = RecordingPen() pen = RecordingPen()
pen.moveTo((0, 0)) pen.moveTo((0, 0))

View File

@ -9,6 +9,7 @@ from fontTools.misc.fixedTools import otRound
from fontTools.misc.loggingTools import deprecateFunction from fontTools.misc.loggingTools import deprecateFunction
from fontTools.misc.transform import Transform from fontTools.misc.transform import Transform
from fontTools.pens.transformPen import TransformPen, TransformPointPen from fontTools.pens.transformPen import TransformPen, TransformPointPen
from fontTools.pens.recordingPen import DecomposingRecordingPen
class _TTGlyphSet(Mapping): class _TTGlyphSet(Mapping):
@ -321,3 +322,64 @@ def _setCoordinates(glyph, coord, glyfTable, *, recalcBounds=True):
verticalAdvanceWidth, verticalAdvanceWidth,
topSideBearing, topSideBearing,
) )
class LerpGlyphSet(Mapping):
"""A glyphset that interpolates between two other glyphsets.
Factor is typically between 0 and 1. 0 means the first glyphset,
1 means the second glyphset, and 0.5 means the average of the
two glyphsets. Other values are possible, and can be useful to
extrapolate. Defaults to 0.5.
"""
def __init__(self, glyphset1, glyphset2, factor=0.5):
self.glyphset1 = glyphset1
self.glyphset2 = glyphset2
self.factor = factor
def __getitem__(self, glyphname):
if glyphname in self.glyphset1 and glyphname in self.glyphset2:
return LerpGlyph(glyphname, self)
raise KeyError(glyphname)
def __contains__(self, glyphname):
return glyphname in self.glyphset1 and glyphname in self.glyphset2
def __iter__(self):
set1 = set(self.glyphset1)
set2 = set(self.glyphset2)
return iter(set1.intersection(set2))
def __len__(self):
set1 = set(self.glyphset1)
set2 = set(self.glyphset2)
return len(set1.intersection(set2))
class LerpGlyph:
def __init__(self, glyphname, glyphset):
self.glyphset = glyphset
self.glyphname = glyphname
def draw(self, pen):
recording1 = DecomposingRecordingPen(self.glyphset.glyphset1)
self.glyphset.glyphset1[self.glyphname].draw(recording1)
recording2 = DecomposingRecordingPen(self.glyphset.glyphset2)
self.glyphset.glyphset2[self.glyphname].draw(recording2)
factor = self.glyphset.factor
if len(recording1.value) != len(recording2.value):
raise ValueError(
"Mismatching number of operations: %d, %d"
% (len(recording1.value), len(recording2.value))
)
for (op1, args1), (op2, args2) in zip(recording1.value, recording2.value):
if op1 != op2:
raise ValueError("Mismatching operations: %s, %s" % (op1, op2))
mid_args = [
(x1 + (x2 - x1) * factor, y1 + (y2 - y1) * factor)
for (x1, y1), (x2, y2) in zip(args1, args2)
]
getattr(pen, op1)(*mid_args)

View File

@ -9,7 +9,11 @@ $ fonttools varLib.interpolatable font1 font2 ...
from .interpolatableHelpers import * from .interpolatableHelpers import *
from .interpolatableTestContourOrder import test_contour_order from .interpolatableTestContourOrder import test_contour_order
from .interpolatableTestStartingPoint import test_starting_point from .interpolatableTestStartingPoint import test_starting_point
from fontTools.pens.recordingPen import RecordingPen, DecomposingRecordingPen from fontTools.pens.recordingPen import (
RecordingPen,
DecomposingRecordingPen,
lerp_recordings,
)
from fontTools.pens.transformPen import TransformPen from fontTools.pens.transformPen import TransformPen
from fontTools.pens.statisticsPen import StatisticsPen, StatisticsControlPen from fontTools.pens.statisticsPen import StatisticsPen, StatisticsControlPen
from fontTools.pens.momentsPen import OpenContourError from fontTools.pens.momentsPen import OpenContourError
@ -332,7 +336,9 @@ def test_gen(
midRecording = [] midRecording = []
for c0, c1 in zip(recording0, recording1): for c0, c1 in zip(recording0, recording1):
try: try:
midRecording.append(lerp_recordings(c0, c1)) r = RecordingPen()
r.value = lerp_recordings(c0.value, c1.value)
midRecording.append(r)
except ValueError: except ValueError:
# Mismatch because of the reordering above # Mismatch because of the reordering above
midRecording.append(None) midRecording.append(None)

View File

@ -1,3 +1,4 @@
from fontTools.ttLib.ttGlyphSet import LerpGlyphSet
from fontTools.pens.basePen import AbstractPen, BasePen, DecomposingPen from fontTools.pens.basePen import AbstractPen, BasePen, DecomposingPen
from fontTools.pens.pointPen import AbstractPointPen, SegmentToPointPen from fontTools.pens.pointPen import AbstractPointPen, SegmentToPointPen
from fontTools.pens.recordingPen import RecordingPen, DecomposingRecordingPen from fontTools.pens.recordingPen import RecordingPen, DecomposingRecordingPen
@ -374,52 +375,3 @@ def transform_from_stats(stats, inverse=False):
trans = trans.translate(stats.meanX, stats.meanY) trans = trans.translate(stats.meanX, stats.meanY)
return trans return trans
class LerpGlyphSet:
def __init__(self, glyphset1, glyphset2, factor=0.5):
self.glyphset1 = glyphset1
self.glyphset2 = glyphset2
self.factor = factor
def __getitem__(self, glyphname):
return LerpGlyph(glyphname, self)
class LerpGlyph:
def __init__(self, glyphname, glyphset):
self.glyphset = glyphset
self.glyphname = glyphname
def draw(self, pen):
recording1 = DecomposingRecordingPen(self.glyphset.glyphset1)
self.glyphset.glyphset1[self.glyphname].draw(recording1)
recording2 = DecomposingRecordingPen(self.glyphset.glyphset2)
self.glyphset.glyphset2[self.glyphname].draw(recording2)
factor = self.glyphset.factor
for (op1, args1), (op2, args2) in zip(recording1.value, recording2.value):
if op1 != op2:
raise ValueError("Mismatching operations: %s, %s" % (op1, op2))
mid_args = [
(x1 + (x2 - x1) * factor, y1 + (y2 - y1) * factor)
for (x1, y1), (x2, y2) in zip(args1, args2)
]
getattr(pen, op1)(*mid_args)
def lerp_recordings(recording1, recording2, factor=0.5):
pen = RecordingPen()
value = pen.value
for (op1, args1), (op2, args2) in zip(recording1.value, recording2.value):
if op1 != op2:
raise ValueError("Mismatched operations: %s, %s" % (op1, op2))
if op1 == "addComponent":
mid_args = args1 # XXX Interpolate transformation?
else:
mid_args = [
(x1 + (x2 - x1) * factor, y1 + (y2 - y1) * factor)
for (x1, y1), (x2, y2) in zip(args1, args2)
]
value.append((op1, mid_args))
return pen

View File

@ -1,5 +1,6 @@
from .interpolatableHelpers import * from .interpolatableHelpers import *
from fontTools.ttLib import TTFont from fontTools.ttLib import TTFont
from fontTools.ttLib.ttGlyphSet import LerpGlyphSet
from fontTools.pens.recordingPen import ( from fontTools.pens.recordingPen import (
RecordingPen, RecordingPen,
DecomposingRecordingPen, DecomposingRecordingPen,
@ -12,10 +13,9 @@ from fontTools.pens.pointPen import (
PointToSegmentPen, PointToSegmentPen,
ReverseContourPointPen, ReverseContourPointPen,
) )
from fontTools.varLib.interpolatable import ( from fontTools.varLib.interpolatableHelpers import (
PerContourOrComponentPen, PerContourOrComponentPen,
SimpleRecordingPointPen, SimpleRecordingPointPen,
LerpGlyphSet,
) )
from itertools import cycle from itertools import cycle
from functools import wraps from functools import wraps

View File

@ -1,5 +1,6 @@
from fontTools.ttLib import TTFont from fontTools.ttLib import TTFont
from fontTools.ttLib import ttGlyphSet from fontTools.ttLib import ttGlyphSet
from fontTools.ttLib.ttGlyphSet import LerpGlyphSet
from fontTools.pens.recordingPen import ( from fontTools.pens.recordingPen import (
RecordingPen, RecordingPen,
RecordingPointPen, RecordingPointPen,
@ -164,6 +165,53 @@ class TTGlyphSetTest(object):
assert actual == expected, (location, actual, expected) assert actual == expected, (location, actual, expected)
@pytest.mark.parametrize(
"fontfile, locations, factor, expected",
[
(
"I.ttf",
({"wght": 400}, {"wght": 1000}),
0.5,
[
("moveTo", ((151.5, 0.0),)),
("lineTo", ((458.5, 0.0),)),
("lineTo", ((458.5, 1456.0),)),
("lineTo", ((151.5, 1456.0),)),
("closePath", ()),
],
),
(
"I.ttf",
({"wght": 400}, {"wght": 1000}),
0.25,
[
("moveTo", ((163.25, 0.0),)),
("lineTo", ((412.75, 0.0),)),
("lineTo", ((412.75, 1456.0),)),
("lineTo", ((163.25, 1456.0),)),
("closePath", ()),
],
),
],
)
def test_lerp_glyphset(self, fontfile, locations, factor, expected):
font = TTFont(self.getpath(fontfile))
glyphset1 = font.getGlyphSet(location=locations[0])
glyphset2 = font.getGlyphSet(location=locations[1])
glyphset = LerpGlyphSet(glyphset1, glyphset2, factor)
assert "I" in glyphset
pen = RecordingPen()
glyph = glyphset["I"]
assert glyphset.get("foobar") is None
glyph.draw(pen)
actual = pen.value
assert actual == expected, (locations, actual, expected)
def test_glyphset_varComposite_components(self): def test_glyphset_varComposite_components(self):
font = TTFont(self.getpath("varc-ac00-ac01.ttf")) font = TTFont(self.getpath("varc-ac00-ac01.ttf"))
glyphset = font.getGlyphSet() glyphset = font.getGlyphSet()