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
|
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))
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user