Move LerpGlyphSet and lerp_recordings to more prominent places
Fixes https://github.com/fonttools/fonttools/issues/3361
This commit is contained in:
parent
dcf3f0c4b1
commit
4f6c739181
@ -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))
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user