[varLib.interpolatable] Support CFF2 input font

Fixes https://github.com/fonttools/fonttools/issues/3666
This commit is contained in:
Behdad Esfahbod 2024-10-21 23:39:08 -06:00
parent 885d7c1ecb
commit b5373bf5d2
3 changed files with 1676 additions and 48 deletions

View File

@ -752,39 +752,41 @@ def main(args=None):
elif args.inputs[0].endswith(".ttf") or args.inputs[0].endswith(".otf"):
from fontTools.ttLib import TTFont
# Is variable font?
font = TTFont(args.inputs[0])
upem = font["head"].unitsPerEm
fvar = font["fvar"]
axisMapping = {}
for axis in fvar.axes:
axisMapping[axis.axisTag] = {
-1: axis.minValue,
0: axis.defaultValue,
1: axis.maxValue,
}
normalized = False
if "avar" in font:
avar = font["avar"]
if getattr(avar.table, "VarStore", None):
axisMapping = {tag: {-1: -1, 0: 0, 1: 1} for tag in axisMapping}
normalized = True
else:
for axisTag, segments in avar.segments.items():
fvarMapping = axisMapping[axisTag].copy()
for location, value in segments.items():
axisMapping[axisTag][value] = piecewiseLinearMap(
location, fvarMapping
)
# Gather all glyphs at their "master" locations
ttGlyphSets = {}
glyphsets = defaultdict(dict)
if "gvar" in font:
fvar = font["fvar"]
axisMapping = {}
for axis in fvar.axes:
axisMapping[axis.axisTag] = {
-1: axis.minValue,
0: axis.defaultValue,
1: axis.maxValue,
}
normalized = False
if "avar" in font:
avar = font["avar"]
if getattr(avar.table, "VarStore", None):
axisMapping = {tag: {-1: -1, 0: 0, 1: 1} for tag in axisMapping}
normalized = True
else:
for axisTag, segments in avar.segments.items():
fvarMapping = axisMapping[axisTag].copy()
for location, value in segments.items():
axisMapping[axisTag][value] = piecewiseLinearMap(
location, fvarMapping
)
gvar = font["gvar"]
glyf = font["glyf"]
# Gather all glyphs at their "master" locations
ttGlyphSets = {}
glyphsets = defaultdict(dict)
if glyphs is None:
glyphs = sorted(gvar.variations.keys())
@ -806,30 +808,84 @@ def main(args=None):
glyphname, glyphsets[locTuple], ttGlyphSets[locTuple], glyf
)
names = ["''"]
fonts = [font.getGlyphSet()]
locations = [{}]
axis_triples = {a: (-1, 0, +1) for a in sorted(axisMapping.keys())}
for locTuple in sorted(glyphsets.keys(), key=lambda v: (len(v), v)):
name = (
"'"
+ " ".join(
"%s=%s"
% (
k,
floatToFixedToStr(
piecewiseLinearMap(v, axisMapping[k]), 14
),
)
for k, v in locTuple
elif "CFF2" in font:
fvarAxes = font["fvar"].axes
cff2 = font["CFF2"].cff.topDictIndex[0]
charstrings = cff2.CharStrings
if glyphs is None:
glyphs = sorted(charstrings.keys())
for glyphname in glyphs:
cs = charstrings[glyphname]
private = cs.private
# Extract vsindex for the glyph
vsindices = {getattr(private, "vsindex", 0)}
vsindex = getattr(private, "vsindex", 0)
last_op = 0
# The spec says vsindex can only appear once and must be the first
# operator in the charstring, but we support multiple.
# https://github.com/harfbuzz/boring-expansion-spec/issues/158
for op in enumerate(cs.program):
if op == "blend":
vsindices.add(vsindex)
elif op == "vsindex":
assert isinstance(last_op, int)
vsindex = last_op
last_op = op
if not hasattr(private, "vstore"):
continue
varStore = private.vstore.otVarStore
for vsindex in vsindices:
varData = varStore.VarData[vsindex]
for regionIndex in varData.VarRegionIndex:
region = varStore.VarRegionList.Region[regionIndex]
locDict = {}
loc = []
for axisIndex, axis in enumerate(region.VarRegionAxis):
tag = fvarAxes[axisIndex].axisTag
val = axis.PeakCoord
locDict[tag] = val
loc.append((tag, val))
locTuple = tuple(loc)
if locTuple not in ttGlyphSets:
ttGlyphSets[locTuple] = font.getGlyphSet(
location=locDict,
normalized=True,
recalcBounds=False,
)
glyphset = glyphsets[locTuple]
glyphset[glyphname] = ttGlyphSets[locTuple][glyphname]
names = ["''"]
fonts = [font.getGlyphSet()]
locations = [{}]
axis_triples = {a: (-1, 0, +1) for a in sorted(axisMapping.keys())}
for locTuple in sorted(glyphsets.keys(), key=lambda v: (len(v), v)):
name = (
"'"
+ " ".join(
"%s=%s"
% (
k,
floatToFixedToStr(
piecewiseLinearMap(v, axisMapping[k]), 14
),
)
+ "'"
for k, v in locTuple
)
if normalized:
name += " (normalized)"
names.append(name)
fonts.append(glyphsets[locTuple])
locations.append(dict(locTuple))
+ "'"
)
if normalized:
name += " (normalized)"
names.append(name)
fonts.append(glyphsets[locTuple])
locations.append(dict(locTuple))
args.ignore_missing = True
args.inputs = []

File diff suppressed because it is too large Load Diff

View File

@ -94,6 +94,33 @@ class InterpolatableTest(unittest.TestCase):
otf_paths = self.get_file_list(self.tempdir, suffix)
self.assertIsNone(interpolatable_main(otf_paths))
def test_interpolatable_cff2(self):
suffix = ".otf"
ttx_dir = self.get_test_input("variable_ttx_interpolatable_cff2")
ttx_path = os.path.abspath(os.path.join(ttx_dir, "interpolatable-test.ttx"))
self.temp_dir()
self.compile_font(ttx_path, suffix, self.tempdir)
otf_path = self.get_file_list(self.tempdir, suffix)[0]
problems = interpolatable_main([otf_path])
print(problems)
self.assertEqual(
problems["uni0408"],
[
{
"type": "underweight",
"contour": 0,
"master_1": "'wght=200.0 opsz=20.0'",
"master_2": "'wght=200.0 opsz=60.0'",
"master_1_idx": 2,
"master_2_idx": 3,
"tolerance": 0.9184032411892079,
},
],
)
def test_interpolatable_ufo(self):
ttx_dir = self.get_test_input("master_ufo")
ufo_paths = self.get_file_list(ttx_dir, ".ufo", "TestFamily2-")