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
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__":
pen = RecordingPen()
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.transform import Transform
from fontTools.pens.transformPen import TransformPen, TransformPointPen
from fontTools.pens.recordingPen import DecomposingRecordingPen
class _TTGlyphSet(Mapping):
@ -321,3 +322,64 @@ def _setCoordinates(glyph, coord, glyfTable, *, recalcBounds=True):
verticalAdvanceWidth,
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 .interpolatableTestContourOrder import test_contour_order
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.statisticsPen import StatisticsPen, StatisticsControlPen
from fontTools.pens.momentsPen import OpenContourError
@ -332,7 +336,9 @@ def test_gen(
midRecording = []
for c0, c1 in zip(recording0, recording1):
try:
midRecording.append(lerp_recordings(c0, c1))
r = RecordingPen()
r.value = lerp_recordings(c0.value, c1.value)
midRecording.append(r)
except ValueError:
# Mismatch because of the reordering above
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.pointPen import AbstractPointPen, SegmentToPointPen
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)
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 fontTools.ttLib import TTFont
from fontTools.ttLib.ttGlyphSet import LerpGlyphSet
from fontTools.pens.recordingPen import (
RecordingPen,
DecomposingRecordingPen,
@ -12,10 +13,9 @@ from fontTools.pens.pointPen import (
PointToSegmentPen,
ReverseContourPointPen,
)
from fontTools.varLib.interpolatable import (
from fontTools.varLib.interpolatableHelpers import (
PerContourOrComponentPen,
SimpleRecordingPointPen,
LerpGlyphSet,
)
from itertools import cycle
from functools import wraps

View File

@ -1,5 +1,6 @@
from fontTools.ttLib import TTFont
from fontTools.ttLib import ttGlyphSet
from fontTools.ttLib.ttGlyphSet import LerpGlyphSet
from fontTools.pens.recordingPen import (
RecordingPen,
RecordingPointPen,
@ -164,6 +165,53 @@ class TTGlyphSetTest(object):
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):
font = TTFont(self.getpath("varc-ac00-ac01.ttf"))
glyphset = font.getGlyphSet()