diff --git a/Lib/fontTools/varLib/interpolatable.py b/Lib/fontTools/varLib/interpolatable.py index 9b6034e03..c5d7ecf52 100644 --- a/Lib/fontTools/varLib/interpolatable.py +++ b/Lib/fontTools/varLib/interpolatable.py @@ -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 = [] diff --git a/Tests/varLib/data/variable_ttx_interpolatable_cff2/interpolatable-test.ttx b/Tests/varLib/data/variable_ttx_interpolatable_cff2/interpolatable-test.ttx new file mode 100644 index 000000000..f3e7b4291 --- /dev/null +++ b/Tests/varLib/data/variable_ttx_interpolatable_cff2/interpolatable-test.ttx @@ -0,0 +1,1545 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + © 2014 - 2023 Adobe (http://www.adobe.com/), with Reserved Font Name ‘Source’. + + + Source Serif 4 Variable + + + Italic + + + 4.005;ADBO;SourceSerif4Variable-Italic;ADOBE + + + Source Serif 4 Variable Italic + + + Version 4.005;hotconv 1.1.0;makeotfexe 2.6.0 + + + SourceSerif4Variable-Italic + + + Italic + + + Italic + + + ExtraLight + + + Light + + + Regular + + + Medium + + + Semibold + + + Bold + + + ExtraBold + + + Black + + + Caption + + + SmallText + + + Text + + + Subhead + + + Display + + + Italic + + + Weight + + + Optical Size + + + Caption ExtraLight Italic + + + SourceSerif4Variable-CaptionExtraLightItalic + + + Caption Light Italic + + + SourceSerif4Variable-CaptionLightItalic + + + Caption Italic + + + SourceSerif4Variable-CaptionRegularItalic + + + Caption Semibold Italic + + + SourceSerif4Variable-CaptionSemiboldItalic + + + Caption Bold Italic + + + SourceSerif4Variable-CaptionBoldItalic + + + Caption Black Italic + + + SourceSerif4Variable-CaptionBlackItalic + + + SmText ExtraLight Italic + + + SourceSerif4Variable-SmTextExtraLightItalic + + + SmText Light Italic + + + SourceSerif4Variable-SmTextLightItalic + + + SmText Italic + + + SourceSerif4Variable-SmTextRegularItalic + + + SmText Semibold Italic + + + SourceSerif4Variable-SmTextSemiboldItalic + + + SmText Bold Italic + + + SourceSerif4Variable-SmTextBoldItalic + + + SmText Black Italic + + + SourceSerif4Variable-SmTextBlackItalic + + + ExtraLight Italic + + + SourceSerif4Variable-ExtraLightItalic + + + Light Italic + + + SourceSerif4Variable-LightItalic + + + SourceSerif4Variable-Italic + + + Semibold Italic + + + SourceSerif4Variable-SemiboldItalic + + + Bold Italic + + + SourceSerif4Variable-BoldItalic + + + Black Italic + + + SourceSerif4Variable-BlackItalic + + + Subhead ExtraLight Italic + + + SourceSerif4Variable-SubheadExtraLightItalic + + + Subhead Light Italic + + + SourceSerif4Variable-SubheadLightItalic + + + Subhead Italic + + + SourceSerif4Variable-SubheadRegularItalic + + + Subhead Semibold Italic + + + SourceSerif4Variable-SubheadSemiboldItalic + + + Subhead Bold Italic + + + SourceSerif4Variable-SubheadBoldItalic + + + Subhead Black Italic + + + SourceSerif4Variable-SubheadBlackItalic + + + Display ExtraLight Italic + + + SourceSerif4Variable-DisplayExtraLightItalic + + + Display Light Italic + + + SourceSerif4Variable-DisplayLightItalic + + + Display Italic + + + SourceSerif4Variable-DisplayRegularItalic + + + Display Semibold Italic + + + SourceSerif4Variable-DisplaySemiboldItalic + + + Display Bold Italic + + + SourceSerif4Variable-DisplayBoldItalic + + + Display Black Italic + + + SourceSerif4Variable-DisplayBlackItalic + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 50 569 51 -28 20 0 1 0 -1 0 -1 63 -57 1 0 -1 0 -1 0 -29 19 -1 -1 1 1 1 1 3 blend + hstem + 89 60 360 60 18 -6 17 13 -20 -19 6 7 -35 20 0 1 0 -1 0 -1 70 -40 0 -2 0 2 0 2 -35 20 0 1 0 -1 0 -1 4 blend + vstem + 89 18 -6 17 13 -20 -19 6 7 1 blend + hmoveto + 61 -36 39 -1 0 1 0 1 0 1 blend + hlineto + 419 670 -61 36 -39 1 0 -1 0 -1 0 6 -18 0 0 0 0 0 0 36 -39 1 0 -1 0 -1 0 3 blend + 0 -419 -670 -36 39 -1 0 1 0 1 0 -6 18 0 0 0 0 0 0 2 blend + rlineto + 480 hmoveto + -419 670 -61 -36 39 -1 0 1 0 1 0 6 -18 0 0 0 0 0 0 36 -39 1 0 -1 0 -1 0 3 blend + 0 419 -670 36 -39 1 0 -1 0 -1 0 -6 18 0 0 0 0 0 0 2 blend + rlineto + 61 -36 39 -1 0 1 0 1 0 1 blend + hlineto + -420 50 -35 20 0 1 0 -1 0 -1 -28 20 0 1 0 -1 0 -1 2 blend + rmoveto + 569 360 -569 -360 63 -57 1 0 -1 0 -1 0 70 -40 0 -2 0 2 0 2 -63 57 -1 0 1 0 1 0 -70 40 0 2 0 -2 0 -2 4 blend + vlineto + -60 -50 35 -20 0 -1 0 1 0 1 28 -20 0 -1 0 1 0 1 2 blend + rmoveto + 480 670 6 -18 0 0 0 0 0 0 1 blend + -480 -670 -6 18 0 0 0 0 0 0 1 blend + hlineto + + + -134 97 660.6587 46.3413 13 -18 2 -11 -2 12 -4 11 -43 40 -1 -11 0 1 3 -1 59.0837 -57.2318 -14.6587 47.5829 10.9163 -28.1159 2.2318 -0.9809 -23.0837 17.2318 13.6587 -25.5829 -8.9163 15.1159 -1.2318 -9.0191 4 blend + hstem + -115 502 26 -17 20 1 19 2 -4 -4 -18 57 18 -19 -41 -15 9 1 2 blend + vstem + -115 -83 26 -17 20 1 19 2 -4 -4 -5 0 3 -16 -5 9 -11 18 2 blend + rmoveto + -35 3 43 -16 46 12 -13 7 3 -1 3 -4 4 4 12 2 0 -9 -4 -2 -14 -13 -1 -14 -12 5 7 13 15 6 -5 -8 2 4 0 11 -11 1 8 16 -6 -20 -18 -1 3 5 blend + hhcurveto + 64 77 54 100 43 12 19 50 -15 -37 -28 -31 -4 -4 -6 -21 -44 -5 9 21 13 13 -8 39 -38 -43 -10 -25 19 40 -22 -37 -68 -39 -39 30 32 -13 10 -11 -8 6 14 4 2 5 blend + hvcurveto + 18 39 11 49 14 75 93 487 -5 24 15 45 -6 -3 -16 16 18 21 27 16 -28 -22 -24 17 -1 12 -6 5 15 14 15 9 -9 45 33 11 31 37 -29 64 8 20 12 27 -3 -25 -19 -37 44 91 58 126 -1 -121 -73 -173 -23 -20 -23 0 11 22 16 -2 -113 -127 -122 -36 82 143 125 30 8 blend + rcurveline + -90 58 -91 -6 20 4 -17 -9 -3 1 blend + hlineto + -15 -104 -20 -104 -18 -102 -30 -173 -9 -60 -14 -67 -15 -71 -24 -54 -42 -25 -9 -17 -6 -7 10 9 20 18 -30 -85 -20 -20 44 29 109 83 5 -8 -4 -7 3 -1 14 9 10 -18 -14 -22 -1 -9 37 21 5 3 -3 -4 -4 -5 0 -2 34 6 -22 -22 -20 -35 18 -8 13 20 17 0 -29 -9 -35 -4 85 113 93 -1 -171 -57 -185 -24 -10 1 -4 1 4 14 7 -5 -26 -1 -33 5 16 67 14 -13 -3 3 -4 7 5 6 6 -6 -21 -9 -8 36 48 34 4 -10 -11 7 -2 0 12 -2 4 -16 -63 11 -1 0 87 -9 6 -75 -19 12 0 -3 20 -19 3 -3 -11 3 15 -6 -6 6 -18 28 -37 28 11 -10 6 31 -6 1 2 -6 -13 4 10 -2 -11 -3 18 blend + rrcurveto + 0 -12 31 0 0 30 0 -11 0 -16 -1 9 2 21 7 -15 -6 10 -4 -8 2 -85 -6 29 -7 -16 -3 3 blend + 0 0 11 0 0 32 0 -25 0 13 1 -8 -1 -27 -6 21 6 -4 2 2 blend + rlineto + 41 -54 -21 13 -23 -10 22 5 6 -15 -14 12 19 6 -5 16 6 11 14 22 7 8 -7 1 3 0 -4 -5 -5 -7 3 5 -3 0 3 7 -6 5 -14 -13 0 7 4 -4 12 5 blend + hhcurveto + -33 -30 -16 -30 -5 15 -11 -7 2 8 -3 1 6 16 0 7 7 -8 -2 -1 -6 7 -12 -4 -2 3 5 -8 8 18 -10 6 8 -6 -9 -2 0 0 -1 0 -1 1 3 2 -1 5 blend + hvcurveto + 219 712 5 -6 -3 20 -24 -21 0 10 32 -36 -22 41 16 -24 13 -8 2 blend + rmoveto + 126 -15 20 0 12 18 -12 -16 -3 -16 -13 8 0 15 3 -8 -2 0 -4 -6 25 8 1 -6 -1 43 30 3 blend + 0 129 15 8 41 -275 -13 23 -7 -23 5 7 -19 -25 -8 0 -15 -3 8 2 0 4 -4 3 2 -5 0 3 1 -1 -21 18 19 -25 -11 15 -2 -10 19 -60 -19 34 17 -3 -8 8 5 blend + 0 -8 -41 4 -3 -2 5 0 -3 -1 1 21 -18 -19 25 11 -15 2 10 2 blend + rlineto + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + wght + 0x0 + 200.0 + 400.0 + 900.0 + 278 + + + + + opsz + 0x0 + 8.0 + 20.0 + 60.0 + 279 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/interpolatable_test.py b/Tests/varLib/interpolatable_test.py index c27b23390..18ec1d524 100644 --- a/Tests/varLib/interpolatable_test.py +++ b/Tests/varLib/interpolatable_test.py @@ -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-")