From b95607513c2afb2d817559c5036270f956ee386d Mon Sep 17 00:00:00 2001 From: Marc Foley Date: Fri, 2 Oct 2020 13:05:08 +0100 Subject: [PATCH 01/28] WIP instancer: update static font nametable --- Lib/fontTools/varLib/instancer.py | 146 ++++++++++++++++++++++++++++++ Tests/varLib/instancer_test.py | 37 ++++++++ 2 files changed, 183 insertions(+) diff --git a/Lib/fontTools/varLib/instancer.py b/Lib/fontTools/varLib/instancer.py index fba178429..6ed8c366f 100644 --- a/Lib/fontTools/varLib/instancer.py +++ b/Lib/fontTools/varLib/instancer.py @@ -127,6 +127,15 @@ class OverlapMode(IntEnum): KEEP_AND_SET_FLAGS = 1 REMOVE = 2 +class NameID(IntEnum): + FAMILY_NAME = 1 + SUBFAMILY_NAME = 2 + UNIQUE_FONT_IDENTIFIER = 3 + FULL_FONT_NAME = 4 + POSTSCRIPT_NAME = 6 + TYPOGRAPHIC_FAMILY_NAME = 16 + TYPOGRAPHIC_SUBFAMILY_NAME = 17 + def instantiateTupleVariationStore( variations, axisLimits, origCoords=None, endPts=None @@ -1187,6 +1196,7 @@ def instantiateVariableFont( inplace=False, optimize=True, overlap=OverlapMode.KEEP_AND_SET_FLAGS, + update_nametable=False ): """Instantiate variable font, either fully or partially. @@ -1272,6 +1282,10 @@ def instantiateVariableFont( log.info("Removing overlaps from glyf table") removeOverlaps(varfont) + if update_nametable: + log.info("Updating nametable") + updateNameTable(varfont, axisLimits) + varLib.set_default_weight_width_slant( varfont, location={ @@ -1284,6 +1298,131 @@ def instantiateVariableFont( return varfont +def updateNameTable(varfont, axisLimits): + nametable = varfont["name"] + if "STAT" not in varfont: + raise ValueError("Cannot update name table since there is no STAT table.") + stat = varfont['STAT'] + + axisRecords = stat.table.DesignAxisRecord.Axis + axisValues = stat.table.AxisValueArray.AxisValue + + axisOrder = {a.AxisOrdering: a.AxisTag for a in axisRecords} + keptAxisValues = [] + for axisValue in axisValues: + # TODO Format 4 + if axisValue.Format == 4: + continue + + axisTag = axisOrder[axisValue.AxisIndex] + if axisTag in axisLimits: + pinnedAxis = isinstance(axisLimits[axisTag], (float, int)) + else: + pinnedAxis = False + + # Ignore axisValue if it has ELIDABLE_AXIS_VALUE_NAME flag enabled. + # Enabling this flag will hide the axisValue in application font menus. + if axisValue.Flags == 2: + continue + + if axisValue.Format in (1, 3): + # Add axisValue if it's used to link to another variable font + if axisTag not in axisLimits and axisValue.Value == 1.0: + keptAxisValues.append(axisValue) + + # Add axisValue if its value is in the axisLimits and the user has + # pinned the axis + elif pinnedAxis and axisValue.Value == axisLimits[axisTag]: + keptAxisValues.append(axisValue) + + if axisValue.Format == 2: + if pinnedAxis and axisLimits[axisTag] >= axisValue.RangeMinValue \ + and axisLimits[axisTag] <= axisValue.RangeMaxValue: + keptAxisValues.append(axisValue) + + _updateNameRecords(varfont, nametable, keptAxisValues) + + +def _updateNameRecords(varfont, nametable, axisValues): + # Update nametable based on the axisValues + # using the R/I/B/BI and WWS models. + engNameRecords = any([r for r in nametable.names if r.langID == 0x409]) + if not engNameRecords: + # TODO (Marc F) improve error msg + raise ValueError("No English namerecords") + + ribbiAxisValues = _ribbiAxisValues(nametable, axisValues) + nonRibbiAxisValues = [av for av in axisValues if av not in ribbiAxisValues] + + nametblLangs = set((r.platformID, r.platEncID, r.langID) for r in nametable.names) + for lang in nametblLangs: + _updateStyleRecords( + nametable, + ribbiAxisValues, + nonRibbiAxisValues, + lang, + ) + + +def _ribbiAxisValues(nametable, axisValues): + ribbiStyles = frozenset(["Regular", "Italic", "Bold", "Bold Italic"]) + res = [] + for axisValue in axisValues: + name = nametable.getName(axisValue.ValueNameID, 3, 1, 0x409).toUnicode() + if name in ribbiStyles: + res.append(axisValue) + return res + + +def _updateStyleRecords( + nametable, + ribbiAxisValues, + nonRibbiAxisValues, + lang=(3, 1, 0x409) +): +# wwsAxes = frozenset(["wght", "wdth", "ital"]) + currentFamilyName = nametable.getName(NameID.TYPOGRAPHIC_FAMILY_NAME, *lang) or \ + nametable.getName(NameID.FAMILY_NAME, *lang) + if not currentFamilyName: + return + currentFamilyName = currentFamilyName.toUnicode() + + currentStyleName = nametable.getName(NameID.TYPOGRAPHIC_SUBFAMILY_NAME, *lang) or \ + nametable.getName(NameID.SUBFAMILY_NAME, *lang) + currentStyleName = currentStyleName.toUnicode() + + ribbiName = " ".join([nametable.getName(a.ValueNameID, *lang).toUnicode() for a in ribbiAxisValues]) + nonRibbiName = " ".join([nametable.getName(a.ValueNameID, *lang).toUnicode() for a in nonRibbiAxisValues]) + + nameIDs = { + NameID.FAMILY_NAME: currentFamilyName, + NameID.SUBFAMILY_NAME: ribbiName or "Regular" + } + if nonRibbiAxisValues: + nameIDs[NameID.FAMILY_NAME] = f"{currentFamilyName} {nonRibbiName}" + nameIDs[NameID.TYPOGRAPHIC_FAMILY_NAME] = currentFamilyName + nameIDs[NameID.TYPOGRAPHIC_SUBFAMILY_NAME] = f"{nonRibbiName} {ribbiName}".strip() +# # Include WWS name records if there are nonWwsParticles +# if nonWwsParticles: +# nameIDs[21] = f"{currentFamilyName} {' '.join(nonWwsParticles)}" +# nameIDs[22] = " ".join(wwsParticles) +# # Enable fsSelection bit 8 (WWS) +# varfont['OS/2'].fsSelection |= (1 << 8) +# + newFamilyName = nameIDs.get(NameID.TYPOGRAPHIC_FAMILY_NAME) or \ + nameIDs.get(NameID.FAMILY_NAME) + newStyleName = nameIDs.get(NameID.TYPOGRAPHIC_SUBFAMILY_NAME) or \ + nameIDs.get(NameID.SUBFAMILY_NAME) + + nameIDs[NameID.FULL_FONT_NAME] = f"{newFamilyName} {newStyleName}" + nameIDs[NameID.POSTSCRIPT_NAME] = f"{newFamilyName.replace(' ', '')}-{newStyleName.replace(' ', '')}" + # Update uniqueID + # TODO + # versionRecord = nametable.getName(5, 3, 1, 0x409) + for nameID, string in nameIDs.items(): + nametable.setName(string, nameID, *lang) + + def splitAxisLocationAndRanges(axisLimits, rangeType=AxisRange): location, axisRanges = {}, {} for axisTag, value in axisLimits.items(): @@ -1377,6 +1516,12 @@ def parseArgs(args): help="Merge overlapping contours and components (only applicable " "when generating a full instance). Requires skia-pathops", ) + parser.add_argument( + "--update-nametable", + action="store_true", + help="Update the instantiated font's nametable using the STAT " + "table Axis Values" + ) loggingGroup = parser.add_mutually_exclusive_group(required=False) loggingGroup.add_argument( "-v", "--verbose", action="store_true", help="Run more verbosely." @@ -1428,6 +1573,7 @@ def main(args=None): inplace=True, optimize=options.optimize, overlap=options.overlap, + update_nametable=options.update_nametable, ) outfile = ( diff --git a/Tests/varLib/instancer_test.py b/Tests/varLib/instancer_test.py index 5e999dc83..75156a497 100644 --- a/Tests/varLib/instancer_test.py +++ b/Tests/varLib/instancer_test.py @@ -1916,6 +1916,43 @@ def test_normalizeAxisLimits_missing_from_fvar(varfont): instancer.normalizeAxisLimits(varfont, {"ZZZZ": 1000}) +def _get_name_records(varfont): + nametable = varfont["name"] + return { + (r.nameID, r.platformID, r.platEncID, r.langID): r.toUnicode() + for r in nametable.names + } + + +def test_updateNameTable(varfont): + instancer.updateNameTable(varfont, {"wght": 400, "wdth": 100}) + names = _get_name_records(varfont) + assert names[(1, 3, 1, 0x409)] == "Test Variable Font" + assert names[(2, 3, 1, 0x0409)] == "Regular" + assert names[(6, 3, 1, 0x409)] == "TestVariableFont-Regular" + assert (16, 3, 1, 0x409) not in names + assert (17, 3, 1, 0x409) not in names + + instancer.updateNameTable(varfont, {"wght": 900, "wdth": 100}) + names = _get_name_records(varfont) + assert names[(1, 3, 1, 0x409)] == "Test Variable Font Black" + assert names[(2, 3, 1, 0x409)] == "Regular" + assert names[(6, 3, 1, 0x409)] == "TestVariableFont-Black" + assert names[(16, 3, 1, 0x409)] == "Test Variable Font" + assert names[(17, 3, 1, 0x409)] == "Black" + + instancer.updateNameTable(varfont, {"wght": 100, "wdth": 100}) + names = _get_name_records(varfont) + assert names[(1, 3, 1, 0x409)] == "Test Variable Font Thin" + assert names[(2, 3, 1, 0x409)] == "Regular" + assert names[(6, 3, 1, 0x409)] == "TestVariableFont-Thin" + assert names[(16, 3, 1, 0x409)] == "Test Variable Font" + assert names[(17, 3, 1, 0x409)] == "Thin" + + # TODO (Marc F) this doesn't work because our test font is using Format 4 for wdth axis + instancer.updateNameTable(varfont, {"wdth": 79, "wdth": 400}) + + def test_sanityCheckVariableTables(varfont): font = ttLib.TTFont() with pytest.raises(ValueError, match="Missing required table fvar"): From b502471a08395d567c1f1fd22ff10c160ac1a93c Mon Sep 17 00:00:00 2001 From: Marc Foley Date: Wed, 7 Oct 2020 22:59:20 +0100 Subject: [PATCH 02/28] wip instancer: support format 4 axisvalues --- Lib/fontTools/varLib/instancer.py | 83 ++++++++++++++++++++----------- Tests/varLib/instancer_test.py | 48 ++++++++++++++++-- 2 files changed, 100 insertions(+), 31 deletions(-) diff --git a/Lib/fontTools/varLib/instancer.py b/Lib/fontTools/varLib/instancer.py index 6ed8c366f..004dad5e1 100644 --- a/Lib/fontTools/varLib/instancer.py +++ b/Lib/fontTools/varLib/instancer.py @@ -1298,49 +1298,74 @@ def instantiateVariableFont( return varfont -def updateNameTable(varfont, axisLimits): - nametable = varfont["name"] - if "STAT" not in varfont: - raise ValueError("Cannot update name table since there is no STAT table.") - stat = varfont['STAT'] +def axisValuesFromAxisLimits(stat, axisLimits): axisRecords = stat.table.DesignAxisRecord.Axis axisValues = stat.table.AxisValueArray.AxisValue - axisOrder = {a.AxisOrdering: a.AxisTag for a in axisRecords} - keptAxisValues = [] - for axisValue in axisValues: - # TODO Format 4 - if axisValue.Format == 4: - continue + format4 = [a for a in axisValues if a.Format == 4] + nonformat4 = [a for a in axisValues if a not in format4] + axisValues = format4 + nonformat4 - axisTag = axisOrder[axisValue.AxisIndex] - if axisTag in axisLimits: - pinnedAxis = isinstance(axisLimits[axisTag], (float, int)) - else: - pinnedAxis = False + axisOrder = {a.AxisOrdering: a.AxisTag for a in axisRecords} + pinnedAxes = set(k for k, v in axisLimits.items() if isinstance(v, (float, int))) + + results, seen_axes = [], set() + for axisValue in axisValues: # Ignore axisValue if it has ELIDABLE_AXIS_VALUE_NAME flag enabled. # Enabling this flag will hide the axisValue in application font menus. + # TODO this is too greedy! we need to retain wght axisValues if axisValue.Flags == 2: continue - if axisValue.Format in (1, 3): + if axisValue.Format == 4: + axisIndexes = set(r.AxisIndex for r in axisValue.AxisValueRecord) + if seen_axes - axisIndexes != seen_axes: + continue + # TODO fix dup appends + for rec in axisValue.AxisValueRecord: + axisTag = axisOrder[rec.AxisIndex] + if axisTag not in pinnedAxes: + continue + if rec.Value == axisLimits[axisTag]: + seen_axes.add(rec.AxisIndex) + results.append((rec.AxisIndex, axisValue)) + + elif axisValue.Format in (1, 3): + axisTag = axisOrder[axisValue.AxisIndex] # Add axisValue if it's used to link to another variable font if axisTag not in axisLimits and axisValue.Value == 1.0: - keptAxisValues.append(axisValue) + seen_axes.add(rec.AxisIndex) + results.append((axisValue.AxisIndex, axisValue)) + if axisTag not in pinnedAxes: + continue # Add axisValue if its value is in the axisLimits and the user has # pinned the axis - elif pinnedAxis and axisValue.Value == axisLimits[axisTag]: - keptAxisValues.append(axisValue) + elif axisValue.Value == axisLimits[axisTag]: + seen_axes.add(rec.AxisIndex) + results.append((axisValue.AxisIndex,axisValue)) - if axisValue.Format == 2: - if pinnedAxis and axisLimits[axisTag] >= axisValue.RangeMinValue \ + elif axisValue.Format == 2: + axisTag = axisOrder[axisValue.AxisIndex] + if axisTag not in pinnedAxes: + continue + if axisLimits[axisTag] >= axisValue.RangeMinValue \ and axisLimits[axisTag] <= axisValue.RangeMaxValue: - keptAxisValues.append(axisValue) + seen_axes.add(axisValue.AxisIndex) + results.append((axisValue.AxisIndex, axisValue)) + return [v for k, v in sorted(results)] - _updateNameRecords(varfont, nametable, keptAxisValues) + +def updateNameTable(varfont, axisLimits): + if "STAT" not in varfont: + raise ValueError("Cannot update name table since there is no STAT table.") + stat = varfont['STAT'] + nametable = varfont["name"] + + selectedAxisValues = axisValuesFromAxisLimits(stat, axisLimits) + _updateNameRecords(varfont, nametable, selectedAxisValues) def _updateNameRecords(varfont, nametable, axisValues): @@ -1383,12 +1408,14 @@ def _updateStyleRecords( # wwsAxes = frozenset(["wght", "wdth", "ital"]) currentFamilyName = nametable.getName(NameID.TYPOGRAPHIC_FAMILY_NAME, *lang) or \ nametable.getName(NameID.FAMILY_NAME, *lang) - if not currentFamilyName: - return - currentFamilyName = currentFamilyName.toUnicode() currentStyleName = nametable.getName(NameID.TYPOGRAPHIC_SUBFAMILY_NAME, *lang) or \ nametable.getName(NameID.SUBFAMILY_NAME, *lang) + # TODO cleanup + if not currentFamilyName or not currentStyleName: + print(f"Cannot update {lang} since it's missing a familyName nameID 1 or subFamilyName nameID 2 entry") + return + currentFamilyName = currentFamilyName.toUnicode() currentStyleName = currentStyleName.toUnicode() ribbiName = " ".join([nametable.getName(a.ValueNameID, *lang).toUnicode() for a in ribbiAxisValues]) @@ -1396,7 +1423,7 @@ def _updateStyleRecords( nameIDs = { NameID.FAMILY_NAME: currentFamilyName, - NameID.SUBFAMILY_NAME: ribbiName or "Regular" + NameID.SUBFAMILY_NAME: ribbiName or nametable.getName(NameID.SUBFAMILY_NAME, *lang).toUnicode() } if nonRibbiAxisValues: nameIDs[NameID.FAMILY_NAME] = f"{currentFamilyName} {nonRibbiName}" diff --git a/Tests/varLib/instancer_test.py b/Tests/varLib/instancer_test.py index 75156a497..c6ae14666 100644 --- a/Tests/varLib/instancer_test.py +++ b/Tests/varLib/instancer_test.py @@ -1924,7 +1924,8 @@ def _get_name_records(varfont): } -def test_updateNameTable(varfont): +def test_updateNameTable_with_registered_axes(varfont): + # Regular instancer.updateNameTable(varfont, {"wght": 400, "wdth": 100}) names = _get_name_records(varfont) assert names[(1, 3, 1, 0x409)] == "Test Variable Font" @@ -1933,6 +1934,7 @@ def test_updateNameTable(varfont): assert (16, 3, 1, 0x409) not in names assert (17, 3, 1, 0x409) not in names + # Black instancer.updateNameTable(varfont, {"wght": 900, "wdth": 100}) names = _get_name_records(varfont) assert names[(1, 3, 1, 0x409)] == "Test Variable Font Black" @@ -1941,6 +1943,7 @@ def test_updateNameTable(varfont): assert names[(16, 3, 1, 0x409)] == "Test Variable Font" assert names[(17, 3, 1, 0x409)] == "Black" + # Thin instancer.updateNameTable(varfont, {"wght": 100, "wdth": 100}) names = _get_name_records(varfont) assert names[(1, 3, 1, 0x409)] == "Test Variable Font Thin" @@ -1949,8 +1952,47 @@ def test_updateNameTable(varfont): assert names[(16, 3, 1, 0x409)] == "Test Variable Font" assert names[(17, 3, 1, 0x409)] == "Thin" - # TODO (Marc F) this doesn't work because our test font is using Format 4 for wdth axis - instancer.updateNameTable(varfont, {"wdth": 79, "wdth": 400}) + # Thin Condensed + instancer.updateNameTable(varfont, {"wdth": 79, "wght": 100}) + names = _get_name_records(varfont) + assert names[(1, 3, 1, 0x409)] == "Test Variable Font Thin Condensed" + assert names[(2, 3, 1, 0x409)] == "Regular" + assert names[(6, 3, 1, 0x409)] == "TestVariableFont-ThinCondensed" + assert names[(16, 3, 1, 0x409)] == "Test Variable Font" + assert names[(17, 3, 1, 0x409)] == "Thin Condensed" + + +def test_updateNameTable_with_multilingual_names(varfont): + name = varfont["name"] + name.setName("Test Variable Font", 1, 3, 1, 0x405) + name.setName("Normal", 2, 3, 1, 0x405) + name.setName("Normal", 261, 3, 1, 0x405) # nameID 261=Regular STAT entry + name.setName("Negreta",266, 3, 1, 0x405) # nameID 266=Black STAT entry + name.setName("Zhuštěné", 279, 3, 1, 0x405) # nameID 279=Condensed STAT entry + + # Regular | Normal + instancer.updateNameTable(varfont, {"wdth": 100, "wght": 400}) + names = _get_name_records(varfont) + assert names[(1, 3, 1, 0x405)] == "Test Variable Font" + assert names[(2, 3, 1, 0x405)] == "Normal" + assert (16, 3, 1, 0x405) not in names + assert (17, 3, 1, 0x405) not in names + + # Black | Negreta + instancer.updateNameTable(varfont, {"wdth": 100, "wght": 900}) + names = _get_name_records(varfont) + assert names[(1, 3, 1, 0x405)] == "Test Variable Font Negreta" + assert names[(2, 3, 1, 0x405)] == "Normal" + assert names[(16, 3, 1, 0x405)] == "Test Variable Font" + assert names[(17, 3, 1, 0x405)] == "Negreta" + + # Black Condensed | Negreta Zhuštěné + instancer.updateNameTable(varfont, {"wdth": 79, "wght": 900}) + names = _get_name_records(varfont) + assert names[(1, 3, 1, 0x405)] == "Test Variable Font Negreta Zhuštěné" + assert names[(2, 3, 1, 0x405)] == "Normal" + assert names[(16, 3, 1, 0x405)] == "Test Variable Font" + assert names[(17, 3, 1, 0x405)] == "Negreta Zhuštěné" def test_sanityCheckVariableTables(varfont): From 4cd0fb80f6a67cddd4b7250006c67a8edf573aae Mon Sep 17 00:00:00 2001 From: Marc Foley Date: Thu, 8 Oct 2020 17:41:12 +0100 Subject: [PATCH 03/28] Fix typos --- Lib/fontTools/varLib/instancer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/fontTools/varLib/instancer.py b/Lib/fontTools/varLib/instancer.py index 004dad5e1..31ecc3d71 100644 --- a/Lib/fontTools/varLib/instancer.py +++ b/Lib/fontTools/varLib/instancer.py @@ -1336,7 +1336,7 @@ def axisValuesFromAxisLimits(stat, axisLimits): axisTag = axisOrder[axisValue.AxisIndex] # Add axisValue if it's used to link to another variable font if axisTag not in axisLimits and axisValue.Value == 1.0: - seen_axes.add(rec.AxisIndex) + seen_axes.add(axisValue.AxisIndex) results.append((axisValue.AxisIndex, axisValue)) if axisTag not in pinnedAxes: @@ -1344,7 +1344,7 @@ def axisValuesFromAxisLimits(stat, axisLimits): # Add axisValue if its value is in the axisLimits and the user has # pinned the axis elif axisValue.Value == axisLimits[axisTag]: - seen_axes.add(rec.AxisIndex) + seen_axes.add(axisValue.AxisIndex) results.append((axisValue.AxisIndex,axisValue)) elif axisValue.Format == 2: @@ -1426,7 +1426,7 @@ def _updateStyleRecords( NameID.SUBFAMILY_NAME: ribbiName or nametable.getName(NameID.SUBFAMILY_NAME, *lang).toUnicode() } if nonRibbiAxisValues: - nameIDs[NameID.FAMILY_NAME] = f"{currentFamilyName} {nonRibbiName}" + nameIDs[NameID.FAMILY_NAME] = f"{currentFamilyName} {nonRibbiName}".strip() nameIDs[NameID.TYPOGRAPHIC_FAMILY_NAME] = currentFamilyName nameIDs[NameID.TYPOGRAPHIC_SUBFAMILY_NAME] = f"{nonRibbiName} {ribbiName}".strip() # # Include WWS name records if there are nonWwsParticles From 78f6c2ae7538bbe18764aa1660083ba91f6eb8b2 Mon Sep 17 00:00:00 2001 From: Marc Foley Date: Fri, 9 Oct 2020 11:05:32 +0100 Subject: [PATCH 04/28] instancer: add test for partial instance name --- Tests/varLib/instancer_test.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Tests/varLib/instancer_test.py b/Tests/varLib/instancer_test.py index c6ae14666..d0aa44f3f 100644 --- a/Tests/varLib/instancer_test.py +++ b/Tests/varLib/instancer_test.py @@ -1995,6 +1995,15 @@ def test_updateNameTable_with_multilingual_names(varfont): assert names[(17, 3, 1, 0x405)] == "Negreta Zhuštěné" +def test_updateNametable_partial(varfont): + instancer.updateNameTable(varfont, {"wdth": 79, "wght": (400, 900)}) + names = _get_name_records(varfont) + assert names[(1, 3, 1, 0x409)] == "Test Variable Font Condensed" + assert names[(2, 3, 1, 0x409)] == "Regular" + assert names[(16, 3, 1, 0x409)] == "Test Variable Font" + assert names[(17, 3, 1, 0x409)] == "Condensed" #? maybe Condensed Regular? + + def test_sanityCheckVariableTables(varfont): font = ttLib.TTFont() with pytest.raises(ValueError, match="Missing required table fvar"): From 2fd934051bda1005be306dad9861f1654b148185 Mon Sep 17 00:00:00 2001 From: Marc Foley Date: Fri, 9 Oct 2020 13:00:23 +0100 Subject: [PATCH 05/28] Refactor axisValuesFromAxisLimits --- Lib/fontTools/varLib/instancer.py | 105 ++++++++++++++---------------- 1 file changed, 50 insertions(+), 55 deletions(-) diff --git a/Lib/fontTools/varLib/instancer.py b/Lib/fontTools/varLib/instancer.py index 31ecc3d71..d8c400d76 100644 --- a/Lib/fontTools/varLib/instancer.py +++ b/Lib/fontTools/varLib/instancer.py @@ -1298,64 +1298,59 @@ def instantiateVariableFont( return varfont +def axisValueIsSelected(axisValue, seeker): + if axisValue.Format == 4: + res = [] + for rec in axisValue.AxisValueRecord: + axisIndex = rec.AxisIndex + if axisIndex not in seeker: + return False + if rec.Value == seeker[axisIndex]: + res.append(True) + else: + res.append(False) + return True if all(res) else False + + axisIndex = axisValue.AxisIndex + if axisIndex not in seeker: + return False + + if axisValue.Format in (1, 3): + # Add axisValue if it's used to link to another variable font + if axisIndex not in seeker and axisValue.Value == 1.0: + return True + + elif axisValue.Value == seeker[axisIndex]: + return True + + if axisValue.Format == 2: + return True if all([ + seeker[axisIndex] >= axisValue.RangeMinValue, + seeker[axisIndex] <= axisValue.RangeMaxValue + ]) else False + return False + + def axisValuesFromAxisLimits(stat, axisLimits): - - axisRecords = stat.table.DesignAxisRecord.Axis axisValues = stat.table.AxisValueArray.AxisValue + axisRecords = stat.table.DesignAxisRecord.Axis + axisOrder = {a.AxisTag: a.AxisOrdering for a in axisRecords} + # Only check pinnedAxes for matching AxisValues + AxisValuesToFind = { + axisOrder[k]: v for k, v in axisLimits.items() \ + if isinstance(v, (float, int)) + } - format4 = [a for a in axisValues if a.Format == 4] - nonformat4 = [a for a in axisValues if a not in format4] - axisValues = format4 + nonformat4 - - axisOrder = {a.AxisOrdering: a.AxisTag for a in axisRecords} - pinnedAxes = set(k for k, v in axisLimits.items() if isinstance(v, (float, int))) - - results, seen_axes = [], set() - for axisValue in axisValues: - - # Ignore axisValue if it has ELIDABLE_AXIS_VALUE_NAME flag enabled. - # Enabling this flag will hide the axisValue in application font menus. - # TODO this is too greedy! we need to retain wght axisValues - if axisValue.Flags == 2: - continue - - if axisValue.Format == 4: - axisIndexes = set(r.AxisIndex for r in axisValue.AxisValueRecord) - if seen_axes - axisIndexes != seen_axes: - continue - # TODO fix dup appends - for rec in axisValue.AxisValueRecord: - axisTag = axisOrder[rec.AxisIndex] - if axisTag not in pinnedAxes: - continue - if rec.Value == axisLimits[axisTag]: - seen_axes.add(rec.AxisIndex) - results.append((rec.AxisIndex, axisValue)) - - elif axisValue.Format in (1, 3): - axisTag = axisOrder[axisValue.AxisIndex] - # Add axisValue if it's used to link to another variable font - if axisTag not in axisLimits and axisValue.Value == 1.0: - seen_axes.add(axisValue.AxisIndex) - results.append((axisValue.AxisIndex, axisValue)) - - if axisTag not in pinnedAxes: - continue - # Add axisValue if its value is in the axisLimits and the user has - # pinned the axis - elif axisValue.Value == axisLimits[axisTag]: - seen_axes.add(axisValue.AxisIndex) - results.append((axisValue.AxisIndex,axisValue)) - - elif axisValue.Format == 2: - axisTag = axisOrder[axisValue.AxisIndex] - if axisTag not in pinnedAxes: - continue - if axisLimits[axisTag] >= axisValue.RangeMinValue \ - and axisLimits[axisTag] <= axisValue.RangeMaxValue: - seen_axes.add(axisValue.AxisIndex) - results.append((axisValue.AxisIndex, axisValue)) - return [v for k, v in sorted(results)] + axisValues = [a for a in axisValues if axisValueIsSelected(a, AxisValuesToFind)] + axisValuesMissing = set(AxisValuesToFind) - set(a.AxisIndex for a in axisValues) + if axisValuesMissing: + # TODO better error msg + missing = [i for i in axisValuesMissing] + raise ValueError(f"Cannot find AxisValues for {missing}") + # filter out Elidable axisValues + axisValues = [a for a in axisValues if a.Flags != 2] + # TODO sort and remove duplicates so format 4 axisValues are dominant + return axisValues def updateNameTable(varfont, axisLimits): From f89c01d2d7ed2b9063b57e1c832622689af9f1f8 Mon Sep 17 00:00:00 2001 From: Marc Foley Date: Fri, 9 Oct 2020 14:14:55 +0100 Subject: [PATCH 06/28] instancer: only updateNames if axisValue with specified coord exists --- Lib/fontTools/varLib/instancer.py | 26 ++++++++++++++++++++------ Tests/varLib/instancer_test.py | 15 ++++++++++----- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/Lib/fontTools/varLib/instancer.py b/Lib/fontTools/varLib/instancer.py index d8c400d76..efe30127c 100644 --- a/Lib/fontTools/varLib/instancer.py +++ b/Lib/fontTools/varLib/instancer.py @@ -1331,25 +1331,39 @@ def axisValueIsSelected(axisValue, seeker): return False +def axisValueIndexes(axisValue): + if axisValue.Format == 4: + return [r.AxisIndex for r in axisValue.AxisValueRecord] + return [axisValue.AxisIndex] + + +def axisValuesIndexes(axisValues): + res = [] + for axisValue in axisValues: + res += axisValueIndexes(axisValue) + return res + + def axisValuesFromAxisLimits(stat, axisLimits): axisValues = stat.table.AxisValueArray.AxisValue axisRecords = stat.table.DesignAxisRecord.Axis axisOrder = {a.AxisTag: a.AxisOrdering for a in axisRecords} + axisTag = {a.AxisOrdering: a.AxisTag for a in axisRecords} # Only check pinnedAxes for matching AxisValues - AxisValuesToFind = { + axisValuesToFind = { axisOrder[k]: v for k, v in axisLimits.items() \ if isinstance(v, (float, int)) } - axisValues = [a for a in axisValues if axisValueIsSelected(a, AxisValuesToFind)] - axisValuesMissing = set(AxisValuesToFind) - set(a.AxisIndex for a in axisValues) + axisValues = [a for a in axisValues if axisValueIsSelected(a, axisValuesToFind)] + axisValuesMissing = set(axisValuesToFind) - set(axisValuesIndexes(axisValues)) if axisValuesMissing: # TODO better error msg - missing = [i for i in axisValuesMissing] - raise ValueError(f"Cannot find AxisValues for {missing}") + missing = [f"{axisTag[i]}={axisValuesToFind[i]}" for i in axisValuesMissing] + raise ValueError(f"Cannot find AxisValue for {', '.join(missing)}") # filter out Elidable axisValues axisValues = [a for a in axisValues if a.Flags != 2] - # TODO sort and remove duplicates so format 4 axisValues are dominant + # TODO sort and remove duplicates so format 4 axisValues are dominant return axisValues diff --git a/Tests/varLib/instancer_test.py b/Tests/varLib/instancer_test.py index d0aa44f3f..ec3f85cfa 100644 --- a/Tests/varLib/instancer_test.py +++ b/Tests/varLib/instancer_test.py @@ -1926,7 +1926,7 @@ def _get_name_records(varfont): def test_updateNameTable_with_registered_axes(varfont): # Regular - instancer.updateNameTable(varfont, {"wght": 400, "wdth": 100}) + instancer.updateNameTable(varfont, {"wght": 400}) names = _get_name_records(varfont) assert names[(1, 3, 1, 0x409)] == "Test Variable Font" assert names[(2, 3, 1, 0x0409)] == "Regular" @@ -1935,7 +1935,7 @@ def test_updateNameTable_with_registered_axes(varfont): assert (17, 3, 1, 0x409) not in names # Black - instancer.updateNameTable(varfont, {"wght": 900, "wdth": 100}) + instancer.updateNameTable(varfont, {"wght": 900}) names = _get_name_records(varfont) assert names[(1, 3, 1, 0x409)] == "Test Variable Font Black" assert names[(2, 3, 1, 0x409)] == "Regular" @@ -1944,7 +1944,7 @@ def test_updateNameTable_with_registered_axes(varfont): assert names[(17, 3, 1, 0x409)] == "Black" # Thin - instancer.updateNameTable(varfont, {"wght": 100, "wdth": 100}) + instancer.updateNameTable(varfont, {"wght": 100}) names = _get_name_records(varfont) assert names[(1, 3, 1, 0x409)] == "Test Variable Font Thin" assert names[(2, 3, 1, 0x409)] == "Regular" @@ -1971,7 +1971,7 @@ def test_updateNameTable_with_multilingual_names(varfont): name.setName("Zhuštěné", 279, 3, 1, 0x405) # nameID 279=Condensed STAT entry # Regular | Normal - instancer.updateNameTable(varfont, {"wdth": 100, "wght": 400}) + instancer.updateNameTable(varfont, {"wght": 400}) names = _get_name_records(varfont) assert names[(1, 3, 1, 0x405)] == "Test Variable Font" assert names[(2, 3, 1, 0x405)] == "Normal" @@ -1979,7 +1979,7 @@ def test_updateNameTable_with_multilingual_names(varfont): assert (17, 3, 1, 0x405) not in names # Black | Negreta - instancer.updateNameTable(varfont, {"wdth": 100, "wght": 900}) + instancer.updateNameTable(varfont, {"wght": 900}) names = _get_name_records(varfont) assert names[(1, 3, 1, 0x405)] == "Test Variable Font Negreta" assert names[(2, 3, 1, 0x405)] == "Normal" @@ -2004,6 +2004,11 @@ def test_updateNametable_partial(varfont): assert names[(17, 3, 1, 0x409)] == "Condensed" #? maybe Condensed Regular? +def test_updateNameTable_missing_axisValues(varfont): + with pytest.raises(ValueError, match="Cannot find AxisValue for wght=200"): + instancer.updateNameTable(varfont, {"wght": 200}) + + def test_sanityCheckVariableTables(varfont): font = ttLib.TTFont() with pytest.raises(ValueError, match="Missing required table fvar"): From b4b1ce35794c9e76c6329a8c418f4332009fdee0 Mon Sep 17 00:00:00 2001 From: Marc Foley Date: Mon, 12 Oct 2020 10:26:58 +0100 Subject: [PATCH 07/28] instancer: include attribute axisValues --- Lib/fontTools/varLib/instancer.py | 17 +++++++++-------- Tests/varLib/instancer_test.py | 23 +++++++++++++++++++++++ 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/Lib/fontTools/varLib/instancer.py b/Lib/fontTools/varLib/instancer.py index efe30127c..ec0f57dd3 100644 --- a/Lib/fontTools/varLib/instancer.py +++ b/Lib/fontTools/varLib/instancer.py @@ -1312,22 +1312,24 @@ def axisValueIsSelected(axisValue, seeker): return True if all(res) else False axisIndex = axisValue.AxisIndex - if axisIndex not in seeker: - return False if axisValue.Format in (1, 3): - # Add axisValue if it's used to link to another variable font - if axisIndex not in seeker and axisValue.Value == 1.0: + # Add axisValue if it's an attribute of a font. Font family + if axisIndex not in seeker and axisValue.Value in [0.0, 1.0]: return True - elif axisValue.Value == seeker[axisIndex]: + elif axisIndex in seeker and axisValue.Value == seeker[axisIndex]: return True if axisValue.Format == 2: return True if all([ - seeker[axisIndex] >= axisValue.RangeMinValue, - seeker[axisIndex] <= axisValue.RangeMaxValue + axisIndex in seeker and seeker[axisIndex] >= axisValue.RangeMinValue, + axisIndex in seeker and seeker[axisIndex] <= axisValue.RangeMaxValue ]) else False + + if axisIndex not in seeker: + return False + return False @@ -1358,7 +1360,6 @@ def axisValuesFromAxisLimits(stat, axisLimits): axisValues = [a for a in axisValues if axisValueIsSelected(a, axisValuesToFind)] axisValuesMissing = set(axisValuesToFind) - set(axisValuesIndexes(axisValues)) if axisValuesMissing: - # TODO better error msg missing = [f"{axisTag[i]}={axisValuesToFind[i]}" for i in axisValuesMissing] raise ValueError(f"Cannot find AxisValue for {', '.join(missing)}") # filter out Elidable axisValues diff --git a/Tests/varLib/instancer_test.py b/Tests/varLib/instancer_test.py index ec3f85cfa..f0725e1a1 100644 --- a/Tests/varLib/instancer_test.py +++ b/Tests/varLib/instancer_test.py @@ -2009,6 +2009,29 @@ def test_updateNameTable_missing_axisValues(varfont): instancer.updateNameTable(varfont, {"wght": 200}) +def test_updateNameTable_vf_with_italic_attribute(varfont): + font_link_axisValue = varfont["STAT"].table.AxisValueArray.AxisValue[4] + font_link_axisValue.Flags = 0 + font_link_axisValue.ValueNameID = 294 # Roman --> Italic + + # Italic + instancer.updateNameTable(varfont, {"wght": 400}) + names = _get_name_records(varfont) + assert names[(1, 3, 1, 0x409)] == "Test Variable Font" + assert names[(2, 3, 1, 0x409)] == "Italic" + assert (16, 3, 1, 0x405) not in names + assert (17, 3, 1, 0x405) not in names + + # Black Condensed Italic + instancer.updateNameTable(varfont, {"wdth": 79, "wght": 900}) + names = _get_name_records(varfont) + assert names[(1, 3, 1, 0x409)] == "Test Variable Font Black Condensed" + assert names[(2, 3, 1, 0x409)] == "Italic" + assert names[(6, 3, 1, 0x409)] == "TestVariableFont-BlackCondensedItalic" + assert names[(16, 3, 1, 0x409)] == "Test Variable Font" + assert names[(17, 3, 1, 0x409)] == "Black Condensed Italic" + + def test_sanityCheckVariableTables(varfont): font = ttLib.TTFont() with pytest.raises(ValueError, match="Missing required table fvar"): From 0b639c2979078aaeb43ebe3cc8a6b1ff77b56018 Mon Sep 17 00:00:00 2001 From: Marc Foley Date: Mon, 12 Oct 2020 10:28:48 +0100 Subject: [PATCH 08/28] instancer: use bit mask for axisValue flags --- Lib/fontTools/varLib/instancer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Lib/fontTools/varLib/instancer.py b/Lib/fontTools/varLib/instancer.py index ec0f57dd3..042309d2a 100644 --- a/Lib/fontTools/varLib/instancer.py +++ b/Lib/fontTools/varLib/instancer.py @@ -136,6 +136,8 @@ class NameID(IntEnum): TYPOGRAPHIC_FAMILY_NAME = 16 TYPOGRAPHIC_SUBFAMILY_NAME = 17 +ELIDABLE_AXIS_VALUE_NAME = 2 + def instantiateTupleVariationStore( variations, axisLimits, origCoords=None, endPts=None @@ -1363,7 +1365,7 @@ def axisValuesFromAxisLimits(stat, axisLimits): missing = [f"{axisTag[i]}={axisValuesToFind[i]}" for i in axisValuesMissing] raise ValueError(f"Cannot find AxisValue for {', '.join(missing)}") # filter out Elidable axisValues - axisValues = [a for a in axisValues if a.Flags != 2] + axisValues = [a for a in axisValues if a.Flags & ELIDABLE_AXIS_VALUE_NAME != 2] # TODO sort and remove duplicates so format 4 axisValues are dominant return axisValues From 69c86679824cf60bdeccb3bb71b743dc4bb87168 Mon Sep 17 00:00:00 2001 From: Marc Foley Date: Mon, 12 Oct 2020 12:37:11 +0100 Subject: [PATCH 09/28] instancer: sort axisValues so format 4 are dominant for constructing names --- Lib/fontTools/varLib/instancer.py | 25 +++++++++++++++++++++++-- Tests/varLib/instancer_test.py | 27 +++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/Lib/fontTools/varLib/instancer.py b/Lib/fontTools/varLib/instancer.py index 042309d2a..d88cb38c9 100644 --- a/Lib/fontTools/varLib/instancer.py +++ b/Lib/fontTools/varLib/instancer.py @@ -1366,8 +1366,29 @@ def axisValuesFromAxisLimits(stat, axisLimits): raise ValueError(f"Cannot find AxisValue for {', '.join(missing)}") # filter out Elidable axisValues axisValues = [a for a in axisValues if a.Flags & ELIDABLE_AXIS_VALUE_NAME != 2] - # TODO sort and remove duplicates so format 4 axisValues are dominant - return axisValues + return sortedAxisValues(axisValues) + + +def sortedAxisValues(axisValues): + # Sort and remove duplicates so format 4 axisValues are dominant + results, seenAxes = [], set() + format4 = sorted( + [a for a in axisValues if a.Format == 4], + key=lambda k: len(k.AxisValueRecord), reverse=True + ) + nonFormat4 = [a for a in axisValues if a not in format4] + + for axisValue in format4: + axes = set([r.AxisIndex for r in axisValue.AxisValueRecord]) + if seenAxes - axes == seenAxes: + seenAxes |= axes + results.append((tuple(axes), axisValue)) + + for axisValue in nonFormat4: + axisIndex = axisValue.AxisIndex + if axisIndex not in seenAxes: + results.append(((axisIndex,), axisValue)) + return [v for k, v in sorted(results)] def updateNameTable(varfont, axisLimits): diff --git a/Tests/varLib/instancer_test.py b/Tests/varLib/instancer_test.py index f0725e1a1..d148a8c45 100644 --- a/Tests/varLib/instancer_test.py +++ b/Tests/varLib/instancer_test.py @@ -2032,6 +2032,33 @@ def test_updateNameTable_vf_with_italic_attribute(varfont): assert names[(17, 3, 1, 0x409)] == "Black Condensed Italic" +def test_updateNameTable_format4_axisValues(varfont): + # format 4 axisValues should dominate the other axisValues + stat = varfont["STAT"].table + + axisValue = otTables.AxisValue() + axisValue.Format = 4 + axisValue.Flags = 0 + varfont["name"].setName("Dominant Value", 297, 3, 1, 0x409) + axisValue.ValueNameID = 297 + axisValue.AxisValueRecord = [] + for tag, value in (("wght", 900), ("wdth", 79)): + rec = otTables.AxisValueRecord() + rec.AxisIndex = next( + i for i, a in enumerate(stat.DesignAxisRecord.Axis) if a.AxisTag == tag + ) + rec.Value = value + axisValue.AxisValueRecord.append(rec) + stat.AxisValueArray.AxisValue.append(axisValue) + + instancer.updateNameTable(varfont, {"wdth": 79, "wght": 900}) + names = _get_name_records(varfont) + assert names[(1, 3, 1, 0x409)] == "Test Variable Font Dominant Value" + assert names[(2, 3, 1, 0x409)] == "Regular" + assert names[(16, 3, 1, 0x409)] == "Test Variable Font" + assert names[(17, 3, 1, 0x409)] == "Dominant Value" + + def test_sanityCheckVariableTables(varfont): font = ttLib.TTFont() with pytest.raises(ValueError, match="Missing required table fvar"): From ce1d8a9955de307ac03036fe6e2d6222044dddaa Mon Sep 17 00:00:00 2001 From: Marc Foley Date: Mon, 12 Oct 2020 15:54:28 +0100 Subject: [PATCH 10/28] instancer: Add axisDefaults to axisLimits --- Lib/fontTools/varLib/instancer.py | 10 +++++++--- Tests/varLib/data/PartialInstancerTest-VF.ttx | 12 ++++++++++++ Tests/varLib/instancer_test.py | 6 +++--- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/Lib/fontTools/varLib/instancer.py b/Lib/fontTools/varLib/instancer.py index d88cb38c9..4943a5463 100644 --- a/Lib/fontTools/varLib/instancer.py +++ b/Lib/fontTools/varLib/instancer.py @@ -1329,9 +1329,6 @@ def axisValueIsSelected(axisValue, seeker): axisIndex in seeker and seeker[axisIndex] <= axisValue.RangeMaxValue ]) else False - if axisIndex not in seeker: - return False - return False @@ -1395,8 +1392,15 @@ def updateNameTable(varfont, axisLimits): if "STAT" not in varfont: raise ValueError("Cannot update name table since there is no STAT table.") stat = varfont['STAT'] + fvar = varfont['fvar'] nametable = varfont["name"] + # add default axis values if they are missing from axisLimits + fvarDefaults = {a.axisTag: a.defaultValue for a in fvar.axes} + for k, v in fvarDefaults.items(): + if k not in axisLimits: + axisLimits[k] = v + selectedAxisValues = axisValuesFromAxisLimits(stat, axisLimits) _updateNameRecords(varfont, nametable, selectedAxisValues) diff --git a/Tests/varLib/data/PartialInstancerTest-VF.ttx b/Tests/varLib/data/PartialInstancerTest-VF.ttx index 92540e03e..268b5068b 100644 --- a/Tests/varLib/data/PartialInstancerTest-VF.ttx +++ b/Tests/varLib/data/PartialInstancerTest-VF.ttx @@ -479,6 +479,9 @@ TestVariableFont-XCdBd + + Normal + @@ -764,6 +767,15 @@ + + + + + + + + + diff --git a/Tests/varLib/instancer_test.py b/Tests/varLib/instancer_test.py index d148a8c45..a794f79d2 100644 --- a/Tests/varLib/instancer_test.py +++ b/Tests/varLib/instancer_test.py @@ -1209,8 +1209,8 @@ class InstantiateSTATTest(object): @pytest.mark.parametrize( "location, expected", [ - ({"wght": 400}, ["Regular", "Condensed", "Upright"]), - ({"wdth": 100}, ["Thin", "Regular", "Black", "Upright"]), + ({"wght": 400}, ["Regular", "Condensed", "Upright", "Normal"]), + ({"wdth": 100}, ["Thin", "Regular", "Black", "Upright", "Normal"]), ], ) def test_pin_and_drop_axis(self, varfont, location, expected): @@ -1344,7 +1344,7 @@ class InstantiateSTATTest(object): def test_pruningUnusedNames(varfont): varNameIDs = instancer.getVariationNameIDs(varfont) - assert varNameIDs == set(range(256, 296 + 1)) + assert varNameIDs == set(range(256, 297 + 1)) fvar = varfont["fvar"] stat = varfont["STAT"].table From b3284750720ea6cc1721d01f491d7924fe6fd083 Mon Sep 17 00:00:00 2001 From: Marc Foley Date: Mon, 12 Oct 2020 18:14:27 +0100 Subject: [PATCH 11/28] wip instancer: update uniqueID --- Lib/fontTools/varLib/instancer.py | 46 +++++++++++++++++++++++++------ Tests/varLib/instancer_test.py | 8 ++++++ 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/Lib/fontTools/varLib/instancer.py b/Lib/fontTools/varLib/instancer.py index 4943a5463..e5078fefb 100644 --- a/Lib/fontTools/varLib/instancer.py +++ b/Lib/fontTools/varLib/instancer.py @@ -1369,6 +1369,7 @@ def axisValuesFromAxisLimits(stat, axisLimits): def sortedAxisValues(axisValues): # Sort and remove duplicates so format 4 axisValues are dominant results, seenAxes = [], set() + # ensure format4 axes with the most AxisValueRecords are first format4 = sorted( [a for a in axisValues if a.Format == 4], key=lambda k: len(k.AxisValueRecord), reverse=True @@ -1392,14 +1393,15 @@ def updateNameTable(varfont, axisLimits): if "STAT" not in varfont: raise ValueError("Cannot update name table since there is no STAT table.") stat = varfont['STAT'] - fvar = varfont['fvar'] nametable = varfont["name"] # add default axis values if they are missing from axisLimits - fvarDefaults = {a.axisTag: a.defaultValue for a in fvar.axes} - for k, v in fvarDefaults.items(): - if k not in axisLimits: - axisLimits[k] = v + if 'fvar' in varfont: + fvar = varfont['fvar'] + fvarDefaults = {a.axisTag: a.defaultValue for a in fvar.axes} + for k, v in fvarDefaults.items(): + if k not in axisLimits: + axisLimits[k] = v selectedAxisValues = axisValuesFromAxisLimits(stat, axisLimits) _updateNameRecords(varfont, nametable, selectedAxisValues) @@ -1419,6 +1421,7 @@ def _updateNameRecords(varfont, nametable, axisValues): nametblLangs = set((r.platformID, r.platEncID, r.langID) for r in nametable.names) for lang in nametblLangs: _updateStyleRecords( + varfont, nametable, ribbiAxisValues, nonRibbiAxisValues, @@ -1437,6 +1440,7 @@ def _ribbiAxisValues(nametable, axisValues): def _updateStyleRecords( + varfont, nametable, ribbiAxisValues, nonRibbiAxisValues, @@ -1480,13 +1484,39 @@ def _updateStyleRecords( nameIDs[NameID.FULL_FONT_NAME] = f"{newFamilyName} {newStyleName}" nameIDs[NameID.POSTSCRIPT_NAME] = f"{newFamilyName.replace(' ', '')}-{newStyleName.replace(' ', '')}" - # Update uniqueID - # TODO - # versionRecord = nametable.getName(5, 3, 1, 0x409) + nameIDs[NameID.UNIQUE_FONT_IDENTIFIER] = _uniqueIdRecord(varfont, lang, nameIDs) + for nameID, string in nameIDs.items(): + if not string: + continue nametable.setName(string, nameID, *lang) +def _uniqueIdRecord(varfont, lang, nameIDs): + name = varfont['name'] + record = name.getName(NameID.UNIQUE_FONT_IDENTIFIER, *lang) + if not record: + return None + + def isSubString(string1, string2): + if string2 in string1: + return True + return False + + # Check if full name and postscript name are a substring + for nameID in (4, 6): + nameRecord = name.getName(nameID, *lang) + if not nameRecord: + continue + if isSubString(record.toUnicode(), nameRecord.toUnicode()): + return record.toUnicode().replace( + nameRecord.toUnicode(), + nameIDs[nameRecord.nameID] + ) + # TODO (M Foley) Construct new uniqueID if full name or postscript names are not subsets + return None + + def splitAxisLocationAndRanges(axisLimits, rangeType=AxisRange): location, axisRanges = {}, {} for axisTag, value in axisLimits.items(): diff --git a/Tests/varLib/instancer_test.py b/Tests/varLib/instancer_test.py index a794f79d2..854a4b4fb 100644 --- a/Tests/varLib/instancer_test.py +++ b/Tests/varLib/instancer_test.py @@ -1930,6 +1930,7 @@ def test_updateNameTable_with_registered_axes(varfont): names = _get_name_records(varfont) assert names[(1, 3, 1, 0x409)] == "Test Variable Font" assert names[(2, 3, 1, 0x0409)] == "Regular" + assert names[(3, 3, 1, 0x0409)] == "2.001;GOOG;TestVariableFont-Regular" assert names[(6, 3, 1, 0x409)] == "TestVariableFont-Regular" assert (16, 3, 1, 0x409) not in names assert (17, 3, 1, 0x409) not in names @@ -1939,6 +1940,7 @@ def test_updateNameTable_with_registered_axes(varfont): names = _get_name_records(varfont) assert names[(1, 3, 1, 0x409)] == "Test Variable Font Black" assert names[(2, 3, 1, 0x409)] == "Regular" + assert names[(3, 3, 1, 0x0409)] == "2.001;GOOG;TestVariableFont-Black" assert names[(6, 3, 1, 0x409)] == "TestVariableFont-Black" assert names[(16, 3, 1, 0x409)] == "Test Variable Font" assert names[(17, 3, 1, 0x409)] == "Black" @@ -1948,6 +1950,7 @@ def test_updateNameTable_with_registered_axes(varfont): names = _get_name_records(varfont) assert names[(1, 3, 1, 0x409)] == "Test Variable Font Thin" assert names[(2, 3, 1, 0x409)] == "Regular" + assert names[(3, 3, 1, 0x0409)] == "2.001;GOOG;TestVariableFont-Thin" assert names[(6, 3, 1, 0x409)] == "TestVariableFont-Thin" assert names[(16, 3, 1, 0x409)] == "Test Variable Font" assert names[(17, 3, 1, 0x409)] == "Thin" @@ -1957,6 +1960,7 @@ def test_updateNameTable_with_registered_axes(varfont): names = _get_name_records(varfont) assert names[(1, 3, 1, 0x409)] == "Test Variable Font Thin Condensed" assert names[(2, 3, 1, 0x409)] == "Regular" + assert names[(3, 3, 1, 0x0409)] == "2.001;GOOG;TestVariableFont-ThinCondensed" assert names[(6, 3, 1, 0x409)] == "TestVariableFont-ThinCondensed" assert names[(16, 3, 1, 0x409)] == "Test Variable Font" assert names[(17, 3, 1, 0x409)] == "Thin Condensed" @@ -1975,6 +1979,7 @@ def test_updateNameTable_with_multilingual_names(varfont): names = _get_name_records(varfont) assert names[(1, 3, 1, 0x405)] == "Test Variable Font" assert names[(2, 3, 1, 0x405)] == "Normal" + assert (3, 3, 1, 0x405) not in names assert (16, 3, 1, 0x405) not in names assert (17, 3, 1, 0x405) not in names @@ -1983,6 +1988,7 @@ def test_updateNameTable_with_multilingual_names(varfont): names = _get_name_records(varfont) assert names[(1, 3, 1, 0x405)] == "Test Variable Font Negreta" assert names[(2, 3, 1, 0x405)] == "Normal" + assert (3, 3, 1, 0x405) not in names assert names[(16, 3, 1, 0x405)] == "Test Variable Font" assert names[(17, 3, 1, 0x405)] == "Negreta" @@ -1991,6 +1997,7 @@ def test_updateNameTable_with_multilingual_names(varfont): names = _get_name_records(varfont) assert names[(1, 3, 1, 0x405)] == "Test Variable Font Negreta Zhuštěné" assert names[(2, 3, 1, 0x405)] == "Normal" + assert (3, 3, 1, 0x405) not in names assert names[(16, 3, 1, 0x405)] == "Test Variable Font" assert names[(17, 3, 1, 0x405)] == "Negreta Zhuštěné" @@ -2000,6 +2007,7 @@ def test_updateNametable_partial(varfont): names = _get_name_records(varfont) assert names[(1, 3, 1, 0x409)] == "Test Variable Font Condensed" assert names[(2, 3, 1, 0x409)] == "Regular" + assert (3, 3, 1, 0x405) not in names assert names[(16, 3, 1, 0x409)] == "Test Variable Font" assert names[(17, 3, 1, 0x409)] == "Condensed" #? maybe Condensed Regular? From 9a72311d197a10e38e8ae2001041fae53071b8e6 Mon Sep 17 00:00:00 2001 From: Marc Foley Date: Wed, 14 Oct 2020 11:56:18 +0100 Subject: [PATCH 12/28] instancer: refactor updateNameTable --- Lib/fontTools/varLib/instancer.py | 514 +++++++++++++++++------------- Tests/varLib/instancer_test.py | 2 +- 2 files changed, 290 insertions(+), 226 deletions(-) diff --git a/Lib/fontTools/varLib/instancer.py b/Lib/fontTools/varLib/instancer.py index e5078fefb..c72ff48f7 100644 --- a/Lib/fontTools/varLib/instancer.py +++ b/Lib/fontTools/varLib/instancer.py @@ -127,6 +127,7 @@ class OverlapMode(IntEnum): KEEP_AND_SET_FLAGS = 1 REMOVE = 2 + class NameID(IntEnum): FAMILY_NAME = 1 SUBFAMILY_NAME = 2 @@ -136,6 +137,7 @@ class NameID(IntEnum): TYPOGRAPHIC_FAMILY_NAME = 16 TYPOGRAPHIC_SUBFAMILY_NAME = 17 + ELIDABLE_AXIS_VALUE_NAME = 2 @@ -1110,6 +1112,280 @@ def pruningUnusedNames(varfont): del varfont["ltag"] +def updateNameTable(varfont, axisLimits): + """Update an instatiated variable font's name table using the STAT + table's Axis Value Tables. + + To establish which Axis Value Tables are needed, we first remove all + tables whose Value's are not in the axisLimits dictionary. We then + remove all tables which have Flag 2 enabled (ELIDABLE_AXIS_VALUE_NAME). + Finally, we remove duplicates and ensure Format 4 tables preside over + the other formats. + + The updated nametable will conform to the R/I/B/BI naming model. + """ + if "STAT" not in varfont: + raise ValueError("Cannot update name table since there is no STAT table.") + stat = varfont["STAT"] + fvar = varfont["fvar"] + + # The updated name table must reflect the 'zero origin' of the font. + # If a user is instantiating a partial font, we will populate the + # unpinned axes with their default values. + fvarDefaults = {a.axisTag: a.defaultValue for a in fvar.axes} + axisCoords = axisLimits + for axisTag, val in fvarDefaults.items(): + if axisTag not in axisCoords: + axisCoords[axisTag] = val + elif isinstance(axisCoords[axisTag], tuple): + axisCoords[axisTag] = val + + axisValueTables = _axisValueTablesFromAxisCoords(stat, axisCoords) + _updateNameRecords(varfont, axisValueTables) + + +def _axisValueTablesFromAxisCoords(stat, axisCoords): + axisValueTables = stat.table.AxisValueArray.AxisValue + axisRecords = stat.table.DesignAxisRecord.Axis + axisRecordIndex = {a.AxisTag: a.AxisOrdering for a in axisRecords} + axisRecordTag = {a.AxisOrdering: a.AxisTag for a in axisRecords} + + axisValuesToFind = { + axisRecordIndex[axisTag]: val for axisTag, val in axisCoords.items() + } + axisValueTables = [ + v for v in axisValueTables if _axisValueInAxisCoords(v, axisValuesToFind) + ] + axisValueTablesMissing = set(axisValuesToFind) - axisValueRecordsIndexes( + axisValueTables + ) + if axisValueTablesMissing: + missing = ", ".join( + f"{axisRecordTag[i]}={axisValuesToFind[i]}" for i in axisValueTablesMissing + ) + raise ValueError(f"Cannot find Axis Value Tables {missing}") + # remove axis Value Tables which have Elidable_AXIS_VALUE_NAME flag set + axisValueTables = [ + v for v in axisValueTables if v.Flags & ELIDABLE_AXIS_VALUE_NAME != 2 + ] + return _sortedAxisValues(axisValueTables) + + +def _axisValueInAxisCoords(axisValueTable, axisCoords): + if axisValueTable.Format == 4: + res = [] + for rec in axisValueTable.AxisValueRecord: + axisIndex = rec.AxisIndex + if axisIndex not in axisCoords: + return False + if rec.Value == axisCoords[axisIndex]: + res.append(True) + else: + res.append(False) + return True if all(res) else False + + axisIndex = axisValueTable.AxisIndex + + if axisValueTable.Format in (1, 3): + # A variable font can have additional axes that are not implemented as + # dynamic-variation axes in the fvar table, but that are + # relevant for the font or the family of which it is a member. This + # condition will include them. + # A common scenario is a family which consists of two variable fonts, + # one for Roman styles, the other for Italic styles. Both fonts have a + # weight axis. In order to establish a relationship between the fonts, + # an Italic Axis Record is created for both fonts. In the Roman font, + # an Axis Value Table is added to the Italic Axis Record which has the + # name "Roman" and its Value is set to 0.0, it also includes link + # Value of 1. In the Italic font, an Axis Value Table is also added + # to the Italic Axis Record which has the name "Italic", its Value set + # to 1.0. + if axisIndex not in axisCoords and axisValueTable.Value in (0.0, 1.0): + return True + + elif axisIndex in axisCoords and axisValueTable.Value == axisCoords[axisIndex]: + return True + + if axisValueTable.Format == 2: + return ( + True + if all( + [ + axisIndex in axisCoords + and axisCoords[axisIndex] >= axisValueTable.RangeMinValue, + axisIndex in axisCoords + and axisCoords[axisIndex] <= axisValueTable.RangeMaxValue, + ] + ) + else False + ) + return False + + +def axisValueRecordsIndexes(axisValueTables): + res = set() + for val in axisValueTables: + res |= axisValueRecordIndexes(val) + return res + + +def axisValueRecordIndexes(axisValueTable): + if axisValueTable.Format == 4: + return set(r.AxisIndex for r in axisValueTable.AxisValueRecord) + return set([axisValueTable.AxisIndex]) + + +def _sortedAxisValues(axisValueTables): + # Sort and remove duplicates ensuring that format 4 axis Value Tables + # are dominant + results = [] + seenAxes = set() + # sort format 4 axes so the tables with the most AxisValueRecords + # are first + format4 = sorted( + [v for v in axisValueTables if v.Format == 4], + key=lambda v: len(v.AxisValueRecord), + reverse=True, + ) + nonFormat4 = [v for v in axisValueTables if v not in format4] + + for val in format4: + axisIndexes = axisValueRecordIndexes(val) + if bool(seenAxes & axisIndexes) == False: + seenAxes |= axisIndexes + results.append((tuple(axisIndexes), val)) + + for val in nonFormat4: + axisIndex = val.AxisIndex + if axisIndex not in seenAxes: + seenAxes.add(axisIndex) + results.append(((axisIndex,), val)) + return [axisValueTable for _, axisValueTable in sorted(results)] + + +def _updateNameRecords(varfont, axisValueTables): + # Update nametable based on the axisValues using the R/I/B/BI model. + nametable = varfont["name"] + + ribbiAxisValues = _ribbiAxisValueTables(nametable, axisValueTables) + nonRibbiAxisValues = [v for v in axisValueTables if v not in ribbiAxisValues] + + nameTablePlatEncLangs = set( + (r.platformID, r.platEncID, r.langID) for r in nametable.names + ) + for platEncLang in nameTablePlatEncLangs: + _updateStyleRecords( + varfont, + nametable, + ribbiAxisValues, + nonRibbiAxisValues, + platEncLang, + ) + + +def _ribbiAxisValueTables(nametable, axisValueTables): + engNameRecords = any([r for r in nametable.names if r.langID == 0x409]) + if not engNameRecords: + raise ValueError( + f"Canot determine if there are RIBBI Axis Value Tables " + "since there are no name table Records which have " + "platformID=3, platEncID=1, langID=0x409" + ) + return [ + v + for v in axisValueTables + if nametable.getName(v.ValueNameID, 3, 1, 0x409).toUnicode() + in ("Regular", "Italic", "Bold", "Bold Italic") + ] + + +def _updateStyleRecords( + varfont, nametable, ribbiAxisValues, nonRibbiAxisValues, platEncLang=(3, 1, 0x409) +): + currentFamilyName = nametable.getName( + NameID.TYPOGRAPHIC_FAMILY_NAME, *platEncLang + ) or nametable.getName(NameID.FAMILY_NAME, *platEncLang) + + currentStyleName = nametable.getName( + NameID.TYPOGRAPHIC_SUBFAMILY_NAME, *platEncLang + ) or nametable.getName(NameID.SUBFAMILY_NAME, *platEncLang) + + if not currentFamilyName or not currentStyleName: + # Since no family name or style name entries were found, we cannot + # update this set of name Records. + return + + currentFamilyName = currentFamilyName.toUnicode() + currentStyleName = currentStyleName.toUnicode() + + ribbiName = " ".join( + nametable.getName(a.ValueNameID, *platEncLang).toUnicode() + for a in ribbiAxisValues + ) + nonRibbiName = " ".join( + nametable.getName(a.ValueNameID, *platEncLang).toUnicode() + for a in nonRibbiAxisValues + ) + + nameIDs = { + NameID.FAMILY_NAME: currentFamilyName, + # TODO (M Foley) what about Elidable fallback name instead? + NameID.SUBFAMILY_NAME: ribbiName + or nametable.getName(NameID.SUBFAMILY_NAME, *platEncLang).toUnicode(), + } + if nonRibbiAxisValues: + nameIDs[NameID.FAMILY_NAME] = f"{currentFamilyName} {nonRibbiName}".strip() + nameIDs[NameID.TYPOGRAPHIC_FAMILY_NAME] = currentFamilyName + nameIDs[ + NameID.TYPOGRAPHIC_SUBFAMILY_NAME + ] = f"{nonRibbiName} {ribbiName}".strip() + + newFamilyName = nameIDs.get(NameID.TYPOGRAPHIC_FAMILY_NAME) or nameIDs.get( + NameID.FAMILY_NAME + ) + newStyleName = nameIDs.get(NameID.TYPOGRAPHIC_SUBFAMILY_NAME) or nameIDs.get( + NameID.SUBFAMILY_NAME + ) + + nameIDs[NameID.FULL_FONT_NAME] = f"{newFamilyName} {newStyleName}" + # TODO (M Foley) implement Adobe PS naming for VFs + nameIDs[ + NameID.POSTSCRIPT_NAME + ] = f"{newFamilyName.replace(' ', '')}-{newStyleName.replace(' ', '')}" + nameIDs[NameID.UNIQUE_FONT_IDENTIFIER] = _updateUniqueIdNameRecord( + varfont, nameIDs, platEncLang + ) + + for nameID, string in nameIDs.items(): + if not string: + continue + nametable.setName(string, nameID, *platEncLang) + + +def _updateUniqueIdNameRecord(varfont, nameIDs, platEncLang): + name = varfont["name"] + record = name.getName(NameID.UNIQUE_FONT_IDENTIFIER, *platEncLang) + if not record: + return None + + def isSubString(string1, string2): + if string2 in string1: + return True + return False + + # Check if full name and postscript name are a substring + for nameID in (4, 6): + nameRecord = name.getName(nameID, *platEncLang) + if not nameRecord: + continue + if isSubString(record.toUnicode(), nameRecord.toUnicode()): + return record.toUnicode().replace( + nameRecord.toUnicode(), nameIDs[nameRecord.nameID] + ) + # TODO (M Foley) Construct new uniqueID if full name or postscript names are not subsets + return None + + def setMacOverlapFlags(glyfTable): flagOverlapCompound = _g_l_y_f.OVERLAP_COMPOUND flagOverlapSimple = _g_l_y_f.flagOverlapSimple @@ -1198,7 +1474,7 @@ def instantiateVariableFont( inplace=False, optimize=True, overlap=OverlapMode.KEEP_AND_SET_FLAGS, - update_nametable=False + updateFontNames=False, ): """Instantiate variable font, either fully or partially. @@ -1231,6 +1507,11 @@ def instantiateVariableFont( contours and components, you can pass OverlapMode.REMOVE. Note that this requires the skia-pathops package (available to pip install). The overlap parameter only has effect when generating full static instances. + updateFontNames (bool): if True, update the instantiated font's nametable using + the Axis Value Tables from the STAT table. The name table will be updated so + it conforms to the R/I/B/BI model. If the STAT table is missing or + an Axis Value table is missing for a given coordinate, an Error will be + raised. """ # 'overlap' used to be bool and is now enum; for backward compat keep accepting bool overlap = OverlapMode(int(overlap)) @@ -1246,6 +1527,10 @@ def instantiateVariableFont( if not inplace: varfont = deepcopy(varfont) + if updateFontNames: + log.info("Updating nametable") + updateNameTable(varfont, axisLimits) + if "gvar" in varfont: instantiateGvar(varfont, normalizedLimits, optimize=optimize) @@ -1284,10 +1569,6 @@ def instantiateVariableFont( log.info("Removing overlaps from glyf table") removeOverlaps(varfont) - if update_nametable: - log.info("Updating nametable") - updateNameTable(varfont, axisLimits) - varLib.set_default_weight_width_slant( varfont, location={ @@ -1300,223 +1581,6 @@ def instantiateVariableFont( return varfont -def axisValueIsSelected(axisValue, seeker): - if axisValue.Format == 4: - res = [] - for rec in axisValue.AxisValueRecord: - axisIndex = rec.AxisIndex - if axisIndex not in seeker: - return False - if rec.Value == seeker[axisIndex]: - res.append(True) - else: - res.append(False) - return True if all(res) else False - - axisIndex = axisValue.AxisIndex - - if axisValue.Format in (1, 3): - # Add axisValue if it's an attribute of a font. Font family - if axisIndex not in seeker and axisValue.Value in [0.0, 1.0]: - return True - - elif axisIndex in seeker and axisValue.Value == seeker[axisIndex]: - return True - - if axisValue.Format == 2: - return True if all([ - axisIndex in seeker and seeker[axisIndex] >= axisValue.RangeMinValue, - axisIndex in seeker and seeker[axisIndex] <= axisValue.RangeMaxValue - ]) else False - - return False - - -def axisValueIndexes(axisValue): - if axisValue.Format == 4: - return [r.AxisIndex for r in axisValue.AxisValueRecord] - return [axisValue.AxisIndex] - - -def axisValuesIndexes(axisValues): - res = [] - for axisValue in axisValues: - res += axisValueIndexes(axisValue) - return res - - -def axisValuesFromAxisLimits(stat, axisLimits): - axisValues = stat.table.AxisValueArray.AxisValue - axisRecords = stat.table.DesignAxisRecord.Axis - axisOrder = {a.AxisTag: a.AxisOrdering for a in axisRecords} - axisTag = {a.AxisOrdering: a.AxisTag for a in axisRecords} - # Only check pinnedAxes for matching AxisValues - axisValuesToFind = { - axisOrder[k]: v for k, v in axisLimits.items() \ - if isinstance(v, (float, int)) - } - - axisValues = [a for a in axisValues if axisValueIsSelected(a, axisValuesToFind)] - axisValuesMissing = set(axisValuesToFind) - set(axisValuesIndexes(axisValues)) - if axisValuesMissing: - missing = [f"{axisTag[i]}={axisValuesToFind[i]}" for i in axisValuesMissing] - raise ValueError(f"Cannot find AxisValue for {', '.join(missing)}") - # filter out Elidable axisValues - axisValues = [a for a in axisValues if a.Flags & ELIDABLE_AXIS_VALUE_NAME != 2] - return sortedAxisValues(axisValues) - - -def sortedAxisValues(axisValues): - # Sort and remove duplicates so format 4 axisValues are dominant - results, seenAxes = [], set() - # ensure format4 axes with the most AxisValueRecords are first - format4 = sorted( - [a for a in axisValues if a.Format == 4], - key=lambda k: len(k.AxisValueRecord), reverse=True - ) - nonFormat4 = [a for a in axisValues if a not in format4] - - for axisValue in format4: - axes = set([r.AxisIndex for r in axisValue.AxisValueRecord]) - if seenAxes - axes == seenAxes: - seenAxes |= axes - results.append((tuple(axes), axisValue)) - - for axisValue in nonFormat4: - axisIndex = axisValue.AxisIndex - if axisIndex not in seenAxes: - results.append(((axisIndex,), axisValue)) - return [v for k, v in sorted(results)] - - -def updateNameTable(varfont, axisLimits): - if "STAT" not in varfont: - raise ValueError("Cannot update name table since there is no STAT table.") - stat = varfont['STAT'] - nametable = varfont["name"] - - # add default axis values if they are missing from axisLimits - if 'fvar' in varfont: - fvar = varfont['fvar'] - fvarDefaults = {a.axisTag: a.defaultValue for a in fvar.axes} - for k, v in fvarDefaults.items(): - if k not in axisLimits: - axisLimits[k] = v - - selectedAxisValues = axisValuesFromAxisLimits(stat, axisLimits) - _updateNameRecords(varfont, nametable, selectedAxisValues) - - -def _updateNameRecords(varfont, nametable, axisValues): - # Update nametable based on the axisValues - # using the R/I/B/BI and WWS models. - engNameRecords = any([r for r in nametable.names if r.langID == 0x409]) - if not engNameRecords: - # TODO (Marc F) improve error msg - raise ValueError("No English namerecords") - - ribbiAxisValues = _ribbiAxisValues(nametable, axisValues) - nonRibbiAxisValues = [av for av in axisValues if av not in ribbiAxisValues] - - nametblLangs = set((r.platformID, r.platEncID, r.langID) for r in nametable.names) - for lang in nametblLangs: - _updateStyleRecords( - varfont, - nametable, - ribbiAxisValues, - nonRibbiAxisValues, - lang, - ) - - -def _ribbiAxisValues(nametable, axisValues): - ribbiStyles = frozenset(["Regular", "Italic", "Bold", "Bold Italic"]) - res = [] - for axisValue in axisValues: - name = nametable.getName(axisValue.ValueNameID, 3, 1, 0x409).toUnicode() - if name in ribbiStyles: - res.append(axisValue) - return res - - -def _updateStyleRecords( - varfont, - nametable, - ribbiAxisValues, - nonRibbiAxisValues, - lang=(3, 1, 0x409) -): -# wwsAxes = frozenset(["wght", "wdth", "ital"]) - currentFamilyName = nametable.getName(NameID.TYPOGRAPHIC_FAMILY_NAME, *lang) or \ - nametable.getName(NameID.FAMILY_NAME, *lang) - - currentStyleName = nametable.getName(NameID.TYPOGRAPHIC_SUBFAMILY_NAME, *lang) or \ - nametable.getName(NameID.SUBFAMILY_NAME, *lang) - # TODO cleanup - if not currentFamilyName or not currentStyleName: - print(f"Cannot update {lang} since it's missing a familyName nameID 1 or subFamilyName nameID 2 entry") - return - currentFamilyName = currentFamilyName.toUnicode() - currentStyleName = currentStyleName.toUnicode() - - ribbiName = " ".join([nametable.getName(a.ValueNameID, *lang).toUnicode() for a in ribbiAxisValues]) - nonRibbiName = " ".join([nametable.getName(a.ValueNameID, *lang).toUnicode() for a in nonRibbiAxisValues]) - - nameIDs = { - NameID.FAMILY_NAME: currentFamilyName, - NameID.SUBFAMILY_NAME: ribbiName or nametable.getName(NameID.SUBFAMILY_NAME, *lang).toUnicode() - } - if nonRibbiAxisValues: - nameIDs[NameID.FAMILY_NAME] = f"{currentFamilyName} {nonRibbiName}".strip() - nameIDs[NameID.TYPOGRAPHIC_FAMILY_NAME] = currentFamilyName - nameIDs[NameID.TYPOGRAPHIC_SUBFAMILY_NAME] = f"{nonRibbiName} {ribbiName}".strip() -# # Include WWS name records if there are nonWwsParticles -# if nonWwsParticles: -# nameIDs[21] = f"{currentFamilyName} {' '.join(nonWwsParticles)}" -# nameIDs[22] = " ".join(wwsParticles) -# # Enable fsSelection bit 8 (WWS) -# varfont['OS/2'].fsSelection |= (1 << 8) -# - newFamilyName = nameIDs.get(NameID.TYPOGRAPHIC_FAMILY_NAME) or \ - nameIDs.get(NameID.FAMILY_NAME) - newStyleName = nameIDs.get(NameID.TYPOGRAPHIC_SUBFAMILY_NAME) or \ - nameIDs.get(NameID.SUBFAMILY_NAME) - - nameIDs[NameID.FULL_FONT_NAME] = f"{newFamilyName} {newStyleName}" - nameIDs[NameID.POSTSCRIPT_NAME] = f"{newFamilyName.replace(' ', '')}-{newStyleName.replace(' ', '')}" - nameIDs[NameID.UNIQUE_FONT_IDENTIFIER] = _uniqueIdRecord(varfont, lang, nameIDs) - - for nameID, string in nameIDs.items(): - if not string: - continue - nametable.setName(string, nameID, *lang) - - -def _uniqueIdRecord(varfont, lang, nameIDs): - name = varfont['name'] - record = name.getName(NameID.UNIQUE_FONT_IDENTIFIER, *lang) - if not record: - return None - - def isSubString(string1, string2): - if string2 in string1: - return True - return False - - # Check if full name and postscript name are a substring - for nameID in (4, 6): - nameRecord = name.getName(nameID, *lang) - if not nameRecord: - continue - if isSubString(record.toUnicode(), nameRecord.toUnicode()): - return record.toUnicode().replace( - nameRecord.toUnicode(), - nameIDs[nameRecord.nameID] - ) - # TODO (M Foley) Construct new uniqueID if full name or postscript names are not subsets - return None - - def splitAxisLocationAndRanges(axisLimits, rangeType=AxisRange): location, axisRanges = {}, {} for axisTag, value in axisLimits.items(): @@ -1613,8 +1677,8 @@ def parseArgs(args): parser.add_argument( "--update-nametable", action="store_true", - help="Update the instantiated font's nametable using the STAT " - "table Axis Values" + help="Update the instantiated font's nametable. Input font must have " + "a STAT table with Axis Value Tables", ) loggingGroup = parser.add_mutually_exclusive_group(required=False) loggingGroup.add_argument( @@ -1667,7 +1731,7 @@ def main(args=None): inplace=True, optimize=options.optimize, overlap=options.overlap, - update_nametable=options.update_nametable, + updateFontNames=options.update_nametable, ) outfile = ( diff --git a/Tests/varLib/instancer_test.py b/Tests/varLib/instancer_test.py index 854a4b4fb..0ae43d7fc 100644 --- a/Tests/varLib/instancer_test.py +++ b/Tests/varLib/instancer_test.py @@ -2013,7 +2013,7 @@ def test_updateNametable_partial(varfont): def test_updateNameTable_missing_axisValues(varfont): - with pytest.raises(ValueError, match="Cannot find AxisValue for wght=200"): + with pytest.raises(ValueError, match="Cannot find Axis Value Tables wght=200"): instancer.updateNameTable(varfont, {"wght": 200}) From 0bcbbfdbb5e3bcc2d73b2f5deda227cfd702d05a Mon Sep 17 00:00:00 2001 From: Marc Foley Date: Fri, 16 Oct 2020 10:47:40 +0100 Subject: [PATCH 13/28] instancer: reuse existing instantiateSTAT func --- Lib/fontTools/varLib/instancer.py | 138 +++++++++--------------------- Tests/varLib/instancer_test.py | 10 +++ 2 files changed, 52 insertions(+), 96 deletions(-) diff --git a/Lib/fontTools/varLib/instancer.py b/Lib/fontTools/varLib/instancer.py index c72ff48f7..6aece3abb 100644 --- a/Lib/fontTools/varLib/instancer.py +++ b/Lib/fontTools/varLib/instancer.py @@ -1021,6 +1021,11 @@ def instantiateSTAT(varfont, axisLimits): ): return # STAT table empty, nothing to do + log.info("Instantiating STAT table") + _instantiateSTAT(stat, axisLimits) + + +def _instantiateSTAT(stat, axisLimits): location, axisRanges = splitAxisLocationAndRanges(axisLimits, rangeType=AxisRange) def isAxisValueOutsideLimits(axisTag, axisValue): @@ -1032,8 +1037,6 @@ def instantiateSTAT(varfont, axisLimits): return True return False - log.info("Instantiating STAT table") - # only keep AxisValues whose axis is not pinned nor restricted, or is pinned at the # exact (nominal) value, or is restricted but the value is within the new range designAxes = stat.DesignAxisRecord.Axis @@ -1113,11 +1116,11 @@ def pruningUnusedNames(varfont): def updateNameTable(varfont, axisLimits): - """Update an instatiated variable font's name table using the STAT - table's Axis Value Tables. + """Update an instatiated variable font's name table using the Axis + Value Tables from the STAT table. To establish which Axis Value Tables are needed, we first remove all - tables whose Value's are not in the axisLimits dictionary. We then + tables whose Value's are not in the axisLimits. We then remove all tables which have Flag 2 enabled (ELIDABLE_AXIS_VALUE_NAME). Finally, we remove duplicates and ensure Format 4 tables preside over the other formats. @@ -1129,9 +1132,9 @@ def updateNameTable(varfont, axisLimits): stat = varfont["STAT"] fvar = varfont["fvar"] - # The updated name table must reflect the 'zero origin' of the font. + # The updated name table must reflect the new 'zero origin' of the font. # If a user is instantiating a partial font, we will populate the - # unpinned axes with their default values. + # unpinned axes with their default axis values. fvarDefaults = {a.axisTag: a.defaultValue for a in fvar.axes} axisCoords = axisLimits for axisTag, val in fvarDefaults.items(): @@ -1140,104 +1143,46 @@ def updateNameTable(varfont, axisLimits): elif isinstance(axisCoords[axisTag], tuple): axisCoords[axisTag] = val - axisValueTables = _axisValueTablesFromAxisCoords(stat, axisCoords) - _updateNameRecords(varfont, axisValueTables) + stat_new = deepcopy(stat).table + _instantiateSTAT(stat_new, axisCoords) + checkMissingAxisValues(stat_new, axisCoords) - -def _axisValueTablesFromAxisCoords(stat, axisCoords): - axisValueTables = stat.table.AxisValueArray.AxisValue - axisRecords = stat.table.DesignAxisRecord.Axis - axisRecordIndex = {a.AxisTag: a.AxisOrdering for a in axisRecords} - axisRecordTag = {a.AxisOrdering: a.AxisTag for a in axisRecords} - - axisValuesToFind = { - axisRecordIndex[axisTag]: val for axisTag, val in axisCoords.items() - } - axisValueTables = [ - v for v in axisValueTables if _axisValueInAxisCoords(v, axisValuesToFind) - ] - axisValueTablesMissing = set(axisValuesToFind) - axisValueRecordsIndexes( - axisValueTables - ) - if axisValueTablesMissing: - missing = ", ".join( - f"{axisRecordTag[i]}={axisValuesToFind[i]}" for i in axisValueTablesMissing - ) - raise ValueError(f"Cannot find Axis Value Tables {missing}") + axisValueTables = stat_new.AxisValueArray.AxisValue # remove axis Value Tables which have Elidable_AXIS_VALUE_NAME flag set + # Axis Value which have this flag enabled won't be visible in + # application font menus. axisValueTables = [ v for v in axisValueTables if v.Flags & ELIDABLE_AXIS_VALUE_NAME != 2 ] - return _sortedAxisValues(axisValueTables) + stat_new.AxisValueArray.AxisValue = axisValueTables + axisValueTables = _sortedAxisValues(stat_new, axisCoords) + _updateNameRecords(varfont, axisValueTables) -def _axisValueInAxisCoords(axisValueTable, axisCoords): - if axisValueTable.Format == 4: - res = [] - for rec in axisValueTable.AxisValueRecord: - axisIndex = rec.AxisIndex - if axisIndex not in axisCoords: - return False - if rec.Value == axisCoords[axisIndex]: - res.append(True) - else: - res.append(False) - return True if all(res) else False - - axisIndex = axisValueTable.AxisIndex - - if axisValueTable.Format in (1, 3): - # A variable font can have additional axes that are not implemented as - # dynamic-variation axes in the fvar table, but that are - # relevant for the font or the family of which it is a member. This - # condition will include them. - # A common scenario is a family which consists of two variable fonts, - # one for Roman styles, the other for Italic styles. Both fonts have a - # weight axis. In order to establish a relationship between the fonts, - # an Italic Axis Record is created for both fonts. In the Roman font, - # an Axis Value Table is added to the Italic Axis Record which has the - # name "Roman" and its Value is set to 0.0, it also includes link - # Value of 1. In the Italic font, an Axis Value Table is also added - # to the Italic Axis Record which has the name "Italic", its Value set - # to 1.0. - if axisIndex not in axisCoords and axisValueTable.Value in (0.0, 1.0): - return True - - elif axisIndex in axisCoords and axisValueTable.Value == axisCoords[axisIndex]: - return True - - if axisValueTable.Format == 2: - return ( - True - if all( - [ - axisIndex in axisCoords - and axisCoords[axisIndex] >= axisValueTable.RangeMinValue, - axisIndex in axisCoords - and axisCoords[axisIndex] <= axisValueTable.RangeMaxValue, - ] - ) - else False - ) - return False - - -def axisValueRecordsIndexes(axisValueTables): - res = set() +def checkMissingAxisValues(stat, axisCoords): + seen = set() + axisValueTables = stat.AxisValueArray.AxisValue + designAxes = stat.DesignAxisRecord.Axis for val in axisValueTables: - res |= axisValueRecordIndexes(val) - return res + if val.Format == 4: + for rec in val.AxisValueRecord: + axisTag = designAxes[rec.AxisIndex].AxisTag + seen.add(axisTag) + else: + axisTag = designAxes[val.AxisIndex].AxisTag + seen.add(axisTag) + + missingAxes = set(axisCoords) - seen + if missingAxes: + missing = ", ".join(f"{i}={axisCoords[i]}" for i in missingAxes) + raise ValueError(f"Cannot find Axis Value Tables {missing}") -def axisValueRecordIndexes(axisValueTable): - if axisValueTable.Format == 4: - return set(r.AxisIndex for r in axisValueTable.AxisValueRecord) - return set([axisValueTable.AxisIndex]) - - -def _sortedAxisValues(axisValueTables): +def _sortedAxisValues(stat, axisCoords): # Sort and remove duplicates ensuring that format 4 axis Value Tables # are dominant + axisValueTables = stat.AxisValueArray.AxisValue + designAxes = stat.DesignAxisRecord.Axis results = [] seenAxes = set() # sort format 4 axes so the tables with the most AxisValueRecords @@ -1250,7 +1195,7 @@ def _sortedAxisValues(axisValueTables): nonFormat4 = [v for v in axisValueTables if v not in format4] for val in format4: - axisIndexes = axisValueRecordIndexes(val) + axisIndexes = set(r.AxisIndex for r in val.AxisValueRecord) if bool(seenAxes & axisIndexes) == False: seenAxes |= axisIndexes results.append((tuple(axisIndexes), val)) @@ -1260,6 +1205,7 @@ def _sortedAxisValues(axisValueTables): if axisIndex not in seenAxes: seenAxes.add(axisIndex) results.append(((axisIndex,), val)) + return [axisValueTable for _, axisValueTable in sorted(results)] @@ -1311,7 +1257,7 @@ def _updateStyleRecords( ) or nametable.getName(NameID.SUBFAMILY_NAME, *platEncLang) if not currentFamilyName or not currentStyleName: - # Since no family name or style name entries were found, we cannot + # Since no family name or style name records were found, we cannot # update this set of name Records. return @@ -1510,7 +1456,7 @@ def instantiateVariableFont( updateFontNames (bool): if True, update the instantiated font's nametable using the Axis Value Tables from the STAT table. The name table will be updated so it conforms to the R/I/B/BI model. If the STAT table is missing or - an Axis Value table is missing for a given coordinate, an Error will be + an Axis Value table is missing for a given axis coordinate, an Error will be raised. """ # 'overlap' used to be bool and is now enum; for backward compat keep accepting bool diff --git a/Tests/varLib/instancer_test.py b/Tests/varLib/instancer_test.py index 0ae43d7fc..a0ba3b418 100644 --- a/Tests/varLib/instancer_test.py +++ b/Tests/varLib/instancer_test.py @@ -1966,6 +1966,10 @@ def test_updateNameTable_with_registered_axes(varfont): assert names[(17, 3, 1, 0x409)] == "Thin Condensed" +def test_updatetNameTable_axis_order(varfont): + pass + + def test_updateNameTable_with_multilingual_names(varfont): name = varfont["name"] name.setName("Test Variable Font", 1, 3, 1, 0x405) @@ -2017,6 +2021,12 @@ def test_updateNameTable_missing_axisValues(varfont): instancer.updateNameTable(varfont, {"wght": 200}) +def test_updateNameTable_missing_stat(varfont): + del varfont["STAT"] + with pytest.raises(ValueError, match="Cannot update name table since there is no STAT table."): + instancer.updateNameTable(varfont, {"wght": 400}) + + def test_updateNameTable_vf_with_italic_attribute(varfont): font_link_axisValue = varfont["STAT"].table.AxisValueArray.AxisValue[4] font_link_axisValue.Flags = 0 From 11f0ade44c5edbcfcac6c22a813dda9da8153744 Mon Sep 17 00:00:00 2001 From: Marc Foley Date: Fri, 16 Oct 2020 13:12:36 +0100 Subject: [PATCH 14/28] cleanup _updateUniqueIdNameRecord --- Lib/fontTools/varLib/instancer.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Lib/fontTools/varLib/instancer.py b/Lib/fontTools/varLib/instancer.py index 6aece3abb..25d9f1120 100644 --- a/Lib/fontTools/varLib/instancer.py +++ b/Lib/fontTools/varLib/instancer.py @@ -1309,9 +1309,9 @@ def _updateStyleRecords( def _updateUniqueIdNameRecord(varfont, nameIDs, platEncLang): - name = varfont["name"] - record = name.getName(NameID.UNIQUE_FONT_IDENTIFIER, *platEncLang) - if not record: + nametable = varfont["name"] + currentRecord = nametable.getName(NameID.UNIQUE_FONT_IDENTIFIER, *platEncLang) + if not currentRecord: return None def isSubString(string1, string2): @@ -1319,13 +1319,13 @@ def _updateUniqueIdNameRecord(varfont, nameIDs, platEncLang): return True return False - # Check if full name and postscript name are a substring + # Check if full name and postscript name are a substring of currentRecord for nameID in (4, 6): - nameRecord = name.getName(nameID, *platEncLang) + nameRecord = nametable.getName(nameID, *platEncLang) if not nameRecord: continue - if isSubString(record.toUnicode(), nameRecord.toUnicode()): - return record.toUnicode().replace( + if isSubString(currentRecord.toUnicode(), nameRecord.toUnicode()): + return currentRecord.toUnicode().replace( nameRecord.toUnicode(), nameIDs[nameRecord.nameID] ) # TODO (M Foley) Construct new uniqueID if full name or postscript names are not subsets From bef1d08c0b40698aea940241c978e894848fbd7c Mon Sep 17 00:00:00 2001 From: Marc Foley Date: Mon, 19 Oct 2020 12:51:59 +0100 Subject: [PATCH 15/28] instancer: updateNameTableStyleRecords use strings as input instead of axis values --- Lib/fontTools/varLib/instancer.py | 32 ++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/Lib/fontTools/varLib/instancer.py b/Lib/fontTools/varLib/instancer.py index 25d9f1120..f8dfb27f0 100644 --- a/Lib/fontTools/varLib/instancer.py +++ b/Lib/fontTools/varLib/instancer.py @@ -1216,15 +1216,26 @@ def _updateNameRecords(varfont, axisValueTables): ribbiAxisValues = _ribbiAxisValueTables(nametable, axisValueTables) nonRibbiAxisValues = [v for v in axisValueTables if v not in ribbiAxisValues] + getName = nametable.getName nameTablePlatEncLangs = set( (r.platformID, r.platEncID, r.langID) for r in nametable.names ) for platEncLang in nameTablePlatEncLangs: - _updateStyleRecords( + + subFamilyName = [ + getName(a.ValueNameID, *platEncLang) for a in ribbiAxisValues if a + ] + subFamilyName = " ".join([r.toUnicode() for r in subFamilyName if r]) + typoSubFamilyName = [ + getName(a.ValueNameID, *platEncLang) for a in nonRibbiAxisValues if a + ] + typoSubFamilyName = " ".join([r.toUnicode() for r in typoSubFamilyName if r]) + + updateNameTableStyleRecords( varfont, nametable, - ribbiAxisValues, - nonRibbiAxisValues, + subFamilyName, + typoSubFamilyName, platEncLang, ) @@ -1245,8 +1256,8 @@ def _ribbiAxisValueTables(nametable, axisValueTables): ] -def _updateStyleRecords( - varfont, nametable, ribbiAxisValues, nonRibbiAxisValues, platEncLang=(3, 1, 0x409) +def updateNameTableStyleRecords( + varfont, nametable, ribbiName, nonRibbiName, platEncLang=(3, 1, 0x409) ): currentFamilyName = nametable.getName( NameID.TYPOGRAPHIC_FAMILY_NAME, *platEncLang @@ -1264,22 +1275,13 @@ def _updateStyleRecords( currentFamilyName = currentFamilyName.toUnicode() currentStyleName = currentStyleName.toUnicode() - ribbiName = " ".join( - nametable.getName(a.ValueNameID, *platEncLang).toUnicode() - for a in ribbiAxisValues - ) - nonRibbiName = " ".join( - nametable.getName(a.ValueNameID, *platEncLang).toUnicode() - for a in nonRibbiAxisValues - ) - nameIDs = { NameID.FAMILY_NAME: currentFamilyName, # TODO (M Foley) what about Elidable fallback name instead? NameID.SUBFAMILY_NAME: ribbiName or nametable.getName(NameID.SUBFAMILY_NAME, *platEncLang).toUnicode(), } - if nonRibbiAxisValues: + if nonRibbiName: nameIDs[NameID.FAMILY_NAME] = f"{currentFamilyName} {nonRibbiName}".strip() nameIDs[NameID.TYPOGRAPHIC_FAMILY_NAME] = currentFamilyName nameIDs[ From 29e4ff987ce2c191caa9184a882b72f87c9d23d9 Mon Sep 17 00:00:00 2001 From: Marc Foley Date: Mon, 19 Oct 2020 16:32:30 +0100 Subject: [PATCH 16/28] instancer: implement Cosimo feedback --- Lib/fontTools/varLib/instancer.py | 159 +++++++++++++++++++----------- Tests/varLib/instancer_test.py | 54 +++++++++- 2 files changed, 151 insertions(+), 62 deletions(-) diff --git a/Lib/fontTools/varLib/instancer.py b/Lib/fontTools/varLib/instancer.py index f8dfb27f0..7dccdcb6c 100644 --- a/Lib/fontTools/varLib/instancer.py +++ b/Lib/fontTools/varLib/instancer.py @@ -133,6 +133,7 @@ class NameID(IntEnum): SUBFAMILY_NAME = 2 UNIQUE_FONT_IDENTIFIER = 3 FULL_FONT_NAME = 4 + VERSION_STRING = 5 POSTSCRIPT_NAME = 6 TYPOGRAPHIC_FAMILY_NAME = 16 TYPOGRAPHIC_SUBFAMILY_NAME = 17 @@ -1117,13 +1118,7 @@ def pruningUnusedNames(varfont): def updateNameTable(varfont, axisLimits): """Update an instatiated variable font's name table using the Axis - Value Tables from the STAT table. - - To establish which Axis Value Tables are needed, we first remove all - tables whose Value's are not in the axisLimits. We then - remove all tables which have Flag 2 enabled (ELIDABLE_AXIS_VALUE_NAME). - Finally, we remove duplicates and ensure Format 4 tables preside over - the other formats. + Values from the STAT table. The updated nametable will conform to the R/I/B/BI naming model. """ @@ -1133,23 +1128,24 @@ def updateNameTable(varfont, axisLimits): fvar = varfont["fvar"] # The updated name table must reflect the new 'zero origin' of the font. - # If a user is instantiating a partial font, we will populate the - # unpinned axes with their default axis values. + # If we're instantiating a partial font, we will populate the unpinned + # axes with their default axis values. fvarDefaults = {a.axisTag: a.defaultValue for a in fvar.axes} - axisCoords = axisLimits + axisCoords = deepcopy(axisLimits) for axisTag, val in fvarDefaults.items(): - if axisTag not in axisCoords: - axisCoords[axisTag] = val - elif isinstance(axisCoords[axisTag], tuple): + if axisTag not in axisCoords or isinstance(axisCoords[axisTag], tuple): axisCoords[axisTag] = val + # To get the required Axis Values for the zero origin, we can simply + # duplicate the STAT table and instantiate it using the axis coords we + # created in the previous step. stat_new = deepcopy(stat).table _instantiateSTAT(stat_new, axisCoords) checkMissingAxisValues(stat_new, axisCoords) axisValueTables = stat_new.AxisValueArray.AxisValue - # remove axis Value Tables which have Elidable_AXIS_VALUE_NAME flag set - # Axis Value which have this flag enabled won't be visible in + # Remove axis Values which have Elidable_AXIS_VALUE_NAME flag set + # Axis Values which have this flag enabled won't be visible in # application font menus. axisValueTables = [ v for v in axisValueTables if v.Flags & ELIDABLE_AXIS_VALUE_NAME != 2 @@ -1174,18 +1170,18 @@ def checkMissingAxisValues(stat, axisCoords): missingAxes = set(axisCoords) - seen if missingAxes: - missing = ", ".join(f"{i}={axisCoords[i]}" for i in missingAxes) - raise ValueError(f"Cannot find Axis Value Tables {missing}") + missing = ", ".join(f"'{i}={axisCoords[i]}'" for i in missingAxes) + raise ValueError(f"Cannot find Axis Value Tables [{missing}]") def _sortedAxisValues(stat, axisCoords): - # Sort and remove duplicates ensuring that format 4 axis Value Tables + # Sort and remove duplicates ensuring that format 4 Axis Values # are dominant axisValueTables = stat.AxisValueArray.AxisValue designAxes = stat.DesignAxisRecord.Axis results = [] seenAxes = set() - # sort format 4 axes so the tables with the most AxisValueRecords + # Sort format 4 axes so the tables with the most AxisValueRecords # are first format4 = sorted( [v for v in axisValueTables if v.Format == 4], @@ -1196,15 +1192,16 @@ def _sortedAxisValues(stat, axisCoords): for val in format4: axisIndexes = set(r.AxisIndex for r in val.AxisValueRecord) - if bool(seenAxes & axisIndexes) == False: + minIndex = min(axisIndexes) + if not seenAxes & axisIndexes: seenAxes |= axisIndexes - results.append((tuple(axisIndexes), val)) + results.append((minIndex, val)) for val in nonFormat4: axisIndex = val.AxisIndex if axisIndex not in seenAxes: seenAxes.add(axisIndex) - results.append(((axisIndex,), val)) + results.append((axisIndex, val)) return [axisValueTable for _, axisValueTable in sorted(results)] @@ -1212,9 +1209,13 @@ def _sortedAxisValues(stat, axisCoords): def _updateNameRecords(varfont, axisValueTables): # Update nametable based on the axisValues using the R/I/B/BI model. nametable = varfont["name"] + stat = varfont["STAT"].table - ribbiAxisValues = _ribbiAxisValueTables(nametable, axisValueTables) - nonRibbiAxisValues = [v for v in axisValueTables if v not in ribbiAxisValues] + axisValueNameIDs = [a.ValueNameID for a in axisValueTables] + ribbiNameIDs = [n for n in axisValueNameIDs if nameIdIsRibbi(nametable, n)] + nonRibbiNameIDs = [n for n in axisValueNameIDs if n not in ribbiNameIDs] + elidedNameID = stat.ElidedFallbackNameID + elidedNameIsRibbi = nameIdIsRibbi(nametable, elidedNameID) getName = nametable.getName nameTablePlatEncLangs = set( @@ -1222,43 +1223,73 @@ def _updateNameRecords(varfont, axisValueTables): ) for platEncLang in nameTablePlatEncLangs: - subFamilyName = [ - getName(a.ValueNameID, *platEncLang) for a in ribbiAxisValues if a - ] - subFamilyName = " ".join([r.toUnicode() for r in subFamilyName if r]) - typoSubFamilyName = [ - getName(a.ValueNameID, *platEncLang) for a in nonRibbiAxisValues if a - ] - typoSubFamilyName = " ".join([r.toUnicode() for r in typoSubFamilyName if r]) + if not all(getName(i, *platEncLang) for i in (1,2, elidedNameID)): + # Since no family name and subfamily name records were found, + # we cannot update this set of name Records. + continue - updateNameTableStyleRecords( + subFamilyName = " ".join( + getName(n, *platEncLang).toUnicode() for n in ribbiNameIDs + ) + typoSubFamilyName = " ".join( + getName(n, *platEncLang).toUnicode() for n in axisValueNameIDs + ) + + # If neither subFamilyName and typographic SubFamilyName exist, + # we will use the STAT's elidedFallbackName + if not typoSubFamilyName and not subFamilyName: + if elidedNameIsRibbi: + subFamilyName = getName(elidedNameID, *platEncLang).toUnicode() + else: + typoSubFamilyName = getName(elidedNameID, *platEncLang).toUnicode() + + familyNameSuffix = " ".join( + getName(n, *platEncLang).toUnicode() for n in nonRibbiNameIDs + ) + + _updateNameTableStyleRecords( varfont, - nametable, + familyNameSuffix, subFamilyName, typoSubFamilyName, - platEncLang, + *platEncLang, ) -def _ribbiAxisValueTables(nametable, axisValueTables): - engNameRecords = any([r for r in nametable.names if r.langID == 0x409]) +def nameIdIsRibbi(nametable, nameID): + engNameRecords = any( + r + for r in nametable.names + if (r.platformID, r.platEncID, r.langID) == (3, 1, 0x409) + ) if not engNameRecords: raise ValueError( f"Canot determine if there are RIBBI Axis Value Tables " "since there are no name table Records which have " "platformID=3, platEncID=1, langID=0x409" ) - return [ - v - for v in axisValueTables - if nametable.getName(v.ValueNameID, 3, 1, 0x409).toUnicode() + return ( + True + if nametable.getName(nameID, 3, 1, 0x409).toUnicode() in ("Regular", "Italic", "Bold", "Bold Italic") - ] + else False + ) -def updateNameTableStyleRecords( - varfont, nametable, ribbiName, nonRibbiName, platEncLang=(3, 1, 0x409) +def _updateNameTableStyleRecords( + varfont, + familyNameSuffix, + subFamilyName, + typoSubFamilyName, + platformID=3, + platEncID=1, + langID=0x409, ): + # TODO (Marc F) It may be nice to make this part a standalone + # font renamer in the future. + nametable = varfont["name"] + platEncLang = (platformID, platEncID, langID) + currentFamilyName = nametable.getName( NameID.TYPOGRAPHIC_FAMILY_NAME, *platEncLang ) or nametable.getName(NameID.FAMILY_NAME, *platEncLang) @@ -1267,26 +1298,25 @@ def updateNameTableStyleRecords( NameID.TYPOGRAPHIC_SUBFAMILY_NAME, *platEncLang ) or nametable.getName(NameID.SUBFAMILY_NAME, *platEncLang) - if not currentFamilyName or not currentStyleName: - # Since no family name or style name records were found, we cannot - # update this set of name Records. - return - currentFamilyName = currentFamilyName.toUnicode() currentStyleName = currentStyleName.toUnicode() nameIDs = { NameID.FAMILY_NAME: currentFamilyName, - # TODO (M Foley) what about Elidable fallback name instead? - NameID.SUBFAMILY_NAME: ribbiName - or nametable.getName(NameID.SUBFAMILY_NAME, *platEncLang).toUnicode(), + NameID.SUBFAMILY_NAME: subFamilyName, } - if nonRibbiName: - nameIDs[NameID.FAMILY_NAME] = f"{currentFamilyName} {nonRibbiName}".strip() + if typoSubFamilyName: + nameIDs[NameID.FAMILY_NAME] = f"{currentFamilyName} {familyNameSuffix}".strip() nameIDs[NameID.TYPOGRAPHIC_FAMILY_NAME] = currentFamilyName - nameIDs[ + nameIDs[NameID.TYPOGRAPHIC_SUBFAMILY_NAME] = f"{typoSubFamilyName}" + # Remove previous Typographic Family and SubFamily names since they're + # no longer required + else: + for nameID in ( + NameID.TYPOGRAPHIC_FAMILY_NAME, NameID.TYPOGRAPHIC_SUBFAMILY_NAME - ] = f"{nonRibbiName} {ribbiName}".strip() + ): + nametable.removeNames(nameID=nameID) newFamilyName = nameIDs.get(NameID.TYPOGRAPHIC_FAMILY_NAME) or nameIDs.get( NameID.FAMILY_NAME @@ -1330,8 +1360,21 @@ def _updateUniqueIdNameRecord(varfont, nameIDs, platEncLang): return currentRecord.toUnicode().replace( nameRecord.toUnicode(), nameIDs[nameRecord.nameID] ) - # TODO (M Foley) Construct new uniqueID if full name or postscript names are not subsets - return None + # Create a new string since we couldn't find any substrings. + fontVersion = _fontVersion(varfont, platEncLang) + vendor = varfont["OS/2"].achVendID.strip() + psName = nameIDs[NameID.POSTSCRIPT_NAME] + return f"{fontVersion};{vendor};{psName}" + + +def _fontVersion(font, platEncLang=(3, 1, 0x409)): + nameRecord = font["name"].getName(NameID.VERSION_STRING, *platEncLang) + if nameRecord is None: + return f'{font["head"].fontRevision:.3f}' + # "Version 1.101; ttfautohint (v1.8.1.43-b0c9)" --> "1.101" + # Also works fine with inputs "Version 1.101" or "1.101" etc + versionNumber = nameRecord.toUnicode().split(";")[0] + return versionNumber.lstrip("Version ").strip() def setMacOverlapFlags(glyfTable): diff --git a/Tests/varLib/instancer_test.py b/Tests/varLib/instancer_test.py index a0ba3b418..332924c46 100644 --- a/Tests/varLib/instancer_test.py +++ b/Tests/varLib/instancer_test.py @@ -7,6 +7,7 @@ from fontTools.ttLib.tables import _f_v_a_r, _g_l_y_f from fontTools.ttLib.tables import otTables from fontTools.ttLib.tables.TupleVariation import TupleVariation from fontTools import varLib +from fontTools.otlLib.builder import buildStatTable from fontTools.varLib import instancer from fontTools.varLib.mvar import MVAR_ENTRIES from fontTools.varLib import builder @@ -1967,7 +1968,34 @@ def test_updateNameTable_with_registered_axes(varfont): def test_updatetNameTable_axis_order(varfont): - pass + axes = [ + dict( + tag="wght", + name="Weight", + values=[ + dict(value=400, name='Regular'), + ], + ), + dict( + tag="wdth", + name="Width", + values=[ + dict(value=75, name="Condensed"), + ] + ) + ] + buildStatTable(varfont, axes) + instancer.updateNameTable(varfont, {"wdth": 75, "wght": 400}) + names = _get_name_records(varfont) + assert names[(17, 3, 1, 0x409)] == "Regular Condensed" + + # Swap the axes so the names get swapped + axes[0], axes[1] = axes[1], axes[0] + + buildStatTable(varfont, axes) + instancer.updateNameTable(varfont, {"wdth": 75, "wght": 400}) + names = _get_name_records(varfont) + assert names[(17, 3, 1, 0x409)] == "Condensed Regular" def test_updateNameTable_with_multilingual_names(varfont): @@ -2013,11 +2041,11 @@ def test_updateNametable_partial(varfont): assert names[(2, 3, 1, 0x409)] == "Regular" assert (3, 3, 1, 0x405) not in names assert names[(16, 3, 1, 0x409)] == "Test Variable Font" - assert names[(17, 3, 1, 0x409)] == "Condensed" #? maybe Condensed Regular? + assert names[(17, 3, 1, 0x409)] == "Condensed" def test_updateNameTable_missing_axisValues(varfont): - with pytest.raises(ValueError, match="Cannot find Axis Value Tables wght=200"): + with pytest.raises(ValueError, match="Cannot find Axis Value Tables \['wght=200'\]"): instancer.updateNameTable(varfont, {"wght": 200}) @@ -2029,7 +2057,8 @@ def test_updateNameTable_missing_stat(varfont): def test_updateNameTable_vf_with_italic_attribute(varfont): font_link_axisValue = varfont["STAT"].table.AxisValueArray.AxisValue[4] - font_link_axisValue.Flags = 0 + # Unset ELIDABLE_AXIS_VALUE_NAME flag + font_link_axisValue.Flags &= ~instancer.ELIDABLE_AXIS_VALUE_NAME font_link_axisValue.ValueNameID = 294 # Roman --> Italic # Italic @@ -2077,6 +2106,23 @@ def test_updateNameTable_format4_axisValues(varfont): assert names[(17, 3, 1, 0x409)] == "Dominant Value" +def test_updateNameTable_elided_axisValues(varfont): + stat = varfont["STAT"].table + # set ELIDABLE_AXIS_VALUE_NAME flag for all axisValues + for axisValue in stat.AxisValueArray.AxisValue: + axisValue.Flags |= instancer.ELIDABLE_AXIS_VALUE_NAME + + stat.ElidedFallbackNameID = 266 # Regular --> Black + instancer.updateNameTable(varfont, {"wght": 400}) + names = _get_name_records(varfont) + # Since all axis values are elided, the elided fallback name + # must be used to construct the style names. Since we + # changed it to Black, we need both a typoSubFamilyName and + # the subFamilyName set so it conforms to the RIBBI model. + assert names[(2, 3, 1, 0x409)] == "Regular" + assert names[(17, 3, 1, 0x409)] == "Black" + + def test_sanityCheckVariableTables(varfont): font = ttLib.TTFont() with pytest.raises(ValueError, match="Missing required table fvar"): From 5eac886e5a34e86569988db2f2f5e4e38ff123cf Mon Sep 17 00:00:00 2001 From: Marc Foley Date: Tue, 27 Oct 2020 21:32:08 +0000 Subject: [PATCH 17/28] Only create typographic subfamily name if there are nonRibbi tokens --- Lib/fontTools/varLib/instancer.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Lib/fontTools/varLib/instancer.py b/Lib/fontTools/varLib/instancer.py index 7dccdcb6c..aefce744c 100644 --- a/Lib/fontTools/varLib/instancer.py +++ b/Lib/fontTools/varLib/instancer.py @@ -1124,7 +1124,9 @@ def updateNameTable(varfont, axisLimits): """ if "STAT" not in varfont: raise ValueError("Cannot update name table since there is no STAT table.") - stat = varfont["STAT"] + stat = varfont["STAT"].table + if not stat.AxisValueArray: + raise ValueError("Cannot update name table since there are no STAT Axis Values") fvar = varfont["fvar"] # The updated name table must reflect the new 'zero origin' of the font. @@ -1139,7 +1141,7 @@ def updateNameTable(varfont, axisLimits): # To get the required Axis Values for the zero origin, we can simply # duplicate the STAT table and instantiate it using the axis coords we # created in the previous step. - stat_new = deepcopy(stat).table + stat_new = deepcopy(stat) _instantiateSTAT(stat_new, axisCoords) checkMissingAxisValues(stat_new, axisCoords) @@ -1232,7 +1234,7 @@ def _updateNameRecords(varfont, axisValueTables): getName(n, *platEncLang).toUnicode() for n in ribbiNameIDs ) typoSubFamilyName = " ".join( - getName(n, *platEncLang).toUnicode() for n in axisValueNameIDs + getName(n, *platEncLang).toUnicode() for n in axisValueNameIDs if nonRibbiNameIDs ) # If neither subFamilyName and typographic SubFamilyName exist, From de38c9ce967160eca357f52d7650d22630e972f6 Mon Sep 17 00:00:00 2001 From: Marc Foley Date: Thu, 29 Oct 2020 10:32:39 +0000 Subject: [PATCH 18/28] Tidy up variable name and run through black --- Lib/fontTools/varLib/instancer.py | 75 +++++++++++++++---------------- 1 file changed, 37 insertions(+), 38 deletions(-) diff --git a/Lib/fontTools/varLib/instancer.py b/Lib/fontTools/varLib/instancer.py index aefce744c..3840f9f59 100644 --- a/Lib/fontTools/varLib/instancer.py +++ b/Lib/fontTools/varLib/instancer.py @@ -1146,7 +1146,7 @@ def updateNameTable(varfont, axisLimits): checkMissingAxisValues(stat_new, axisCoords) axisValueTables = stat_new.AxisValueArray.AxisValue - # Remove axis Values which have Elidable_AXIS_VALUE_NAME flag set + # Remove axis Values which have Elidable_AXIS_VALUE_NAME flag set. # Axis Values which have this flag enabled won't be visible in # application font menus. axisValueTables = [ @@ -1159,9 +1159,9 @@ def updateNameTable(varfont, axisLimits): def checkMissingAxisValues(stat, axisCoords): seen = set() - axisValueTables = stat.AxisValueArray.AxisValue + axisValues = stat.AxisValueArray.AxisValue designAxes = stat.DesignAxisRecord.Axis - for val in axisValueTables: + for val in axisValues: if val.Format == 4: for rec in val.AxisValueRecord: axisTag = designAxes[rec.AxisIndex].AxisTag @@ -1179,18 +1179,18 @@ def checkMissingAxisValues(stat, axisCoords): def _sortedAxisValues(stat, axisCoords): # Sort and remove duplicates ensuring that format 4 Axis Values # are dominant - axisValueTables = stat.AxisValueArray.AxisValue + axisValues = stat.AxisValueArray.AxisValue designAxes = stat.DesignAxisRecord.Axis results = [] seenAxes = set() # Sort format 4 axes so the tables with the most AxisValueRecords # are first format4 = sorted( - [v for v in axisValueTables if v.Format == 4], + [v for v in axisValues if v.Format == 4], key=lambda v: len(v.AxisValueRecord), reverse=True, ) - nonFormat4 = [v for v in axisValueTables if v not in format4] + nonFormat4 = [v for v in axisValues if v not in format4] for val in format4: axisIndexes = set(r.AxisIndex for r in val.AxisValueRecord) @@ -1205,48 +1205,47 @@ def _sortedAxisValues(stat, axisCoords): seenAxes.add(axisIndex) results.append((axisIndex, val)) - return [axisValueTable for _, axisValueTable in sorted(results)] + return [axisValue for _, axisValue in sorted(results)] -def _updateNameRecords(varfont, axisValueTables): +def _updateNameRecords(varfont, axisValues): # Update nametable based on the axisValues using the R/I/B/BI model. nametable = varfont["name"] stat = varfont["STAT"].table - axisValueNameIDs = [a.ValueNameID for a in axisValueTables] - ribbiNameIDs = [n for n in axisValueNameIDs if nameIdIsRibbi(nametable, n)] + axisValueNameIDs = [a.ValueNameID for a in axisValues] + ribbiNameIDs = [n for n in axisValueNameIDs if nameIDIsRibbi(nametable, n)] nonRibbiNameIDs = [n for n in axisValueNameIDs if n not in ribbiNameIDs] elidedNameID = stat.ElidedFallbackNameID - elidedNameIsRibbi = nameIdIsRibbi(nametable, elidedNameID) + elidedNameIsRibbi = nameIDIsRibbi(nametable, elidedNameID) getName = nametable.getName - nameTablePlatEncLangs = set( - (r.platformID, r.platEncID, r.langID) for r in nametable.names - ) - for platEncLang in nameTablePlatEncLangs: - - if not all(getName(i, *platEncLang) for i in (1,2, elidedNameID)): + platforms = set((r.platformID, r.platEncID, r.langID) for r in nametable.names) + for platform in platforms: + if not all(getName(i, *platform) for i in (1, 2, elidedNameID)): # Since no family name and subfamily name records were found, # we cannot update this set of name Records. continue subFamilyName = " ".join( - getName(n, *platEncLang).toUnicode() for n in ribbiNameIDs + getName(n, *platform).toUnicode() for n in ribbiNameIDs ) typoSubFamilyName = " ".join( - getName(n, *platEncLang).toUnicode() for n in axisValueNameIDs if nonRibbiNameIDs + getName(n, *platform).toUnicode() + for n in axisValueNameIDs + if nonRibbiNameIDs ) # If neither subFamilyName and typographic SubFamilyName exist, # we will use the STAT's elidedFallbackName if not typoSubFamilyName and not subFamilyName: if elidedNameIsRibbi: - subFamilyName = getName(elidedNameID, *platEncLang).toUnicode() + subFamilyName = getName(elidedNameID, *platform).toUnicode() else: - typoSubFamilyName = getName(elidedNameID, *platEncLang).toUnicode() + typoSubFamilyName = getName(elidedNameID, *platform).toUnicode() familyNameSuffix = " ".join( - getName(n, *platEncLang).toUnicode() for n in nonRibbiNameIDs + getName(n, *platform).toUnicode() for n in nonRibbiNameIDs ) _updateNameTableStyleRecords( @@ -1254,11 +1253,11 @@ def _updateNameRecords(varfont, axisValueTables): familyNameSuffix, subFamilyName, typoSubFamilyName, - *platEncLang, + *platform, ) -def nameIdIsRibbi(nametable, nameID): +def nameIDIsRibbi(nametable, nameID): engNameRecords = any( r for r in nametable.names @@ -1290,15 +1289,15 @@ def _updateNameTableStyleRecords( # TODO (Marc F) It may be nice to make this part a standalone # font renamer in the future. nametable = varfont["name"] - platEncLang = (platformID, platEncID, langID) + platform = (platformID, platEncID, langID) currentFamilyName = nametable.getName( - NameID.TYPOGRAPHIC_FAMILY_NAME, *platEncLang - ) or nametable.getName(NameID.FAMILY_NAME, *platEncLang) + NameID.TYPOGRAPHIC_FAMILY_NAME, *platform + ) or nametable.getName(NameID.FAMILY_NAME, *platform) currentStyleName = nametable.getName( - NameID.TYPOGRAPHIC_SUBFAMILY_NAME, *platEncLang - ) or nametable.getName(NameID.SUBFAMILY_NAME, *platEncLang) + NameID.TYPOGRAPHIC_SUBFAMILY_NAME, *platform + ) or nametable.getName(NameID.SUBFAMILY_NAME, *platform) currentFamilyName = currentFamilyName.toUnicode() currentStyleName = currentStyleName.toUnicode() @@ -1316,7 +1315,7 @@ def _updateNameTableStyleRecords( else: for nameID in ( NameID.TYPOGRAPHIC_FAMILY_NAME, - NameID.TYPOGRAPHIC_SUBFAMILY_NAME + NameID.TYPOGRAPHIC_SUBFAMILY_NAME, ): nametable.removeNames(nameID=nameID) @@ -1333,18 +1332,18 @@ def _updateNameTableStyleRecords( NameID.POSTSCRIPT_NAME ] = f"{newFamilyName.replace(' ', '')}-{newStyleName.replace(' ', '')}" nameIDs[NameID.UNIQUE_FONT_IDENTIFIER] = _updateUniqueIdNameRecord( - varfont, nameIDs, platEncLang + varfont, nameIDs, platform ) for nameID, string in nameIDs.items(): if not string: continue - nametable.setName(string, nameID, *platEncLang) + nametable.setName(string, nameID, *platform) -def _updateUniqueIdNameRecord(varfont, nameIDs, platEncLang): +def _updateUniqueIdNameRecord(varfont, nameIDs, platform): nametable = varfont["name"] - currentRecord = nametable.getName(NameID.UNIQUE_FONT_IDENTIFIER, *platEncLang) + currentRecord = nametable.getName(NameID.UNIQUE_FONT_IDENTIFIER, *platform) if not currentRecord: return None @@ -1355,7 +1354,7 @@ def _updateUniqueIdNameRecord(varfont, nameIDs, platEncLang): # Check if full name and postscript name are a substring of currentRecord for nameID in (4, 6): - nameRecord = nametable.getName(nameID, *platEncLang) + nameRecord = nametable.getName(nameID, *platform) if not nameRecord: continue if isSubString(currentRecord.toUnicode(), nameRecord.toUnicode()): @@ -1363,14 +1362,14 @@ def _updateUniqueIdNameRecord(varfont, nameIDs, platEncLang): nameRecord.toUnicode(), nameIDs[nameRecord.nameID] ) # Create a new string since we couldn't find any substrings. - fontVersion = _fontVersion(varfont, platEncLang) + fontVersion = _fontVersion(varfont, platform) vendor = varfont["OS/2"].achVendID.strip() psName = nameIDs[NameID.POSTSCRIPT_NAME] return f"{fontVersion};{vendor};{psName}" -def _fontVersion(font, platEncLang=(3, 1, 0x409)): - nameRecord = font["name"].getName(NameID.VERSION_STRING, *platEncLang) +def _fontVersion(font, platform=(3, 1, 0x409)): + nameRecord = font["name"].getName(NameID.VERSION_STRING, *platform) if nameRecord is None: return f'{font["head"].fontRevision:.3f}' # "Version 1.101; ttfautohint (v1.8.1.43-b0c9)" --> "1.101" From daf6427b0b18793b3d3534012f1c618df8f4445e Mon Sep 17 00:00:00 2001 From: Marc Foley Date: Tue, 5 Jan 2021 14:21:04 +0000 Subject: [PATCH 19/28] Implement Adobe ps naming for instantiated instances --- Lib/fontTools/varLib/instancer.py | 36 ++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/Lib/fontTools/varLib/instancer.py b/Lib/fontTools/varLib/instancer.py index 3840f9f59..986d8bcc3 100644 --- a/Lib/fontTools/varLib/instancer.py +++ b/Lib/fontTools/varLib/instancer.py @@ -137,6 +137,7 @@ class NameID(IntEnum): POSTSCRIPT_NAME = 6 TYPOGRAPHIC_FAMILY_NAME = 16 TYPOGRAPHIC_SUBFAMILY_NAME = 17 + VARIATIONS_POSTSCRIPT_NAME_PREFIX = 25 ELIDABLE_AXIS_VALUE_NAME = 2 @@ -1173,7 +1174,7 @@ def checkMissingAxisValues(stat, axisCoords): missingAxes = set(axisCoords) - seen if missingAxes: missing = ", ".join(f"'{i}={axisCoords[i]}'" for i in missingAxes) - raise ValueError(f"Cannot find Axis Value Tables [{missing}]") + raise ValueError(f"Cannot find Axis Values [{missing}]") def _sortedAxisValues(stat, axisCoords): @@ -1327,10 +1328,9 @@ def _updateNameTableStyleRecords( ) nameIDs[NameID.FULL_FONT_NAME] = f"{newFamilyName} {newStyleName}" - # TODO (M Foley) implement Adobe PS naming for VFs - nameIDs[ - NameID.POSTSCRIPT_NAME - ] = f"{newFamilyName.replace(' ', '')}-{newStyleName.replace(' ', '')}" + nameIDs[NameID.POSTSCRIPT_NAME] = _updatePSNameRecord( + varfont, newFamilyName, newStyleName, platform + ) nameIDs[NameID.UNIQUE_FONT_IDENTIFIER] = _updateUniqueIdNameRecord( varfont, nameIDs, platform ) @@ -1341,6 +1341,32 @@ def _updateNameTableStyleRecords( nametable.setName(string, nameID, *platform) +def _updatePSNameRecord(varfont, familyName, styleName, platform): + # Implementation based on Adobe Technical Note #5902 : + # https://wwwimages2.adobe.com/content/dam/acom/en/devnet/font/pdfs/5902.AdobePSNameGeneration.pdf + nametable = varfont["name"] + + family_prefix = nametable.getName( + NameID.VARIATIONS_POSTSCRIPT_NAME_PREFIX, *platform + ) + if family_prefix: + family_prefix = familyPrefix.toUnicode() + else: + family_prefix = familyName + + psName = f"{family_prefix}-{styleName}" + # Remove any characters other than uppercase Latin letters, lowercase + # Latin letters, digits and hyphens. + psName = re.sub(r"[^A-Za-z0-9-]", r"", psName) + + if len(psName) > 127: + # Abbreviating the stylename so it fits within 127 characters whilst + # conforming to every vendor's specification is too complex. Instead + # we simply truncate the psname and add the required "..." + return f"{psName[:124]}..." + return psName + + def _updateUniqueIdNameRecord(varfont, nameIDs, platform): nametable = varfont["name"] currentRecord = nametable.getName(NameID.UNIQUE_FONT_IDENTIFIER, *platform) From 336e7827e7debca37a3595acfdb72bf5aa9f8cd5 Mon Sep 17 00:00:00 2001 From: Marc Foley Date: Tue, 5 Jan 2021 14:34:25 +0000 Subject: [PATCH 20/28] Implement Cosimo feedback --- Lib/fontTools/varLib/instancer.py | 182 ++++++++++++++++++++++-------- Tests/varLib/instancer_test.py | 6 +- 2 files changed, 137 insertions(+), 51 deletions(-) diff --git a/Lib/fontTools/varLib/instancer.py b/Lib/fontTools/varLib/instancer.py index 986d8bcc3..581f52ee6 100644 --- a/Lib/fontTools/varLib/instancer.py +++ b/Lib/fontTools/varLib/instancer.py @@ -1024,10 +1024,12 @@ def instantiateSTAT(varfont, axisLimits): return # STAT table empty, nothing to do log.info("Instantiating STAT table") - _instantiateSTAT(stat, axisLimits) + newAxisValueTables = axisValuesFromAxisLimits(stat, axisLimits) + stat.AxisValueArray.AxisValue = newAxisValueTables + stat.AxisValueCount = len(stat.AxisValueArray.AxisValue) -def _instantiateSTAT(stat, axisLimits): +def axisValuesFromAxisLimits(stat, axisLimits): location, axisRanges = splitAxisLocationAndRanges(axisLimits, rangeType=AxisRange) def isAxisValueOutsideLimits(axisTag, axisValue): @@ -1068,9 +1070,7 @@ def _instantiateSTAT(stat, axisLimits): else: log.warn("Unknown AxisValue table format (%s); ignored", axisValueFormat) newAxisValueTables.append(axisValueTable) - - stat.AxisValueArray.AxisValue = newAxisValueTables - stat.AxisValueCount = len(stat.AxisValueArray.AxisValue) + return newAxisValueTables def getVariationNameIDs(varfont): @@ -1121,8 +1121,97 @@ def updateNameTable(varfont, axisLimits): """Update an instatiated variable font's name table using the Axis Values from the STAT table. - The updated nametable will conform to the R/I/B/BI naming model. + The updated name table will conform to the R/I/B/BI naming model. """ + # This task can be split into two parts: + + # Task 1: Collecting and sorting the relevant AxisValues: + # 1. First check the variable font has a STAT table and it contains + # AxisValues. + # 2. Create a dictionary which contains the pinned axes from the + # axisLimits dict and for the unpinned axes, we'll use the fvar + # default coordinates e.g + # axisLimits = {"wght": 500, "wdth": AxisRange(75, 100), our dict will + # be {"wght": 500, "wdth": 100} if the width axis has a default of 100. + # 3. Create a new list of AxisValues whose Values match the dict we just + # created. + # 4. Remove any AxisValues from the list which have the + # Elidable_AXIS_VALUE_NAME flag set. + # 5. Remove and sort AxisValues in the list so format 4 AxisValues take + # precedence. According to the MS Spec "if a format 1, format 2 or + # format 3 table has a (nominal) value used in a format 4 table that + # also has values for other axes, the format 4 table, being the more + # specific match, is used", + # https://docs.microsoft.com/en-us/typography/opentype/spec/stat#axis-value-table-format-4 + + # Task 2: Updating a name table's style and family names from a list of + # AxisValues: + # 1. Sort AxisValues into two groups. For the first group, the names must be + # any of the following ["Regular", "Italic", "Bold", "Bold Italic"]. + # This group of names is often referred to as "RIBBI" names. For the + # other group, names must be non-RIBBI e.g "Medium Italic", "Condensed" + # etc. + # 2. Repeat the next steps for each name table record platform: + # a. Create new subFamily name and Typographic subFamily name from the + # above groups. + # b. Update nameIDs 1, 2, 3, 4, 6, 16, 17 using the new name created + # in the last step. + # + # Step by step example: + # A variable font which has a width and weight axes. + # AxisValues in font (represented as simplified dicts): + # axisValues = [ + # {"name": "Light", "axis": "wght", "value": 300}, + # {"name": "Regular", "axis": "wght", "value": 400}, + # {"name": "Medium", "axis": "wght", "value": 500}, + # {"name": "Bold", "axis": "wght", "value": 600}, + # {"name": "Condensed", "axis": "wdth", "value": 75}, + # {"name": "Normal", "axis": "wdth", "value": 100, "flags": 0x2}, + # ] + # # Let's instantiate a partial font which has a pinned wght axis and an + # unpinned width axis. + # >>> axisLimits = {"wght": 500, "width": AxisRange(75, 100)} + # >>> updateNameTable(varfont, axisLimits) + # + # AxisValues remaining after task 1.3: + # axisValues = [ + # {"name": "Medium", "axis": "wght", "value": 500}, + # {"name": "Normal", "axis": "wdth", "value": 100, "flags": 0x2} + # ] + # + # AxisValues remaining after completing all 1.x tasks: + # axisValues = [{"name": "Medium", "axis": "wght", "value": 500}] + # The Normal AxisValue is removed because it has the + # Elidable_AXIS_VALUE_NAME flag set. + # + # # AxisValues after separating into two groups in task 2.1: + # ribbiAxisValues = [] + # nonRibbiAxisValues = [{"name": "Medium", "axis": "wght", "value": 500}] + # + # # Names created from AxisValues in task 2.2a for Win US English platform: + # subFamilyName = "" + # typoSubFamilyName = "Medium" + # + # NameRecords updated in task 2.2b for Win US English platform: + # NameID 1 familyName: "Open Sans" --> "Open Sans Medium" + # NameID 2 subFamilyName: "Regular" --> "Regular" + # NameID 3 Unique font identifier: "3.000;GOOG;OpenSans-Regular" --> \ + # "3.000;GOOG;OpenSans-Medium" + # NameID 4 Full font name: "Open Sans Regular" --> "Open Sans Medium" + # NameID 6 PostScript name: "OpenSans-Regular" --> "OpenSans-Medium" + # NameID 16 Typographic Family name: None --> "Open Sans" + # NameID 17 Typographic Subfamily name: None --> "Medium" + # + # Notes on name table record updates: + # - Typographic names have been added since Medium is a non-Ribbi name. + # - Neither the before or after name records include the Width AxisValue + # names because the "Normal" AxisValue has the + # Elidable_AXIS_VALUE_NAME flag set. + # If we instantiate the same font but pin the wdth axis to 75, + # the "Condensed" AxisValue will be included. + # - For info regarding how RIBBI and non-RIBBI can be constructed see: + # https://docs.microsoft.com/en-us/typography/opentype/spec/name#name-ids + if "STAT" not in varfont: raise ValueError("Cannot update name table since there is no STAT table.") stat = varfont["STAT"].table @@ -1134,42 +1223,44 @@ def updateNameTable(varfont, axisLimits): # If we're instantiating a partial font, we will populate the unpinned # axes with their default axis values. fvarDefaults = {a.axisTag: a.defaultValue for a in fvar.axes} - axisCoords = deepcopy(axisLimits) + defaultAxisCoords = deepcopy(axisLimits) for axisTag, val in fvarDefaults.items(): - if axisTag not in axisCoords or isinstance(axisCoords[axisTag], tuple): - axisCoords[axisTag] = val + if axisTag not in defaultAxisCoords or isinstance( + defaultAxisCoords[axisTag], AxisRange + ): + defaultAxisCoords[axisTag] = val - # To get the required Axis Values for the zero origin, we can simply - # duplicate the STAT table and instantiate it using the axis coords we - # created in the previous step. - stat_new = deepcopy(stat) - _instantiateSTAT(stat_new, axisCoords) - checkMissingAxisValues(stat_new, axisCoords) + axisValueTables = axisValuesFromAxisLimits(stat, defaultAxisCoords) + checkAxisValuesExist(stat, axisValueTables, defaultAxisCoords) - axisValueTables = stat_new.AxisValueArray.AxisValue - # Remove axis Values which have Elidable_AXIS_VALUE_NAME flag set. + # Remove axis Values which have ELIDABLE_AXIS_VALUE_NAME flag set. # Axis Values which have this flag enabled won't be visible in # application font menus. axisValueTables = [ - v for v in axisValueTables if v.Flags & ELIDABLE_AXIS_VALUE_NAME != 2 + v for v in axisValueTables if not v.Flags & ELIDABLE_AXIS_VALUE_NAME ] - stat_new.AxisValueArray.AxisValue = axisValueTables - axisValueTables = _sortedAxisValues(stat_new, axisCoords) + axisValueTables = _sortAxisValues(axisValueTables) _updateNameRecords(varfont, axisValueTables) -def checkMissingAxisValues(stat, axisCoords): +def checkAxisValuesExist(stat, axisValues, axisCoords): seen = set() - axisValues = stat.AxisValueArray.AxisValue designAxes = stat.DesignAxisRecord.Axis - for val in axisValues: - if val.Format == 4: - for rec in val.AxisValueRecord: - axisTag = designAxes[rec.AxisIndex].AxisTag + for axisValueTable in axisValues: + axisValueFormat = axisValueTable.Format + if axisValueTable.Format in (1, 2, 3): + axisTag = designAxes[axisValueTable.AxisIndex].AxisTag + if axisValueFormat == 2: + axisValue = axisValueTable.NominalValue + else: + axisValue = axisValueTable.Value + if axisTag in axisCoords and axisValue == axisCoords[axisTag]: seen.add(axisTag) - else: - axisTag = designAxes[val.AxisIndex].AxisTag - seen.add(axisTag) + elif axisValueTable.Format == 4: + for rec in axisValueTable.AxisValueRecord: + axisTag = designAxes[rec.AxisIndex].AxisTag + if axisTag in axisCoords and rec.Value == axisCoords[axisTag]: + seen.add(axisTag) missingAxes = set(axisCoords) - seen if missingAxes: @@ -1177,11 +1268,9 @@ def checkMissingAxisValues(stat, axisCoords): raise ValueError(f"Cannot find Axis Values [{missing}]") -def _sortedAxisValues(stat, axisCoords): +def _sortAxisValues(axisValues): # Sort and remove duplicates ensuring that format 4 Axis Values # are dominant - axisValues = stat.AxisValueArray.AxisValue - designAxes = stat.DesignAxisRecord.Axis results = [] seenAxes = set() # Sort format 4 axes so the tables with the most AxisValueRecords @@ -1320,11 +1409,11 @@ def _updateNameTableStyleRecords( ): nametable.removeNames(nameID=nameID) - newFamilyName = nameIDs.get(NameID.TYPOGRAPHIC_FAMILY_NAME) or nameIDs.get( - NameID.FAMILY_NAME + newFamilyName = ( + nameIDs.get(NameID.TYPOGRAPHIC_FAMILY_NAME) or nameIDs[NameID.FAMILY_NAME] ) - newStyleName = nameIDs.get(NameID.TYPOGRAPHIC_SUBFAMILY_NAME) or nameIDs.get( - NameID.SUBFAMILY_NAME + newStyleName = ( + nameIDs.get(NameID.TYPOGRAPHIC_SUBFAMILY_NAME) or nameIDs[NameID.SUBFAMILY_NAME] ) nameIDs[NameID.FULL_FONT_NAME] = f"{newFamilyName} {newStyleName}" @@ -1373,23 +1462,20 @@ def _updateUniqueIdNameRecord(varfont, nameIDs, platform): if not currentRecord: return None - def isSubString(string1, string2): - if string2 in string1: - return True - return False - # Check if full name and postscript name are a substring of currentRecord - for nameID in (4, 6): + for nameID in (NameID.FULL_FONT_NAME, NameID.POSTSCRIPT_NAME): nameRecord = nametable.getName(nameID, *platform) if not nameRecord: continue - if isSubString(currentRecord.toUnicode(), nameRecord.toUnicode()): + if currentRecord.toUnicode() in nameRecord.toUnicode(): return currentRecord.toUnicode().replace( nameRecord.toUnicode(), nameIDs[nameRecord.nameID] ) # Create a new string since we couldn't find any substrings. fontVersion = _fontVersion(varfont, platform) - vendor = varfont["OS/2"].achVendID.strip() + achVendID = varfont["OS/2"].achVendID + # Remove non-ASCII characers and trailing spaces + vendor = re.sub(r"[^\x00-\x7F]", "", achVendID).strip() psName = nameIDs[NameID.POSTSCRIPT_NAME] return f"{fontVersion};{vendor};{psName}" @@ -1546,7 +1632,7 @@ def instantiateVariableFont( varfont = deepcopy(varfont) if updateFontNames: - log.info("Updating nametable") + log.info("Updating name table") updateNameTable(varfont, axisLimits) if "gvar" in varfont: @@ -1693,9 +1779,9 @@ def parseArgs(args): "when generating a full instance). Requires skia-pathops", ) parser.add_argument( - "--update-nametable", + "--update-name-table", action="store_true", - help="Update the instantiated font's nametable. Input font must have " + help="Update the instantiated font's `name` table. Input font must have " "a STAT table with Axis Value Tables", ) loggingGroup = parser.add_mutually_exclusive_group(required=False) @@ -1749,7 +1835,7 @@ def main(args=None): inplace=True, optimize=options.optimize, overlap=options.overlap, - updateFontNames=options.update_nametable, + updateFontNames=options.update_name_table, ) outfile = ( diff --git a/Tests/varLib/instancer_test.py b/Tests/varLib/instancer_test.py index 332924c46..6b23bfde2 100644 --- a/Tests/varLib/instancer_test.py +++ b/Tests/varLib/instancer_test.py @@ -2000,6 +2000,7 @@ def test_updatetNameTable_axis_order(varfont): def test_updateNameTable_with_multilingual_names(varfont): name = varfont["name"] + # langID 0x405 is the Czech Windows langID name.setName("Test Variable Font", 1, 3, 1, 0x405) name.setName("Normal", 2, 3, 1, 0x405) name.setName("Normal", 261, 3, 1, 0x405) # nameID 261=Regular STAT entry @@ -2035,17 +2036,16 @@ def test_updateNameTable_with_multilingual_names(varfont): def test_updateNametable_partial(varfont): - instancer.updateNameTable(varfont, {"wdth": 79, "wght": (400, 900)}) + instancer.updateNameTable(varfont, {"wdth": 79, "wght": instancer.AxisRange(400, 900)}) names = _get_name_records(varfont) assert names[(1, 3, 1, 0x409)] == "Test Variable Font Condensed" assert names[(2, 3, 1, 0x409)] == "Regular" - assert (3, 3, 1, 0x405) not in names assert names[(16, 3, 1, 0x409)] == "Test Variable Font" assert names[(17, 3, 1, 0x409)] == "Condensed" def test_updateNameTable_missing_axisValues(varfont): - with pytest.raises(ValueError, match="Cannot find Axis Value Tables \['wght=200'\]"): + with pytest.raises(ValueError, match="Cannot find Axis Values \['wght=200'\]"): instancer.updateNameTable(varfont, {"wght": 200}) From 0280eb36cc0551c2e61254fcc5942dfd28e7edaa Mon Sep 17 00:00:00 2001 From: Marc Foley Date: Mon, 1 Feb 2021 15:37:16 +0000 Subject: [PATCH 21/28] Parametrize updateNameTable tests --- Tests/varLib/instancer_test.py | 306 ++++++++++++++++++++------------- 1 file changed, 189 insertions(+), 117 deletions(-) diff --git a/Tests/varLib/instancer_test.py b/Tests/varLib/instancer_test.py index 6b23bfde2..e4fc81bc9 100644 --- a/Tests/varLib/instancer_test.py +++ b/Tests/varLib/instancer_test.py @@ -145,7 +145,7 @@ class InstantiateGvarTest(object): assert "gvar" not in varfont def test_composite_glyph_not_in_gvar(self, varfont): - """ The 'minus' glyph is a composite glyph, which references 'hyphen' as a + """The 'minus' glyph is a composite glyph, which references 'hyphen' as a component, but has no tuple variations in gvar table, so the component offset and the phantom points do not change; however the sidebearings and bounding box do change as a result of the parent glyph 'hyphen' changing. @@ -1917,54 +1917,106 @@ def test_normalizeAxisLimits_missing_from_fvar(varfont): instancer.normalizeAxisLimits(varfont, {"ZZZZ": 1000}) -def _get_name_records(varfont): +def _test_name_records(varfont, expected, isNonRIBBI, platforms=[0x409]): nametable = varfont["name"] - return { + font_names = { (r.nameID, r.platformID, r.platEncID, r.langID): r.toUnicode() for r in nametable.names } + for k in expected: + if k[-1] not in platforms: + continue + assert font_names[k] == expected[k] + if isNonRIBBI: + font_nameids = set(i[0] for i in font_names) + assert 16 in font_nameids + assert 17 in font_nameids -def test_updateNameTable_with_registered_axes(varfont): - # Regular - instancer.updateNameTable(varfont, {"wght": 400}) - names = _get_name_records(varfont) - assert names[(1, 3, 1, 0x409)] == "Test Variable Font" - assert names[(2, 3, 1, 0x0409)] == "Regular" - assert names[(3, 3, 1, 0x0409)] == "2.001;GOOG;TestVariableFont-Regular" - assert names[(6, 3, 1, 0x409)] == "TestVariableFont-Regular" - assert (16, 3, 1, 0x409) not in names - assert (17, 3, 1, 0x409) not in names - - # Black - instancer.updateNameTable(varfont, {"wght": 900}) - names = _get_name_records(varfont) - assert names[(1, 3, 1, 0x409)] == "Test Variable Font Black" - assert names[(2, 3, 1, 0x409)] == "Regular" - assert names[(3, 3, 1, 0x0409)] == "2.001;GOOG;TestVariableFont-Black" - assert names[(6, 3, 1, 0x409)] == "TestVariableFont-Black" - assert names[(16, 3, 1, 0x409)] == "Test Variable Font" - assert names[(17, 3, 1, 0x409)] == "Black" - - # Thin - instancer.updateNameTable(varfont, {"wght": 100}) - names = _get_name_records(varfont) - assert names[(1, 3, 1, 0x409)] == "Test Variable Font Thin" - assert names[(2, 3, 1, 0x409)] == "Regular" - assert names[(3, 3, 1, 0x0409)] == "2.001;GOOG;TestVariableFont-Thin" - assert names[(6, 3, 1, 0x409)] == "TestVariableFont-Thin" - assert names[(16, 3, 1, 0x409)] == "Test Variable Font" - assert names[(17, 3, 1, 0x409)] == "Thin" - - # Thin Condensed - instancer.updateNameTable(varfont, {"wdth": 79, "wght": 100}) - names = _get_name_records(varfont) - assert names[(1, 3, 1, 0x409)] == "Test Variable Font Thin Condensed" - assert names[(2, 3, 1, 0x409)] == "Regular" - assert names[(3, 3, 1, 0x0409)] == "2.001;GOOG;TestVariableFont-ThinCondensed" - assert names[(6, 3, 1, 0x409)] == "TestVariableFont-ThinCondensed" - assert names[(16, 3, 1, 0x409)] == "Test Variable Font" - assert names[(17, 3, 1, 0x409)] == "Thin Condensed" +@pytest.mark.parametrize( + "limits, expected, isNonRIBBI", + [ + # Regular + ( + {"wght": 400}, + { + (1, 3, 1, 0x409): "Test Variable Font", + (2, 3, 1, 0x409): "Regular", + (3, 3, 1, 0x409): "2.001;GOOG;TestVariableFont-Regular", + (6, 3, 1, 0x409): "TestVariableFont-Regular", + }, + False, + ), + # Regular Normal (width axis Normal isn't included since it is elided) + ( + {"wght": 400, "wdth": 100}, + { + (1, 3, 1, 0x409): "Test Variable Font", + (2, 3, 1, 0x409): "Regular", + (3, 3, 1, 0x409): "2.001;GOOG;TestVariableFont-Regular", + (6, 3, 1, 0x409): "TestVariableFont-Regular", + }, + False, + ), + # Black + ( + {"wght": 900}, + { + (1, 3, 1, 0x409): "Test Variable Font Black", + (2, 3, 1, 0x409): "Regular", + (3, 3, 1, 0x409): "2.001;GOOG;TestVariableFont-Black", + (6, 3, 1, 0x409): "TestVariableFont-Black", + (16, 3, 1, 0x409): "Test Variable Font", + (17, 3, 1, 0x409): "Black", + }, + True, + ), + # Thin + ( + {"wght": 100}, + { + (1, 3, 1, 0x409): "Test Variable Font Thin", + (2, 3, 1, 0x409): "Regular", + (3, 3, 1, 0x409): "2.001;GOOG;TestVariableFont-Thin", + (6, 3, 1, 0x409): "TestVariableFont-Thin", + (16, 3, 1, 0x409): "Test Variable Font", + (17, 3, 1, 0x409): "Thin", + }, + True, + ), + # Thin Condensed + ( + {"wght": 100, "wdth": 79}, + { + (1, 3, 1, 0x409): "Test Variable Font Thin Condensed", + (2, 3, 1, 0x409): "Regular", + (3, 3, 1, 0x409): "2.001;GOOG;TestVariableFont-ThinCondensed", + (6, 3, 1, 0x409): "TestVariableFont-ThinCondensed", + (16, 3, 1, 0x409): "Test Variable Font", + (17, 3, 1, 0x409): "Thin Condensed", + }, + True, + ), + # Condensed with unpinned weights + ( + {"wdth": 79, "wght": instancer.AxisRange(400, 900)}, + { + (1, 3, 1, 0x409): "Test Variable Font Condensed", + (2, 3, 1, 0x409): "Regular", + (3, 3, 1, 0x409): "2.001;GOOG;TestVariableFont-Condensed", + (6, 3, 1, 0x409): "TestVariableFont-Condensed", + (16, 3, 1, 0x409): "Test Variable Font", + (17, 3, 1, 0x409): "Condensed", + }, + True, + ), + ], +) +def test_updateNameTable_with_registered_axes_ribbi( + varfont, limits, expected, isNonRIBBI +): + instancer.updateNameTable(varfont, limits) + _test_name_records(varfont, expected, isNonRIBBI) def test_updatetNameTable_axis_order(varfont): @@ -1973,7 +2025,7 @@ def test_updatetNameTable_axis_order(varfont): tag="wght", name="Weight", values=[ - dict(value=400, name='Regular'), + dict(value=400, name="Regular"), ], ), dict( @@ -1981,67 +2033,69 @@ def test_updatetNameTable_axis_order(varfont): name="Width", values=[ dict(value=75, name="Condensed"), - ] - ) + ], + ), ] + nametable = varfont["name"] buildStatTable(varfont, axes) instancer.updateNameTable(varfont, {"wdth": 75, "wght": 400}) - names = _get_name_records(varfont) - assert names[(17, 3, 1, 0x409)] == "Regular Condensed" + assert nametable.getName(17, 3, 1, 0x409).toUnicode() == "Regular Condensed" # Swap the axes so the names get swapped axes[0], axes[1] = axes[1], axes[0] buildStatTable(varfont, axes) instancer.updateNameTable(varfont, {"wdth": 75, "wght": 400}) - names = _get_name_records(varfont) - assert names[(17, 3, 1, 0x409)] == "Condensed Regular" + assert nametable.getName(17, 3, 1, 0x409).toUnicode() == "Condensed Regular" -def test_updateNameTable_with_multilingual_names(varfont): +@pytest.mark.parametrize( + "limits, expected, isNonRIBBI", + [ + # Regular | Normal + ( + {"wght": 400}, + { + (1, 3, 1, 0x409): "Test Variable Font", + (2, 3, 1, 0x409): "Normal", + }, + False, + ), + # Black | Negreta + ( + {"wght": 900}, + { + (1, 3, 1, 0x409): "Test Variable Font Negreta", + (2, 3, 1, 0x409): "Normal", + (16, 3, 1, 0x409): "Test Variable Font", + (17, 3, 1, 0x409): "Negreta", + }, + True, + ), + # Black Condensed | Negreta Zhuštěné + ( + {"wght": 900, "wdth": 79}, + { + (1, 3, 1, 0x409): "Test Variable Font Negreta Zhuštěné", + (2, 3, 1, 0x409): "Normal", + (16, 3, 1, 0x409): "Test Variable Font", + (17, 3, 1, 0x409): "Negreta Zhuštěné", + }, + True, + ), + ], +) +def test_updateNameTable_with_multilingual_names(varfont, limits, expected, isNonRIBBI): name = varfont["name"] # langID 0x405 is the Czech Windows langID name.setName("Test Variable Font", 1, 3, 1, 0x405) name.setName("Normal", 2, 3, 1, 0x405) - name.setName("Normal", 261, 3, 1, 0x405) # nameID 261=Regular STAT entry - name.setName("Negreta",266, 3, 1, 0x405) # nameID 266=Black STAT entry - name.setName("Zhuštěné", 279, 3, 1, 0x405) # nameID 279=Condensed STAT entry + name.setName("Normal", 261, 3, 1, 0x405) # nameID 261=Regular STAT entry + name.setName("Negreta", 266, 3, 1, 0x405) # nameID 266=Black STAT entry + name.setName("Zhuštěné", 279, 3, 1, 0x405) # nameID 279=Condensed STAT entry - # Regular | Normal - instancer.updateNameTable(varfont, {"wght": 400}) - names = _get_name_records(varfont) - assert names[(1, 3, 1, 0x405)] == "Test Variable Font" - assert names[(2, 3, 1, 0x405)] == "Normal" - assert (3, 3, 1, 0x405) not in names - assert (16, 3, 1, 0x405) not in names - assert (17, 3, 1, 0x405) not in names - - # Black | Negreta - instancer.updateNameTable(varfont, {"wght": 900}) - names = _get_name_records(varfont) - assert names[(1, 3, 1, 0x405)] == "Test Variable Font Negreta" - assert names[(2, 3, 1, 0x405)] == "Normal" - assert (3, 3, 1, 0x405) not in names - assert names[(16, 3, 1, 0x405)] == "Test Variable Font" - assert names[(17, 3, 1, 0x405)] == "Negreta" - - # Black Condensed | Negreta Zhuštěné - instancer.updateNameTable(varfont, {"wdth": 79, "wght": 900}) - names = _get_name_records(varfont) - assert names[(1, 3, 1, 0x405)] == "Test Variable Font Negreta Zhuštěné" - assert names[(2, 3, 1, 0x405)] == "Normal" - assert (3, 3, 1, 0x405) not in names - assert names[(16, 3, 1, 0x405)] == "Test Variable Font" - assert names[(17, 3, 1, 0x405)] == "Negreta Zhuštěné" - - -def test_updateNametable_partial(varfont): - instancer.updateNameTable(varfont, {"wdth": 79, "wght": instancer.AxisRange(400, 900)}) - names = _get_name_records(varfont) - assert names[(1, 3, 1, 0x409)] == "Test Variable Font Condensed" - assert names[(2, 3, 1, 0x409)] == "Regular" - assert names[(16, 3, 1, 0x409)] == "Test Variable Font" - assert names[(17, 3, 1, 0x409)] == "Condensed" + instancer.updateNameTable(varfont, limits) + names = _test_name_records(varfont, expected, isNonRIBBI, platforms=[0x405]) def test_updateNameTable_missing_axisValues(varfont): @@ -2051,32 +2105,49 @@ def test_updateNameTable_missing_axisValues(varfont): def test_updateNameTable_missing_stat(varfont): del varfont["STAT"] - with pytest.raises(ValueError, match="Cannot update name table since there is no STAT table."): + with pytest.raises( + ValueError, match="Cannot update name table since there is no STAT table." + ): instancer.updateNameTable(varfont, {"wght": 400}) -def test_updateNameTable_vf_with_italic_attribute(varfont): +@pytest.mark.parametrize( + "limits, expected, isNonRIBBI", + [ + # Regular | Normal + ( + {"wght": 400}, + { + (1, 3, 1, 0x409): "Test Variable Font", + (2, 3, 1, 0x409): "Italic", + (6, 3, 1, 0x409): "TestVariableFont-Italic", + }, + False, + ), + # Black Condensed Italic + ( + {"wght": 900, "wdth": 79}, + { + (1, 3, 1, 0x409): "Test Variable Font Black Condensed", + (2, 3, 1, 0x409): "Italic", + (6, 3, 1, 0x409): "TestVariableFont-BlackCondensedItalic", + (16, 3, 1, 0x409): "Test Variable Font", + (17, 3, 1, 0x409): "Black Condensed Italic", + }, + True, + ), + ], +) +def test_updateNameTable_vf_with_italic_attribute( + varfont, limits, expected, isNonRIBBI +): font_link_axisValue = varfont["STAT"].table.AxisValueArray.AxisValue[4] # Unset ELIDABLE_AXIS_VALUE_NAME flag font_link_axisValue.Flags &= ~instancer.ELIDABLE_AXIS_VALUE_NAME - font_link_axisValue.ValueNameID = 294 # Roman --> Italic + font_link_axisValue.ValueNameID = 294 # Roman --> Italic - # Italic - instancer.updateNameTable(varfont, {"wght": 400}) - names = _get_name_records(varfont) - assert names[(1, 3, 1, 0x409)] == "Test Variable Font" - assert names[(2, 3, 1, 0x409)] == "Italic" - assert (16, 3, 1, 0x405) not in names - assert (17, 3, 1, 0x405) not in names - - # Black Condensed Italic - instancer.updateNameTable(varfont, {"wdth": 79, "wght": 900}) - names = _get_name_records(varfont) - assert names[(1, 3, 1, 0x409)] == "Test Variable Font Black Condensed" - assert names[(2, 3, 1, 0x409)] == "Italic" - assert names[(6, 3, 1, 0x409)] == "TestVariableFont-BlackCondensedItalic" - assert names[(16, 3, 1, 0x409)] == "Test Variable Font" - assert names[(17, 3, 1, 0x409)] == "Black Condensed Italic" + instancer.updateNameTable(varfont, limits) + names = _test_name_records(varfont, expected, isNonRIBBI) def test_updateNameTable_format4_axisValues(varfont): @@ -2099,11 +2170,13 @@ def test_updateNameTable_format4_axisValues(varfont): stat.AxisValueArray.AxisValue.append(axisValue) instancer.updateNameTable(varfont, {"wdth": 79, "wght": 900}) - names = _get_name_records(varfont) - assert names[(1, 3, 1, 0x409)] == "Test Variable Font Dominant Value" - assert names[(2, 3, 1, 0x409)] == "Regular" - assert names[(16, 3, 1, 0x409)] == "Test Variable Font" - assert names[(17, 3, 1, 0x409)] == "Dominant Value" + expected = { + (1, 3, 1, 0x409): "Test Variable Font Dominant Value", + (2, 3, 1, 0x409): "Regular", + (16, 3, 1, 0x409): "Test Variable Font", + (17, 3, 1, 0x409): "Dominant Value", + } + _test_name_records(varfont, expected, isNonRIBBI=True) def test_updateNameTable_elided_axisValues(varfont): @@ -2112,15 +2185,14 @@ def test_updateNameTable_elided_axisValues(varfont): for axisValue in stat.AxisValueArray.AxisValue: axisValue.Flags |= instancer.ELIDABLE_AXIS_VALUE_NAME - stat.ElidedFallbackNameID = 266 # Regular --> Black + stat.ElidedFallbackNameID = 266 # Regular --> Black instancer.updateNameTable(varfont, {"wght": 400}) - names = _get_name_records(varfont) # Since all axis values are elided, the elided fallback name # must be used to construct the style names. Since we # changed it to Black, we need both a typoSubFamilyName and # the subFamilyName set so it conforms to the RIBBI model. - assert names[(2, 3, 1, 0x409)] == "Regular" - assert names[(17, 3, 1, 0x409)] == "Black" + expected = {(2, 3, 1, 0x409): "Regular", (17, 3, 1, 0x409): "Black"} + _test_name_records(varfont, expected, isNonRIBBI=True) def test_sanityCheckVariableTables(varfont): From 2be13d50acf3f7c89dc06bbe26f79b7da4f61106 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Mon, 15 Feb 2021 12:22:48 +0000 Subject: [PATCH 22/28] make instancer into a package dir and move all names-related funcs to submodule move instancer tests to Tests/varLib/instancer directory create instancer/__main__.py to make package executable --- .../{instancer.py => instancer/__init__.py} | 443 +----------------- Lib/fontTools/varLib/instancer/__main__.py | 5 + Lib/fontTools/varLib/instancer/names.py | 441 +++++++++++++++++ Tests/varLib/instancer/conftest.py | 13 + .../data/PartialInstancerTest-VF.ttx | 0 .../data/PartialInstancerTest2-VF.ttx | 0 .../data/PartialInstancerTest3-VF.ttx | 0 ...tialInstancerTest2-VF-instance-100,100.ttx | 0 ...ialInstancerTest2-VF-instance-100,62.5.ttx | 0 ...tialInstancerTest2-VF-instance-400,100.ttx | 0 ...ialInstancerTest2-VF-instance-400,62.5.ttx | 0 ...tialInstancerTest2-VF-instance-900,100.ttx | 0 ...ialInstancerTest2-VF-instance-900,62.5.ttx | 0 ...Test3-VF-instance-400-no-overlap-flags.ttx | 0 ...ancerTest3-VF-instance-400-no-overlaps.ttx | 0 ...ancerTest3-VF-instance-700-no-overlaps.ttx | 0 .../varLib/{ => instancer}/instancer_test.py | 312 +----------- Tests/varLib/instancer/names_test.py | 307 ++++++++++++ 18 files changed, 771 insertions(+), 750 deletions(-) rename Lib/fontTools/varLib/{instancer.py => instancer/__init__.py} (76%) create mode 100644 Lib/fontTools/varLib/instancer/__main__.py create mode 100644 Lib/fontTools/varLib/instancer/names.py create mode 100644 Tests/varLib/instancer/conftest.py rename Tests/varLib/{ => instancer}/data/PartialInstancerTest-VF.ttx (100%) rename Tests/varLib/{ => instancer}/data/PartialInstancerTest2-VF.ttx (100%) rename Tests/varLib/{ => instancer}/data/PartialInstancerTest3-VF.ttx (100%) rename Tests/varLib/{ => instancer}/data/test_results/PartialInstancerTest2-VF-instance-100,100.ttx (100%) rename Tests/varLib/{ => instancer}/data/test_results/PartialInstancerTest2-VF-instance-100,62.5.ttx (100%) rename Tests/varLib/{ => instancer}/data/test_results/PartialInstancerTest2-VF-instance-400,100.ttx (100%) rename Tests/varLib/{ => instancer}/data/test_results/PartialInstancerTest2-VF-instance-400,62.5.ttx (100%) rename Tests/varLib/{ => instancer}/data/test_results/PartialInstancerTest2-VF-instance-900,100.ttx (100%) rename Tests/varLib/{ => instancer}/data/test_results/PartialInstancerTest2-VF-instance-900,62.5.ttx (100%) rename Tests/varLib/{ => instancer}/data/test_results/PartialInstancerTest3-VF-instance-400-no-overlap-flags.ttx (100%) rename Tests/varLib/{ => instancer}/data/test_results/PartialInstancerTest3-VF-instance-400-no-overlaps.ttx (100%) rename Tests/varLib/{ => instancer}/data/test_results/PartialInstancerTest3-VF-instance-700-no-overlaps.ttx (100%) rename Tests/varLib/{ => instancer}/instancer_test.py (86%) create mode 100644 Tests/varLib/instancer/names_test.py diff --git a/Lib/fontTools/varLib/instancer.py b/Lib/fontTools/varLib/instancer/__init__.py similarity index 76% rename from Lib/fontTools/varLib/instancer.py rename to Lib/fontTools/varLib/instancer/__init__.py index 581f52ee6..aa64cc798 100644 --- a/Lib/fontTools/varLib/instancer.py +++ b/Lib/fontTools/varLib/instancer/__init__.py @@ -84,6 +84,7 @@ from fontTools import subset # noqa: F401 from fontTools.varLib import builder from fontTools.varLib.mvar import MVAR_ENTRIES from fontTools.varLib.merger import MutatorMerger +from fontTools.varLib.instancer import names from contextlib import contextmanager import collections from copy import deepcopy @@ -128,21 +129,6 @@ class OverlapMode(IntEnum): REMOVE = 2 -class NameID(IntEnum): - FAMILY_NAME = 1 - SUBFAMILY_NAME = 2 - UNIQUE_FONT_IDENTIFIER = 3 - FULL_FONT_NAME = 4 - VERSION_STRING = 5 - POSTSCRIPT_NAME = 6 - TYPOGRAPHIC_FAMILY_NAME = 16 - TYPOGRAPHIC_SUBFAMILY_NAME = 17 - VARIATIONS_POSTSCRIPT_NAME_PREFIX = 25 - - -ELIDABLE_AXIS_VALUE_NAME = 2 - - def instantiateTupleVariationStore( variations, axisLimits, origCoords=None, endPts=None ): @@ -1073,423 +1059,6 @@ def axisValuesFromAxisLimits(stat, axisLimits): return newAxisValueTables -def getVariationNameIDs(varfont): - used = [] - if "fvar" in varfont: - fvar = varfont["fvar"] - for axis in fvar.axes: - used.append(axis.axisNameID) - for instance in fvar.instances: - used.append(instance.subfamilyNameID) - if instance.postscriptNameID != 0xFFFF: - used.append(instance.postscriptNameID) - if "STAT" in varfont: - stat = varfont["STAT"].table - for axis in stat.DesignAxisRecord.Axis if stat.DesignAxisRecord else (): - used.append(axis.AxisNameID) - for value in stat.AxisValueArray.AxisValue if stat.AxisValueArray else (): - used.append(value.ValueNameID) - # nameIDs <= 255 are reserved by OT spec so we don't touch them - return {nameID for nameID in used if nameID > 255} - - -@contextmanager -def pruningUnusedNames(varfont): - origNameIDs = getVariationNameIDs(varfont) - - yield - - log.info("Pruning name table") - exclude = origNameIDs - getVariationNameIDs(varfont) - varfont["name"].names[:] = [ - record for record in varfont["name"].names if record.nameID not in exclude - ] - if "ltag" in varfont: - # Drop the whole 'ltag' table if all the language-dependent Unicode name - # records that reference it have been dropped. - # TODO: Only prune unused ltag tags, renumerating langIDs accordingly. - # Note ltag can also be used by feat or morx tables, so check those too. - if not any( - record - for record in varfont["name"].names - if record.platformID == 0 and record.langID != 0xFFFF - ): - del varfont["ltag"] - - -def updateNameTable(varfont, axisLimits): - """Update an instatiated variable font's name table using the Axis - Values from the STAT table. - - The updated name table will conform to the R/I/B/BI naming model. - """ - # This task can be split into two parts: - - # Task 1: Collecting and sorting the relevant AxisValues: - # 1. First check the variable font has a STAT table and it contains - # AxisValues. - # 2. Create a dictionary which contains the pinned axes from the - # axisLimits dict and for the unpinned axes, we'll use the fvar - # default coordinates e.g - # axisLimits = {"wght": 500, "wdth": AxisRange(75, 100), our dict will - # be {"wght": 500, "wdth": 100} if the width axis has a default of 100. - # 3. Create a new list of AxisValues whose Values match the dict we just - # created. - # 4. Remove any AxisValues from the list which have the - # Elidable_AXIS_VALUE_NAME flag set. - # 5. Remove and sort AxisValues in the list so format 4 AxisValues take - # precedence. According to the MS Spec "if a format 1, format 2 or - # format 3 table has a (nominal) value used in a format 4 table that - # also has values for other axes, the format 4 table, being the more - # specific match, is used", - # https://docs.microsoft.com/en-us/typography/opentype/spec/stat#axis-value-table-format-4 - - # Task 2: Updating a name table's style and family names from a list of - # AxisValues: - # 1. Sort AxisValues into two groups. For the first group, the names must be - # any of the following ["Regular", "Italic", "Bold", "Bold Italic"]. - # This group of names is often referred to as "RIBBI" names. For the - # other group, names must be non-RIBBI e.g "Medium Italic", "Condensed" - # etc. - # 2. Repeat the next steps for each name table record platform: - # a. Create new subFamily name and Typographic subFamily name from the - # above groups. - # b. Update nameIDs 1, 2, 3, 4, 6, 16, 17 using the new name created - # in the last step. - # - # Step by step example: - # A variable font which has a width and weight axes. - # AxisValues in font (represented as simplified dicts): - # axisValues = [ - # {"name": "Light", "axis": "wght", "value": 300}, - # {"name": "Regular", "axis": "wght", "value": 400}, - # {"name": "Medium", "axis": "wght", "value": 500}, - # {"name": "Bold", "axis": "wght", "value": 600}, - # {"name": "Condensed", "axis": "wdth", "value": 75}, - # {"name": "Normal", "axis": "wdth", "value": 100, "flags": 0x2}, - # ] - # # Let's instantiate a partial font which has a pinned wght axis and an - # unpinned width axis. - # >>> axisLimits = {"wght": 500, "width": AxisRange(75, 100)} - # >>> updateNameTable(varfont, axisLimits) - # - # AxisValues remaining after task 1.3: - # axisValues = [ - # {"name": "Medium", "axis": "wght", "value": 500}, - # {"name": "Normal", "axis": "wdth", "value": 100, "flags": 0x2} - # ] - # - # AxisValues remaining after completing all 1.x tasks: - # axisValues = [{"name": "Medium", "axis": "wght", "value": 500}] - # The Normal AxisValue is removed because it has the - # Elidable_AXIS_VALUE_NAME flag set. - # - # # AxisValues after separating into two groups in task 2.1: - # ribbiAxisValues = [] - # nonRibbiAxisValues = [{"name": "Medium", "axis": "wght", "value": 500}] - # - # # Names created from AxisValues in task 2.2a for Win US English platform: - # subFamilyName = "" - # typoSubFamilyName = "Medium" - # - # NameRecords updated in task 2.2b for Win US English platform: - # NameID 1 familyName: "Open Sans" --> "Open Sans Medium" - # NameID 2 subFamilyName: "Regular" --> "Regular" - # NameID 3 Unique font identifier: "3.000;GOOG;OpenSans-Regular" --> \ - # "3.000;GOOG;OpenSans-Medium" - # NameID 4 Full font name: "Open Sans Regular" --> "Open Sans Medium" - # NameID 6 PostScript name: "OpenSans-Regular" --> "OpenSans-Medium" - # NameID 16 Typographic Family name: None --> "Open Sans" - # NameID 17 Typographic Subfamily name: None --> "Medium" - # - # Notes on name table record updates: - # - Typographic names have been added since Medium is a non-Ribbi name. - # - Neither the before or after name records include the Width AxisValue - # names because the "Normal" AxisValue has the - # Elidable_AXIS_VALUE_NAME flag set. - # If we instantiate the same font but pin the wdth axis to 75, - # the "Condensed" AxisValue will be included. - # - For info regarding how RIBBI and non-RIBBI can be constructed see: - # https://docs.microsoft.com/en-us/typography/opentype/spec/name#name-ids - - if "STAT" not in varfont: - raise ValueError("Cannot update name table since there is no STAT table.") - stat = varfont["STAT"].table - if not stat.AxisValueArray: - raise ValueError("Cannot update name table since there are no STAT Axis Values") - fvar = varfont["fvar"] - - # The updated name table must reflect the new 'zero origin' of the font. - # If we're instantiating a partial font, we will populate the unpinned - # axes with their default axis values. - fvarDefaults = {a.axisTag: a.defaultValue for a in fvar.axes} - defaultAxisCoords = deepcopy(axisLimits) - for axisTag, val in fvarDefaults.items(): - if axisTag not in defaultAxisCoords or isinstance( - defaultAxisCoords[axisTag], AxisRange - ): - defaultAxisCoords[axisTag] = val - - axisValueTables = axisValuesFromAxisLimits(stat, defaultAxisCoords) - checkAxisValuesExist(stat, axisValueTables, defaultAxisCoords) - - # Remove axis Values which have ELIDABLE_AXIS_VALUE_NAME flag set. - # Axis Values which have this flag enabled won't be visible in - # application font menus. - axisValueTables = [ - v for v in axisValueTables if not v.Flags & ELIDABLE_AXIS_VALUE_NAME - ] - axisValueTables = _sortAxisValues(axisValueTables) - _updateNameRecords(varfont, axisValueTables) - - -def checkAxisValuesExist(stat, axisValues, axisCoords): - seen = set() - designAxes = stat.DesignAxisRecord.Axis - for axisValueTable in axisValues: - axisValueFormat = axisValueTable.Format - if axisValueTable.Format in (1, 2, 3): - axisTag = designAxes[axisValueTable.AxisIndex].AxisTag - if axisValueFormat == 2: - axisValue = axisValueTable.NominalValue - else: - axisValue = axisValueTable.Value - if axisTag in axisCoords and axisValue == axisCoords[axisTag]: - seen.add(axisTag) - elif axisValueTable.Format == 4: - for rec in axisValueTable.AxisValueRecord: - axisTag = designAxes[rec.AxisIndex].AxisTag - if axisTag in axisCoords and rec.Value == axisCoords[axisTag]: - seen.add(axisTag) - - missingAxes = set(axisCoords) - seen - if missingAxes: - missing = ", ".join(f"'{i}={axisCoords[i]}'" for i in missingAxes) - raise ValueError(f"Cannot find Axis Values [{missing}]") - - -def _sortAxisValues(axisValues): - # Sort and remove duplicates ensuring that format 4 Axis Values - # are dominant - results = [] - seenAxes = set() - # Sort format 4 axes so the tables with the most AxisValueRecords - # are first - format4 = sorted( - [v for v in axisValues if v.Format == 4], - key=lambda v: len(v.AxisValueRecord), - reverse=True, - ) - nonFormat4 = [v for v in axisValues if v not in format4] - - for val in format4: - axisIndexes = set(r.AxisIndex for r in val.AxisValueRecord) - minIndex = min(axisIndexes) - if not seenAxes & axisIndexes: - seenAxes |= axisIndexes - results.append((minIndex, val)) - - for val in nonFormat4: - axisIndex = val.AxisIndex - if axisIndex not in seenAxes: - seenAxes.add(axisIndex) - results.append((axisIndex, val)) - - return [axisValue for _, axisValue in sorted(results)] - - -def _updateNameRecords(varfont, axisValues): - # Update nametable based on the axisValues using the R/I/B/BI model. - nametable = varfont["name"] - stat = varfont["STAT"].table - - axisValueNameIDs = [a.ValueNameID for a in axisValues] - ribbiNameIDs = [n for n in axisValueNameIDs if nameIDIsRibbi(nametable, n)] - nonRibbiNameIDs = [n for n in axisValueNameIDs if n not in ribbiNameIDs] - elidedNameID = stat.ElidedFallbackNameID - elidedNameIsRibbi = nameIDIsRibbi(nametable, elidedNameID) - - getName = nametable.getName - platforms = set((r.platformID, r.platEncID, r.langID) for r in nametable.names) - for platform in platforms: - if not all(getName(i, *platform) for i in (1, 2, elidedNameID)): - # Since no family name and subfamily name records were found, - # we cannot update this set of name Records. - continue - - subFamilyName = " ".join( - getName(n, *platform).toUnicode() for n in ribbiNameIDs - ) - typoSubFamilyName = " ".join( - getName(n, *platform).toUnicode() - for n in axisValueNameIDs - if nonRibbiNameIDs - ) - - # If neither subFamilyName and typographic SubFamilyName exist, - # we will use the STAT's elidedFallbackName - if not typoSubFamilyName and not subFamilyName: - if elidedNameIsRibbi: - subFamilyName = getName(elidedNameID, *platform).toUnicode() - else: - typoSubFamilyName = getName(elidedNameID, *platform).toUnicode() - - familyNameSuffix = " ".join( - getName(n, *platform).toUnicode() for n in nonRibbiNameIDs - ) - - _updateNameTableStyleRecords( - varfont, - familyNameSuffix, - subFamilyName, - typoSubFamilyName, - *platform, - ) - - -def nameIDIsRibbi(nametable, nameID): - engNameRecords = any( - r - for r in nametable.names - if (r.platformID, r.platEncID, r.langID) == (3, 1, 0x409) - ) - if not engNameRecords: - raise ValueError( - f"Canot determine if there are RIBBI Axis Value Tables " - "since there are no name table Records which have " - "platformID=3, platEncID=1, langID=0x409" - ) - return ( - True - if nametable.getName(nameID, 3, 1, 0x409).toUnicode() - in ("Regular", "Italic", "Bold", "Bold Italic") - else False - ) - - -def _updateNameTableStyleRecords( - varfont, - familyNameSuffix, - subFamilyName, - typoSubFamilyName, - platformID=3, - platEncID=1, - langID=0x409, -): - # TODO (Marc F) It may be nice to make this part a standalone - # font renamer in the future. - nametable = varfont["name"] - platform = (platformID, platEncID, langID) - - currentFamilyName = nametable.getName( - NameID.TYPOGRAPHIC_FAMILY_NAME, *platform - ) or nametable.getName(NameID.FAMILY_NAME, *platform) - - currentStyleName = nametable.getName( - NameID.TYPOGRAPHIC_SUBFAMILY_NAME, *platform - ) or nametable.getName(NameID.SUBFAMILY_NAME, *platform) - - currentFamilyName = currentFamilyName.toUnicode() - currentStyleName = currentStyleName.toUnicode() - - nameIDs = { - NameID.FAMILY_NAME: currentFamilyName, - NameID.SUBFAMILY_NAME: subFamilyName, - } - if typoSubFamilyName: - nameIDs[NameID.FAMILY_NAME] = f"{currentFamilyName} {familyNameSuffix}".strip() - nameIDs[NameID.TYPOGRAPHIC_FAMILY_NAME] = currentFamilyName - nameIDs[NameID.TYPOGRAPHIC_SUBFAMILY_NAME] = f"{typoSubFamilyName}" - # Remove previous Typographic Family and SubFamily names since they're - # no longer required - else: - for nameID in ( - NameID.TYPOGRAPHIC_FAMILY_NAME, - NameID.TYPOGRAPHIC_SUBFAMILY_NAME, - ): - nametable.removeNames(nameID=nameID) - - newFamilyName = ( - nameIDs.get(NameID.TYPOGRAPHIC_FAMILY_NAME) or nameIDs[NameID.FAMILY_NAME] - ) - newStyleName = ( - nameIDs.get(NameID.TYPOGRAPHIC_SUBFAMILY_NAME) or nameIDs[NameID.SUBFAMILY_NAME] - ) - - nameIDs[NameID.FULL_FONT_NAME] = f"{newFamilyName} {newStyleName}" - nameIDs[NameID.POSTSCRIPT_NAME] = _updatePSNameRecord( - varfont, newFamilyName, newStyleName, platform - ) - nameIDs[NameID.UNIQUE_FONT_IDENTIFIER] = _updateUniqueIdNameRecord( - varfont, nameIDs, platform - ) - - for nameID, string in nameIDs.items(): - if not string: - continue - nametable.setName(string, nameID, *platform) - - -def _updatePSNameRecord(varfont, familyName, styleName, platform): - # Implementation based on Adobe Technical Note #5902 : - # https://wwwimages2.adobe.com/content/dam/acom/en/devnet/font/pdfs/5902.AdobePSNameGeneration.pdf - nametable = varfont["name"] - - family_prefix = nametable.getName( - NameID.VARIATIONS_POSTSCRIPT_NAME_PREFIX, *platform - ) - if family_prefix: - family_prefix = familyPrefix.toUnicode() - else: - family_prefix = familyName - - psName = f"{family_prefix}-{styleName}" - # Remove any characters other than uppercase Latin letters, lowercase - # Latin letters, digits and hyphens. - psName = re.sub(r"[^A-Za-z0-9-]", r"", psName) - - if len(psName) > 127: - # Abbreviating the stylename so it fits within 127 characters whilst - # conforming to every vendor's specification is too complex. Instead - # we simply truncate the psname and add the required "..." - return f"{psName[:124]}..." - return psName - - -def _updateUniqueIdNameRecord(varfont, nameIDs, platform): - nametable = varfont["name"] - currentRecord = nametable.getName(NameID.UNIQUE_FONT_IDENTIFIER, *platform) - if not currentRecord: - return None - - # Check if full name and postscript name are a substring of currentRecord - for nameID in (NameID.FULL_FONT_NAME, NameID.POSTSCRIPT_NAME): - nameRecord = nametable.getName(nameID, *platform) - if not nameRecord: - continue - if currentRecord.toUnicode() in nameRecord.toUnicode(): - return currentRecord.toUnicode().replace( - nameRecord.toUnicode(), nameIDs[nameRecord.nameID] - ) - # Create a new string since we couldn't find any substrings. - fontVersion = _fontVersion(varfont, platform) - achVendID = varfont["OS/2"].achVendID - # Remove non-ASCII characers and trailing spaces - vendor = re.sub(r"[^\x00-\x7F]", "", achVendID).strip() - psName = nameIDs[NameID.POSTSCRIPT_NAME] - return f"{fontVersion};{vendor};{psName}" - - -def _fontVersion(font, platform=(3, 1, 0x409)): - nameRecord = font["name"].getName(NameID.VERSION_STRING, *platform) - if nameRecord is None: - return f'{font["head"].fontRevision:.3f}' - # "Version 1.101; ttfautohint (v1.8.1.43-b0c9)" --> "1.101" - # Also works fine with inputs "Version 1.101" or "1.101" etc - versionNumber = nameRecord.toUnicode().split(";")[0] - return versionNumber.lstrip("Version ").strip() - - def setMacOverlapFlags(glyfTable): flagOverlapCompound = _g_l_y_f.OVERLAP_COMPOUND flagOverlapSimple = _g_l_y_f.flagOverlapSimple @@ -1633,7 +1202,7 @@ def instantiateVariableFont( if updateFontNames: log.info("Updating name table") - updateNameTable(varfont, axisLimits) + names.updateNameTable(varfont, axisLimits) if "gvar" in varfont: instantiateGvar(varfont, normalizedLimits, optimize=optimize) @@ -1657,7 +1226,7 @@ def instantiateVariableFont( if "avar" in varfont: instantiateAvar(varfont, axisLimits) - with pruningUnusedNames(varfont): + with names.pruningUnusedNames(varfont): if "STAT" in varfont: instantiateSTAT(varfont, axisLimits) @@ -1851,9 +1420,3 @@ def main(args=None): outfile, ) varfont.save(outfile) - - -if __name__ == "__main__": - import sys - - sys.exit(main()) diff --git a/Lib/fontTools/varLib/instancer/__main__.py b/Lib/fontTools/varLib/instancer/__main__.py new file mode 100644 index 000000000..64ffff2b9 --- /dev/null +++ b/Lib/fontTools/varLib/instancer/__main__.py @@ -0,0 +1,5 @@ +import sys +from fontTools.varLib.instancer import main + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Lib/fontTools/varLib/instancer/names.py b/Lib/fontTools/varLib/instancer/names.py new file mode 100644 index 000000000..7cd7515c5 --- /dev/null +++ b/Lib/fontTools/varLib/instancer/names.py @@ -0,0 +1,441 @@ +"""Helpers for instantiating name table records.""" + +from contextlib import contextmanager +from copy import deepcopy +from enum import IntEnum +import re + + +class NameID(IntEnum): + FAMILY_NAME = 1 + SUBFAMILY_NAME = 2 + UNIQUE_FONT_IDENTIFIER = 3 + FULL_FONT_NAME = 4 + VERSION_STRING = 5 + POSTSCRIPT_NAME = 6 + TYPOGRAPHIC_FAMILY_NAME = 16 + TYPOGRAPHIC_SUBFAMILY_NAME = 17 + VARIATIONS_POSTSCRIPT_NAME_PREFIX = 25 + + +ELIDABLE_AXIS_VALUE_NAME = 2 + + +def getVariationNameIDs(varfont): + used = [] + if "fvar" in varfont: + fvar = varfont["fvar"] + for axis in fvar.axes: + used.append(axis.axisNameID) + for instance in fvar.instances: + used.append(instance.subfamilyNameID) + if instance.postscriptNameID != 0xFFFF: + used.append(instance.postscriptNameID) + if "STAT" in varfont: + stat = varfont["STAT"].table + for axis in stat.DesignAxisRecord.Axis if stat.DesignAxisRecord else (): + used.append(axis.AxisNameID) + for value in stat.AxisValueArray.AxisValue if stat.AxisValueArray else (): + used.append(value.ValueNameID) + # nameIDs <= 255 are reserved by OT spec so we don't touch them + return {nameID for nameID in used if nameID > 255} + + +@contextmanager +def pruningUnusedNames(varfont): + from . import log + + origNameIDs = getVariationNameIDs(varfont) + + yield + + log.info("Pruning name table") + exclude = origNameIDs - getVariationNameIDs(varfont) + varfont["name"].names[:] = [ + record for record in varfont["name"].names if record.nameID not in exclude + ] + if "ltag" in varfont: + # Drop the whole 'ltag' table if all the language-dependent Unicode name + # records that reference it have been dropped. + # TODO: Only prune unused ltag tags, renumerating langIDs accordingly. + # Note ltag can also be used by feat or morx tables, so check those too. + if not any( + record + for record in varfont["name"].names + if record.platformID == 0 and record.langID != 0xFFFF + ): + del varfont["ltag"] + + +def updateNameTable(varfont, axisLimits): + """Update an instatiated variable font's name table using the Axis + Values from the STAT table. + + The updated name table will conform to the R/I/B/BI naming model. + """ + # This task can be split into two parts: + + # Task 1: Collecting and sorting the relevant AxisValues: + # 1. First check the variable font has a STAT table and it contains + # AxisValues. + # 2. Create a dictionary which contains the pinned axes from the + # axisLimits dict and for the unpinned axes, we'll use the fvar + # default coordinates e.g + # axisLimits = {"wght": 500, "wdth": AxisRange(75, 100), our dict will + # be {"wght": 500, "wdth": 100} if the width axis has a default of 100. + # 3. Create a new list of AxisValues whose Values match the dict we just + # created. + # 4. Remove any AxisValues from the list which have the + # Elidable_AXIS_VALUE_NAME flag set. + # 5. Remove and sort AxisValues in the list so format 4 AxisValues take + # precedence. According to the MS Spec "if a format 1, format 2 or + # format 3 table has a (nominal) value used in a format 4 table that + # also has values for other axes, the format 4 table, being the more + # specific match, is used", + # https://docs.microsoft.com/en-us/typography/opentype/spec/stat#axis-value-table-format-4 + + # Task 2: Updating a name table's style and family names from a list of + # AxisValues: + # 1. Sort AxisValues into two groups. For the first group, the names must be + # any of the following ["Regular", "Italic", "Bold", "Bold Italic"]. + # This group of names is often referred to as "RIBBI" names. For the + # other group, names must be non-RIBBI e.g "Medium Italic", "Condensed" + # etc. + # 2. Repeat the next steps for each name table record platform: + # a. Create new subFamily name and Typographic subFamily name from the + # above groups. + # b. Update nameIDs 1, 2, 3, 4, 6, 16, 17 using the new name created + # in the last step. + # + # Step by step example: + # A variable font which has a width and weight axes. + # AxisValues in font (represented as simplified dicts): + # axisValues = [ + # {"name": "Light", "axis": "wght", "value": 300}, + # {"name": "Regular", "axis": "wght", "value": 400}, + # {"name": "Medium", "axis": "wght", "value": 500}, + # {"name": "Bold", "axis": "wght", "value": 600}, + # {"name": "Condensed", "axis": "wdth", "value": 75}, + # {"name": "Normal", "axis": "wdth", "value": 100, "flags": 0x2}, + # ] + # # Let's instantiate a partial font which has a pinned wght axis and an + # unpinned width axis. + # >>> axisLimits = {"wght": 500, "width": AxisRange(75, 100)} + # >>> updateNameTable(varfont, axisLimits) + # + # AxisValues remaining after task 1.3: + # axisValues = [ + # {"name": "Medium", "axis": "wght", "value": 500}, + # {"name": "Normal", "axis": "wdth", "value": 100, "flags": 0x2} + # ] + # + # AxisValues remaining after completing all 1.x tasks: + # axisValues = [{"name": "Medium", "axis": "wght", "value": 500}] + # The Normal AxisValue is removed because it has the + # Elidable_AXIS_VALUE_NAME flag set. + # + # # AxisValues after separating into two groups in task 2.1: + # ribbiAxisValues = [] + # nonRibbiAxisValues = [{"name": "Medium", "axis": "wght", "value": 500}] + # + # # Names created from AxisValues in task 2.2a for Win US English platform: + # subFamilyName = "" + # typoSubFamilyName = "Medium" + # + # NameRecords updated in task 2.2b for Win US English platform: + # NameID 1 familyName: "Open Sans" --> "Open Sans Medium" + # NameID 2 subFamilyName: "Regular" --> "Regular" + # NameID 3 Unique font identifier: "3.000;GOOG;OpenSans-Regular" --> \ + # "3.000;GOOG;OpenSans-Medium" + # NameID 4 Full font name: "Open Sans Regular" --> "Open Sans Medium" + # NameID 6 PostScript name: "OpenSans-Regular" --> "OpenSans-Medium" + # NameID 16 Typographic Family name: None --> "Open Sans" + # NameID 17 Typographic Subfamily name: None --> "Medium" + # + # Notes on name table record updates: + # - Typographic names have been added since Medium is a non-Ribbi name. + # - Neither the before or after name records include the Width AxisValue + # names because the "Normal" AxisValue has the + # Elidable_AXIS_VALUE_NAME flag set. + # If we instantiate the same font but pin the wdth axis to 75, + # the "Condensed" AxisValue will be included. + # - For info regarding how RIBBI and non-RIBBI can be constructed see: + # https://docs.microsoft.com/en-us/typography/opentype/spec/name#name-ids + from . import AxisRange, axisValuesFromAxisLimits + + if "STAT" not in varfont: + raise ValueError("Cannot update name table since there is no STAT table.") + stat = varfont["STAT"].table + if not stat.AxisValueArray: + raise ValueError("Cannot update name table since there are no STAT Axis Values") + fvar = varfont["fvar"] + + # The updated name table must reflect the new 'zero origin' of the font. + # If we're instantiating a partial font, we will populate the unpinned + # axes with their default axis values. + fvarDefaults = {a.axisTag: a.defaultValue for a in fvar.axes} + defaultAxisCoords = deepcopy(axisLimits) + for axisTag, val in fvarDefaults.items(): + if axisTag not in defaultAxisCoords or isinstance( + defaultAxisCoords[axisTag], AxisRange + ): + defaultAxisCoords[axisTag] = val + + axisValueTables = axisValuesFromAxisLimits(stat, defaultAxisCoords) + checkAxisValuesExist(stat, axisValueTables, defaultAxisCoords) + + # Remove axis Values which have ELIDABLE_AXIS_VALUE_NAME flag set. + # Axis Values which have this flag enabled won't be visible in + # application font menus. + axisValueTables = [ + v for v in axisValueTables if not v.Flags & ELIDABLE_AXIS_VALUE_NAME + ] + axisValueTables = _sortAxisValues(axisValueTables) + _updateNameRecords(varfont, axisValueTables) + + +def checkAxisValuesExist(stat, axisValues, axisCoords): + seen = set() + designAxes = stat.DesignAxisRecord.Axis + for axisValueTable in axisValues: + axisValueFormat = axisValueTable.Format + if axisValueTable.Format in (1, 2, 3): + axisTag = designAxes[axisValueTable.AxisIndex].AxisTag + if axisValueFormat == 2: + axisValue = axisValueTable.NominalValue + else: + axisValue = axisValueTable.Value + if axisTag in axisCoords and axisValue == axisCoords[axisTag]: + seen.add(axisTag) + elif axisValueTable.Format == 4: + for rec in axisValueTable.AxisValueRecord: + axisTag = designAxes[rec.AxisIndex].AxisTag + if axisTag in axisCoords and rec.Value == axisCoords[axisTag]: + seen.add(axisTag) + + missingAxes = set(axisCoords) - seen + if missingAxes: + missing = ", ".join(f"'{i}={axisCoords[i]}'" for i in missingAxes) + raise ValueError(f"Cannot find Axis Values [{missing}]") + + +def _sortAxisValues(axisValues): + # Sort and remove duplicates ensuring that format 4 Axis Values + # are dominant + results = [] + seenAxes = set() + # Sort format 4 axes so the tables with the most AxisValueRecords + # are first + format4 = sorted( + [v for v in axisValues if v.Format == 4], + key=lambda v: len(v.AxisValueRecord), + reverse=True, + ) + nonFormat4 = [v for v in axisValues if v not in format4] + + for val in format4: + axisIndexes = set(r.AxisIndex for r in val.AxisValueRecord) + minIndex = min(axisIndexes) + if not seenAxes & axisIndexes: + seenAxes |= axisIndexes + results.append((minIndex, val)) + + for val in nonFormat4: + axisIndex = val.AxisIndex + if axisIndex not in seenAxes: + seenAxes.add(axisIndex) + results.append((axisIndex, val)) + + return [axisValue for _, axisValue in sorted(results)] + + +def _updateNameRecords(varfont, axisValues): + # Update nametable based on the axisValues using the R/I/B/BI model. + nametable = varfont["name"] + stat = varfont["STAT"].table + + axisValueNameIDs = [a.ValueNameID for a in axisValues] + ribbiNameIDs = [n for n in axisValueNameIDs if nameIDIsRibbi(nametable, n)] + nonRibbiNameIDs = [n for n in axisValueNameIDs if n not in ribbiNameIDs] + elidedNameID = stat.ElidedFallbackNameID + elidedNameIsRibbi = nameIDIsRibbi(nametable, elidedNameID) + + getName = nametable.getName + platforms = set((r.platformID, r.platEncID, r.langID) for r in nametable.names) + for platform in platforms: + if not all(getName(i, *platform) for i in (1, 2, elidedNameID)): + # Since no family name and subfamily name records were found, + # we cannot update this set of name Records. + continue + + subFamilyName = " ".join( + getName(n, *platform).toUnicode() for n in ribbiNameIDs + ) + typoSubFamilyName = " ".join( + getName(n, *platform).toUnicode() + for n in axisValueNameIDs + if nonRibbiNameIDs + ) + + # If neither subFamilyName and typographic SubFamilyName exist, + # we will use the STAT's elidedFallbackName + if not typoSubFamilyName and not subFamilyName: + if elidedNameIsRibbi: + subFamilyName = getName(elidedNameID, *platform).toUnicode() + else: + typoSubFamilyName = getName(elidedNameID, *platform).toUnicode() + + familyNameSuffix = " ".join( + getName(n, *platform).toUnicode() for n in nonRibbiNameIDs + ) + + _updateNameTableStyleRecords( + varfont, + familyNameSuffix, + subFamilyName, + typoSubFamilyName, + *platform, + ) + + +def nameIDIsRibbi(nametable, nameID): + engNameRecords = any( + r + for r in nametable.names + if (r.platformID, r.platEncID, r.langID) == (3, 1, 0x409) + ) + if not engNameRecords: + raise ValueError( + f"Canot determine if there are RIBBI Axis Value Tables " + "since there are no name table Records which have " + "platformID=3, platEncID=1, langID=0x409" + ) + return ( + True + if nametable.getName(nameID, 3, 1, 0x409).toUnicode() + in ("Regular", "Italic", "Bold", "Bold Italic") + else False + ) + + +def _updateNameTableStyleRecords( + varfont, + familyNameSuffix, + subFamilyName, + typoSubFamilyName, + platformID=3, + platEncID=1, + langID=0x409, +): + # TODO (Marc F) It may be nice to make this part a standalone + # font renamer in the future. + nametable = varfont["name"] + platform = (platformID, platEncID, langID) + + currentFamilyName = nametable.getName( + NameID.TYPOGRAPHIC_FAMILY_NAME, *platform + ) or nametable.getName(NameID.FAMILY_NAME, *platform) + + currentStyleName = nametable.getName( + NameID.TYPOGRAPHIC_SUBFAMILY_NAME, *platform + ) or nametable.getName(NameID.SUBFAMILY_NAME, *platform) + + currentFamilyName = currentFamilyName.toUnicode() + currentStyleName = currentStyleName.toUnicode() + + nameIDs = { + NameID.FAMILY_NAME: currentFamilyName, + NameID.SUBFAMILY_NAME: subFamilyName, + } + if typoSubFamilyName: + nameIDs[NameID.FAMILY_NAME] = f"{currentFamilyName} {familyNameSuffix}".strip() + nameIDs[NameID.TYPOGRAPHIC_FAMILY_NAME] = currentFamilyName + nameIDs[NameID.TYPOGRAPHIC_SUBFAMILY_NAME] = f"{typoSubFamilyName}" + # Remove previous Typographic Family and SubFamily names since they're + # no longer required + else: + for nameID in ( + NameID.TYPOGRAPHIC_FAMILY_NAME, + NameID.TYPOGRAPHIC_SUBFAMILY_NAME, + ): + nametable.removeNames(nameID=nameID) + + newFamilyName = ( + nameIDs.get(NameID.TYPOGRAPHIC_FAMILY_NAME) or nameIDs[NameID.FAMILY_NAME] + ) + newStyleName = ( + nameIDs.get(NameID.TYPOGRAPHIC_SUBFAMILY_NAME) or nameIDs[NameID.SUBFAMILY_NAME] + ) + + nameIDs[NameID.FULL_FONT_NAME] = f"{newFamilyName} {newStyleName}" + nameIDs[NameID.POSTSCRIPT_NAME] = _updatePSNameRecord( + varfont, newFamilyName, newStyleName, platform + ) + nameIDs[NameID.UNIQUE_FONT_IDENTIFIER] = _updateUniqueIdNameRecord( + varfont, nameIDs, platform + ) + + for nameID, string in nameIDs.items(): + if not string: + continue + nametable.setName(string, nameID, *platform) + + +def _updatePSNameRecord(varfont, familyName, styleName, platform): + # Implementation based on Adobe Technical Note #5902 : + # https://wwwimages2.adobe.com/content/dam/acom/en/devnet/font/pdfs/5902.AdobePSNameGeneration.pdf + nametable = varfont["name"] + + family_prefix = nametable.getName( + NameID.VARIATIONS_POSTSCRIPT_NAME_PREFIX, *platform + ) + if family_prefix: + family_prefix = family_prefix.toUnicode() + else: + family_prefix = familyName + + psName = f"{family_prefix}-{styleName}" + # Remove any characters other than uppercase Latin letters, lowercase + # Latin letters, digits and hyphens. + psName = re.sub(r"[^A-Za-z0-9-]", r"", psName) + + if len(psName) > 127: + # Abbreviating the stylename so it fits within 127 characters whilst + # conforming to every vendor's specification is too complex. Instead + # we simply truncate the psname and add the required "..." + return f"{psName[:124]}..." + return psName + + +def _updateUniqueIdNameRecord(varfont, nameIDs, platform): + nametable = varfont["name"] + currentRecord = nametable.getName(NameID.UNIQUE_FONT_IDENTIFIER, *platform) + if not currentRecord: + return None + + # Check if full name and postscript name are a substring of currentRecord + for nameID in (NameID.FULL_FONT_NAME, NameID.POSTSCRIPT_NAME): + nameRecord = nametable.getName(nameID, *platform) + if not nameRecord: + continue + if currentRecord.toUnicode() in nameRecord.toUnicode(): + return currentRecord.toUnicode().replace( + nameRecord.toUnicode(), nameIDs[nameRecord.nameID] + ) + # Create a new string since we couldn't find any substrings. + fontVersion = _fontVersion(varfont, platform) + achVendID = varfont["OS/2"].achVendID + # Remove non-ASCII characers and trailing spaces + vendor = re.sub(r"[^\x00-\x7F]", "", achVendID).strip() + psName = nameIDs[NameID.POSTSCRIPT_NAME] + return f"{fontVersion};{vendor};{psName}" + + +def _fontVersion(font, platform=(3, 1, 0x409)): + nameRecord = font["name"].getName(NameID.VERSION_STRING, *platform) + if nameRecord is None: + return f'{font["head"].fontRevision:.3f}' + # "Version 1.101; ttfautohint (v1.8.1.43-b0c9)" --> "1.101" + # Also works fine with inputs "Version 1.101" or "1.101" etc + versionNumber = nameRecord.toUnicode().split(";")[0] + return versionNumber.lstrip("Version ").strip() diff --git a/Tests/varLib/instancer/conftest.py b/Tests/varLib/instancer/conftest.py new file mode 100644 index 000000000..0ac8091df --- /dev/null +++ b/Tests/varLib/instancer/conftest.py @@ -0,0 +1,13 @@ +import os +from fontTools import ttLib +import pytest + + +TESTDATA = os.path.join(os.path.dirname(__file__), "data") + + +@pytest.fixture +def varfont(): + f = ttLib.TTFont() + f.importXML(os.path.join(TESTDATA, "PartialInstancerTest-VF.ttx")) + return f diff --git a/Tests/varLib/data/PartialInstancerTest-VF.ttx b/Tests/varLib/instancer/data/PartialInstancerTest-VF.ttx similarity index 100% rename from Tests/varLib/data/PartialInstancerTest-VF.ttx rename to Tests/varLib/instancer/data/PartialInstancerTest-VF.ttx diff --git a/Tests/varLib/data/PartialInstancerTest2-VF.ttx b/Tests/varLib/instancer/data/PartialInstancerTest2-VF.ttx similarity index 100% rename from Tests/varLib/data/PartialInstancerTest2-VF.ttx rename to Tests/varLib/instancer/data/PartialInstancerTest2-VF.ttx diff --git a/Tests/varLib/data/PartialInstancerTest3-VF.ttx b/Tests/varLib/instancer/data/PartialInstancerTest3-VF.ttx similarity index 100% rename from Tests/varLib/data/PartialInstancerTest3-VF.ttx rename to Tests/varLib/instancer/data/PartialInstancerTest3-VF.ttx diff --git a/Tests/varLib/data/test_results/PartialInstancerTest2-VF-instance-100,100.ttx b/Tests/varLib/instancer/data/test_results/PartialInstancerTest2-VF-instance-100,100.ttx similarity index 100% rename from Tests/varLib/data/test_results/PartialInstancerTest2-VF-instance-100,100.ttx rename to Tests/varLib/instancer/data/test_results/PartialInstancerTest2-VF-instance-100,100.ttx diff --git a/Tests/varLib/data/test_results/PartialInstancerTest2-VF-instance-100,62.5.ttx b/Tests/varLib/instancer/data/test_results/PartialInstancerTest2-VF-instance-100,62.5.ttx similarity index 100% rename from Tests/varLib/data/test_results/PartialInstancerTest2-VF-instance-100,62.5.ttx rename to Tests/varLib/instancer/data/test_results/PartialInstancerTest2-VF-instance-100,62.5.ttx diff --git a/Tests/varLib/data/test_results/PartialInstancerTest2-VF-instance-400,100.ttx b/Tests/varLib/instancer/data/test_results/PartialInstancerTest2-VF-instance-400,100.ttx similarity index 100% rename from Tests/varLib/data/test_results/PartialInstancerTest2-VF-instance-400,100.ttx rename to Tests/varLib/instancer/data/test_results/PartialInstancerTest2-VF-instance-400,100.ttx diff --git a/Tests/varLib/data/test_results/PartialInstancerTest2-VF-instance-400,62.5.ttx b/Tests/varLib/instancer/data/test_results/PartialInstancerTest2-VF-instance-400,62.5.ttx similarity index 100% rename from Tests/varLib/data/test_results/PartialInstancerTest2-VF-instance-400,62.5.ttx rename to Tests/varLib/instancer/data/test_results/PartialInstancerTest2-VF-instance-400,62.5.ttx diff --git a/Tests/varLib/data/test_results/PartialInstancerTest2-VF-instance-900,100.ttx b/Tests/varLib/instancer/data/test_results/PartialInstancerTest2-VF-instance-900,100.ttx similarity index 100% rename from Tests/varLib/data/test_results/PartialInstancerTest2-VF-instance-900,100.ttx rename to Tests/varLib/instancer/data/test_results/PartialInstancerTest2-VF-instance-900,100.ttx diff --git a/Tests/varLib/data/test_results/PartialInstancerTest2-VF-instance-900,62.5.ttx b/Tests/varLib/instancer/data/test_results/PartialInstancerTest2-VF-instance-900,62.5.ttx similarity index 100% rename from Tests/varLib/data/test_results/PartialInstancerTest2-VF-instance-900,62.5.ttx rename to Tests/varLib/instancer/data/test_results/PartialInstancerTest2-VF-instance-900,62.5.ttx diff --git a/Tests/varLib/data/test_results/PartialInstancerTest3-VF-instance-400-no-overlap-flags.ttx b/Tests/varLib/instancer/data/test_results/PartialInstancerTest3-VF-instance-400-no-overlap-flags.ttx similarity index 100% rename from Tests/varLib/data/test_results/PartialInstancerTest3-VF-instance-400-no-overlap-flags.ttx rename to Tests/varLib/instancer/data/test_results/PartialInstancerTest3-VF-instance-400-no-overlap-flags.ttx diff --git a/Tests/varLib/data/test_results/PartialInstancerTest3-VF-instance-400-no-overlaps.ttx b/Tests/varLib/instancer/data/test_results/PartialInstancerTest3-VF-instance-400-no-overlaps.ttx similarity index 100% rename from Tests/varLib/data/test_results/PartialInstancerTest3-VF-instance-400-no-overlaps.ttx rename to Tests/varLib/instancer/data/test_results/PartialInstancerTest3-VF-instance-400-no-overlaps.ttx diff --git a/Tests/varLib/data/test_results/PartialInstancerTest3-VF-instance-700-no-overlaps.ttx b/Tests/varLib/instancer/data/test_results/PartialInstancerTest3-VF-instance-700-no-overlaps.ttx similarity index 100% rename from Tests/varLib/data/test_results/PartialInstancerTest3-VF-instance-700-no-overlaps.ttx rename to Tests/varLib/instancer/data/test_results/PartialInstancerTest3-VF-instance-700-no-overlaps.ttx diff --git a/Tests/varLib/instancer_test.py b/Tests/varLib/instancer/instancer_test.py similarity index 86% rename from Tests/varLib/instancer_test.py rename to Tests/varLib/instancer/instancer_test.py index e4fc81bc9..c3e0729be 100644 --- a/Tests/varLib/instancer_test.py +++ b/Tests/varLib/instancer/instancer_test.py @@ -7,7 +7,6 @@ from fontTools.ttLib.tables import _f_v_a_r, _g_l_y_f from fontTools.ttLib.tables import otTables from fontTools.ttLib.tables.TupleVariation import TupleVariation from fontTools import varLib -from fontTools.otlLib.builder import buildStatTable from fontTools.varLib import instancer from fontTools.varLib.mvar import MVAR_ENTRIES from fontTools.varLib import builder @@ -21,16 +20,11 @@ import re import pytest +# see Tests/varLib/instancer/conftest.py for "varfont" fixture definition + TESTDATA = os.path.join(os.path.dirname(__file__), "data") -@pytest.fixture -def varfont(): - f = ttLib.TTFont() - f.importXML(os.path.join(TESTDATA, "PartialInstancerTest-VF.ttx")) - return f - - @pytest.fixture(params=[True, False], ids=["optimize", "no-optimize"]) def optimize(request): return request.param @@ -1342,30 +1336,6 @@ class InstantiateSTATTest(object): assert axisValue in varfont2["STAT"].table.AxisValueArray.AxisValue -def test_pruningUnusedNames(varfont): - varNameIDs = instancer.getVariationNameIDs(varfont) - - assert varNameIDs == set(range(256, 297 + 1)) - - fvar = varfont["fvar"] - stat = varfont["STAT"].table - - with instancer.pruningUnusedNames(varfont): - del fvar.axes[0] # Weight (nameID=256) - del fvar.instances[0] # Thin (nameID=258) - del stat.DesignAxisRecord.Axis[0] # Weight (nameID=256) - del stat.AxisValueArray.AxisValue[0] # Thin (nameID=258) - - assert not any(n for n in varfont["name"].names if n.nameID in {256, 258}) - - with instancer.pruningUnusedNames(varfont): - del varfont["fvar"] - del varfont["STAT"] - - assert not any(n for n in varfont["name"].names if n.nameID in varNameIDs) - assert "ltag" not in varfont - - def test_setMacOverlapFlags(): flagOverlapCompound = _g_l_y_f.OVERLAP_COMPOUND flagOverlapSimple = _g_l_y_f.flagOverlapSimple @@ -1917,284 +1887,6 @@ def test_normalizeAxisLimits_missing_from_fvar(varfont): instancer.normalizeAxisLimits(varfont, {"ZZZZ": 1000}) -def _test_name_records(varfont, expected, isNonRIBBI, platforms=[0x409]): - nametable = varfont["name"] - font_names = { - (r.nameID, r.platformID, r.platEncID, r.langID): r.toUnicode() - for r in nametable.names - } - for k in expected: - if k[-1] not in platforms: - continue - assert font_names[k] == expected[k] - if isNonRIBBI: - font_nameids = set(i[0] for i in font_names) - assert 16 in font_nameids - assert 17 in font_nameids - - -@pytest.mark.parametrize( - "limits, expected, isNonRIBBI", - [ - # Regular - ( - {"wght": 400}, - { - (1, 3, 1, 0x409): "Test Variable Font", - (2, 3, 1, 0x409): "Regular", - (3, 3, 1, 0x409): "2.001;GOOG;TestVariableFont-Regular", - (6, 3, 1, 0x409): "TestVariableFont-Regular", - }, - False, - ), - # Regular Normal (width axis Normal isn't included since it is elided) - ( - {"wght": 400, "wdth": 100}, - { - (1, 3, 1, 0x409): "Test Variable Font", - (2, 3, 1, 0x409): "Regular", - (3, 3, 1, 0x409): "2.001;GOOG;TestVariableFont-Regular", - (6, 3, 1, 0x409): "TestVariableFont-Regular", - }, - False, - ), - # Black - ( - {"wght": 900}, - { - (1, 3, 1, 0x409): "Test Variable Font Black", - (2, 3, 1, 0x409): "Regular", - (3, 3, 1, 0x409): "2.001;GOOG;TestVariableFont-Black", - (6, 3, 1, 0x409): "TestVariableFont-Black", - (16, 3, 1, 0x409): "Test Variable Font", - (17, 3, 1, 0x409): "Black", - }, - True, - ), - # Thin - ( - {"wght": 100}, - { - (1, 3, 1, 0x409): "Test Variable Font Thin", - (2, 3, 1, 0x409): "Regular", - (3, 3, 1, 0x409): "2.001;GOOG;TestVariableFont-Thin", - (6, 3, 1, 0x409): "TestVariableFont-Thin", - (16, 3, 1, 0x409): "Test Variable Font", - (17, 3, 1, 0x409): "Thin", - }, - True, - ), - # Thin Condensed - ( - {"wght": 100, "wdth": 79}, - { - (1, 3, 1, 0x409): "Test Variable Font Thin Condensed", - (2, 3, 1, 0x409): "Regular", - (3, 3, 1, 0x409): "2.001;GOOG;TestVariableFont-ThinCondensed", - (6, 3, 1, 0x409): "TestVariableFont-ThinCondensed", - (16, 3, 1, 0x409): "Test Variable Font", - (17, 3, 1, 0x409): "Thin Condensed", - }, - True, - ), - # Condensed with unpinned weights - ( - {"wdth": 79, "wght": instancer.AxisRange(400, 900)}, - { - (1, 3, 1, 0x409): "Test Variable Font Condensed", - (2, 3, 1, 0x409): "Regular", - (3, 3, 1, 0x409): "2.001;GOOG;TestVariableFont-Condensed", - (6, 3, 1, 0x409): "TestVariableFont-Condensed", - (16, 3, 1, 0x409): "Test Variable Font", - (17, 3, 1, 0x409): "Condensed", - }, - True, - ), - ], -) -def test_updateNameTable_with_registered_axes_ribbi( - varfont, limits, expected, isNonRIBBI -): - instancer.updateNameTable(varfont, limits) - _test_name_records(varfont, expected, isNonRIBBI) - - -def test_updatetNameTable_axis_order(varfont): - axes = [ - dict( - tag="wght", - name="Weight", - values=[ - dict(value=400, name="Regular"), - ], - ), - dict( - tag="wdth", - name="Width", - values=[ - dict(value=75, name="Condensed"), - ], - ), - ] - nametable = varfont["name"] - buildStatTable(varfont, axes) - instancer.updateNameTable(varfont, {"wdth": 75, "wght": 400}) - assert nametable.getName(17, 3, 1, 0x409).toUnicode() == "Regular Condensed" - - # Swap the axes so the names get swapped - axes[0], axes[1] = axes[1], axes[0] - - buildStatTable(varfont, axes) - instancer.updateNameTable(varfont, {"wdth": 75, "wght": 400}) - assert nametable.getName(17, 3, 1, 0x409).toUnicode() == "Condensed Regular" - - -@pytest.mark.parametrize( - "limits, expected, isNonRIBBI", - [ - # Regular | Normal - ( - {"wght": 400}, - { - (1, 3, 1, 0x409): "Test Variable Font", - (2, 3, 1, 0x409): "Normal", - }, - False, - ), - # Black | Negreta - ( - {"wght": 900}, - { - (1, 3, 1, 0x409): "Test Variable Font Negreta", - (2, 3, 1, 0x409): "Normal", - (16, 3, 1, 0x409): "Test Variable Font", - (17, 3, 1, 0x409): "Negreta", - }, - True, - ), - # Black Condensed | Negreta Zhuštěné - ( - {"wght": 900, "wdth": 79}, - { - (1, 3, 1, 0x409): "Test Variable Font Negreta Zhuštěné", - (2, 3, 1, 0x409): "Normal", - (16, 3, 1, 0x409): "Test Variable Font", - (17, 3, 1, 0x409): "Negreta Zhuštěné", - }, - True, - ), - ], -) -def test_updateNameTable_with_multilingual_names(varfont, limits, expected, isNonRIBBI): - name = varfont["name"] - # langID 0x405 is the Czech Windows langID - name.setName("Test Variable Font", 1, 3, 1, 0x405) - name.setName("Normal", 2, 3, 1, 0x405) - name.setName("Normal", 261, 3, 1, 0x405) # nameID 261=Regular STAT entry - name.setName("Negreta", 266, 3, 1, 0x405) # nameID 266=Black STAT entry - name.setName("Zhuštěné", 279, 3, 1, 0x405) # nameID 279=Condensed STAT entry - - instancer.updateNameTable(varfont, limits) - names = _test_name_records(varfont, expected, isNonRIBBI, platforms=[0x405]) - - -def test_updateNameTable_missing_axisValues(varfont): - with pytest.raises(ValueError, match="Cannot find Axis Values \['wght=200'\]"): - instancer.updateNameTable(varfont, {"wght": 200}) - - -def test_updateNameTable_missing_stat(varfont): - del varfont["STAT"] - with pytest.raises( - ValueError, match="Cannot update name table since there is no STAT table." - ): - instancer.updateNameTable(varfont, {"wght": 400}) - - -@pytest.mark.parametrize( - "limits, expected, isNonRIBBI", - [ - # Regular | Normal - ( - {"wght": 400}, - { - (1, 3, 1, 0x409): "Test Variable Font", - (2, 3, 1, 0x409): "Italic", - (6, 3, 1, 0x409): "TestVariableFont-Italic", - }, - False, - ), - # Black Condensed Italic - ( - {"wght": 900, "wdth": 79}, - { - (1, 3, 1, 0x409): "Test Variable Font Black Condensed", - (2, 3, 1, 0x409): "Italic", - (6, 3, 1, 0x409): "TestVariableFont-BlackCondensedItalic", - (16, 3, 1, 0x409): "Test Variable Font", - (17, 3, 1, 0x409): "Black Condensed Italic", - }, - True, - ), - ], -) -def test_updateNameTable_vf_with_italic_attribute( - varfont, limits, expected, isNonRIBBI -): - font_link_axisValue = varfont["STAT"].table.AxisValueArray.AxisValue[4] - # Unset ELIDABLE_AXIS_VALUE_NAME flag - font_link_axisValue.Flags &= ~instancer.ELIDABLE_AXIS_VALUE_NAME - font_link_axisValue.ValueNameID = 294 # Roman --> Italic - - instancer.updateNameTable(varfont, limits) - names = _test_name_records(varfont, expected, isNonRIBBI) - - -def test_updateNameTable_format4_axisValues(varfont): - # format 4 axisValues should dominate the other axisValues - stat = varfont["STAT"].table - - axisValue = otTables.AxisValue() - axisValue.Format = 4 - axisValue.Flags = 0 - varfont["name"].setName("Dominant Value", 297, 3, 1, 0x409) - axisValue.ValueNameID = 297 - axisValue.AxisValueRecord = [] - for tag, value in (("wght", 900), ("wdth", 79)): - rec = otTables.AxisValueRecord() - rec.AxisIndex = next( - i for i, a in enumerate(stat.DesignAxisRecord.Axis) if a.AxisTag == tag - ) - rec.Value = value - axisValue.AxisValueRecord.append(rec) - stat.AxisValueArray.AxisValue.append(axisValue) - - instancer.updateNameTable(varfont, {"wdth": 79, "wght": 900}) - expected = { - (1, 3, 1, 0x409): "Test Variable Font Dominant Value", - (2, 3, 1, 0x409): "Regular", - (16, 3, 1, 0x409): "Test Variable Font", - (17, 3, 1, 0x409): "Dominant Value", - } - _test_name_records(varfont, expected, isNonRIBBI=True) - - -def test_updateNameTable_elided_axisValues(varfont): - stat = varfont["STAT"].table - # set ELIDABLE_AXIS_VALUE_NAME flag for all axisValues - for axisValue in stat.AxisValueArray.AxisValue: - axisValue.Flags |= instancer.ELIDABLE_AXIS_VALUE_NAME - - stat.ElidedFallbackNameID = 266 # Regular --> Black - instancer.updateNameTable(varfont, {"wght": 400}) - # Since all axis values are elided, the elided fallback name - # must be used to construct the style names. Since we - # changed it to Black, we need both a typoSubFamilyName and - # the subFamilyName set so it conforms to the RIBBI model. - expected = {(2, 3, 1, 0x409): "Regular", (17, 3, 1, 0x409): "Black"} - _test_name_records(varfont, expected, isNonRIBBI=True) - - def test_sanityCheckVariableTables(varfont): font = ttLib.TTFont() with pytest.raises(ValueError, match="Missing required table fvar"): diff --git a/Tests/varLib/instancer/names_test.py b/Tests/varLib/instancer/names_test.py new file mode 100644 index 000000000..27446e373 --- /dev/null +++ b/Tests/varLib/instancer/names_test.py @@ -0,0 +1,307 @@ +from fontTools.ttLib.tables import otTables +from fontTools.otlLib.builder import buildStatTable +from fontTools.varLib import instancer + +import pytest + + +def test_pruningUnusedNames(varfont): + varNameIDs = instancer.names.getVariationNameIDs(varfont) + + assert varNameIDs == set(range(256, 297 + 1)) + + fvar = varfont["fvar"] + stat = varfont["STAT"].table + + with instancer.names.pruningUnusedNames(varfont): + del fvar.axes[0] # Weight (nameID=256) + del fvar.instances[0] # Thin (nameID=258) + del stat.DesignAxisRecord.Axis[0] # Weight (nameID=256) + del stat.AxisValueArray.AxisValue[0] # Thin (nameID=258) + + assert not any(n for n in varfont["name"].names if n.nameID in {256, 258}) + + with instancer.names.pruningUnusedNames(varfont): + del varfont["fvar"] + del varfont["STAT"] + + assert not any(n for n in varfont["name"].names if n.nameID in varNameIDs) + assert "ltag" not in varfont + + +def _test_name_records(varfont, expected, isNonRIBBI, platforms=[0x409]): + nametable = varfont["name"] + font_names = { + (r.nameID, r.platformID, r.platEncID, r.langID): r.toUnicode() + for r in nametable.names + } + for k in expected: + if k[-1] not in platforms: + continue + assert font_names[k] == expected[k] + if isNonRIBBI: + font_nameids = set(i[0] for i in font_names) + assert 16 in font_nameids + assert 17 in font_nameids + + +@pytest.mark.parametrize( + "limits, expected, isNonRIBBI", + [ + # Regular + ( + {"wght": 400}, + { + (1, 3, 1, 0x409): "Test Variable Font", + (2, 3, 1, 0x409): "Regular", + (3, 3, 1, 0x409): "2.001;GOOG;TestVariableFont-Regular", + (6, 3, 1, 0x409): "TestVariableFont-Regular", + }, + False, + ), + # Regular Normal (width axis Normal isn't included since it is elided) + ( + {"wght": 400, "wdth": 100}, + { + (1, 3, 1, 0x409): "Test Variable Font", + (2, 3, 1, 0x409): "Regular", + (3, 3, 1, 0x409): "2.001;GOOG;TestVariableFont-Regular", + (6, 3, 1, 0x409): "TestVariableFont-Regular", + }, + False, + ), + # Black + ( + {"wght": 900}, + { + (1, 3, 1, 0x409): "Test Variable Font Black", + (2, 3, 1, 0x409): "Regular", + (3, 3, 1, 0x409): "2.001;GOOG;TestVariableFont-Black", + (6, 3, 1, 0x409): "TestVariableFont-Black", + (16, 3, 1, 0x409): "Test Variable Font", + (17, 3, 1, 0x409): "Black", + }, + True, + ), + # Thin + ( + {"wght": 100}, + { + (1, 3, 1, 0x409): "Test Variable Font Thin", + (2, 3, 1, 0x409): "Regular", + (3, 3, 1, 0x409): "2.001;GOOG;TestVariableFont-Thin", + (6, 3, 1, 0x409): "TestVariableFont-Thin", + (16, 3, 1, 0x409): "Test Variable Font", + (17, 3, 1, 0x409): "Thin", + }, + True, + ), + # Thin Condensed + ( + {"wght": 100, "wdth": 79}, + { + (1, 3, 1, 0x409): "Test Variable Font Thin Condensed", + (2, 3, 1, 0x409): "Regular", + (3, 3, 1, 0x409): "2.001;GOOG;TestVariableFont-ThinCondensed", + (6, 3, 1, 0x409): "TestVariableFont-ThinCondensed", + (16, 3, 1, 0x409): "Test Variable Font", + (17, 3, 1, 0x409): "Thin Condensed", + }, + True, + ), + # Condensed with unpinned weights + ( + {"wdth": 79, "wght": instancer.AxisRange(400, 900)}, + { + (1, 3, 1, 0x409): "Test Variable Font Condensed", + (2, 3, 1, 0x409): "Regular", + (3, 3, 1, 0x409): "2.001;GOOG;TestVariableFont-Condensed", + (6, 3, 1, 0x409): "TestVariableFont-Condensed", + (16, 3, 1, 0x409): "Test Variable Font", + (17, 3, 1, 0x409): "Condensed", + }, + True, + ), + ], +) +def test_updateNameTable_with_registered_axes_ribbi( + varfont, limits, expected, isNonRIBBI +): + instancer.names.updateNameTable(varfont, limits) + _test_name_records(varfont, expected, isNonRIBBI) + + +def test_updatetNameTable_axis_order(varfont): + axes = [ + dict( + tag="wght", + name="Weight", + values=[ + dict(value=400, name="Regular"), + ], + ), + dict( + tag="wdth", + name="Width", + values=[ + dict(value=75, name="Condensed"), + ], + ), + ] + nametable = varfont["name"] + buildStatTable(varfont, axes) + instancer.names.updateNameTable(varfont, {"wdth": 75, "wght": 400}) + assert nametable.getName(17, 3, 1, 0x409).toUnicode() == "Regular Condensed" + + # Swap the axes so the names get swapped + axes[0], axes[1] = axes[1], axes[0] + + buildStatTable(varfont, axes) + instancer.names.updateNameTable(varfont, {"wdth": 75, "wght": 400}) + assert nametable.getName(17, 3, 1, 0x409).toUnicode() == "Condensed Regular" + + +@pytest.mark.parametrize( + "limits, expected, isNonRIBBI", + [ + # Regular | Normal + ( + {"wght": 400}, + { + (1, 3, 1, 0x409): "Test Variable Font", + (2, 3, 1, 0x409): "Normal", + }, + False, + ), + # Black | Negreta + ( + {"wght": 900}, + { + (1, 3, 1, 0x409): "Test Variable Font Negreta", + (2, 3, 1, 0x409): "Normal", + (16, 3, 1, 0x409): "Test Variable Font", + (17, 3, 1, 0x409): "Negreta", + }, + True, + ), + # Black Condensed | Negreta Zhuštěné + ( + {"wght": 900, "wdth": 79}, + { + (1, 3, 1, 0x409): "Test Variable Font Negreta Zhuštěné", + (2, 3, 1, 0x409): "Normal", + (16, 3, 1, 0x409): "Test Variable Font", + (17, 3, 1, 0x409): "Negreta Zhuštěné", + }, + True, + ), + ], +) +def test_updateNameTable_with_multilingual_names(varfont, limits, expected, isNonRIBBI): + name = varfont["name"] + # langID 0x405 is the Czech Windows langID + name.setName("Test Variable Font", 1, 3, 1, 0x405) + name.setName("Normal", 2, 3, 1, 0x405) + name.setName("Normal", 261, 3, 1, 0x405) # nameID 261=Regular STAT entry + name.setName("Negreta", 266, 3, 1, 0x405) # nameID 266=Black STAT entry + name.setName("Zhuštěné", 279, 3, 1, 0x405) # nameID 279=Condensed STAT entry + + instancer.names.updateNameTable(varfont, limits) + _test_name_records(varfont, expected, isNonRIBBI, platforms=[0x405]) + + +def test_updateNameTable_missing_axisValues(varfont): + with pytest.raises(ValueError, match="Cannot find Axis Values \['wght=200'\]"): + instancer.names.updateNameTable(varfont, {"wght": 200}) + + +def test_updateNameTable_missing_stat(varfont): + del varfont["STAT"] + with pytest.raises( + ValueError, match="Cannot update name table since there is no STAT table." + ): + instancer.names.updateNameTable(varfont, {"wght": 400}) + + +@pytest.mark.parametrize( + "limits, expected, isNonRIBBI", + [ + # Regular | Normal + ( + {"wght": 400}, + { + (1, 3, 1, 0x409): "Test Variable Font", + (2, 3, 1, 0x409): "Italic", + (6, 3, 1, 0x409): "TestVariableFont-Italic", + }, + False, + ), + # Black Condensed Italic + ( + {"wght": 900, "wdth": 79}, + { + (1, 3, 1, 0x409): "Test Variable Font Black Condensed", + (2, 3, 1, 0x409): "Italic", + (6, 3, 1, 0x409): "TestVariableFont-BlackCondensedItalic", + (16, 3, 1, 0x409): "Test Variable Font", + (17, 3, 1, 0x409): "Black Condensed Italic", + }, + True, + ), + ], +) +def test_updateNameTable_vf_with_italic_attribute( + varfont, limits, expected, isNonRIBBI +): + font_link_axisValue = varfont["STAT"].table.AxisValueArray.AxisValue[4] + # Unset ELIDABLE_AXIS_VALUE_NAME flag + font_link_axisValue.Flags &= ~instancer.names.ELIDABLE_AXIS_VALUE_NAME + font_link_axisValue.ValueNameID = 294 # Roman --> Italic + + instancer.names.updateNameTable(varfont, limits) + _test_name_records(varfont, expected, isNonRIBBI) + + +def test_updateNameTable_format4_axisValues(varfont): + # format 4 axisValues should dominate the other axisValues + stat = varfont["STAT"].table + + axisValue = otTables.AxisValue() + axisValue.Format = 4 + axisValue.Flags = 0 + varfont["name"].setName("Dominant Value", 297, 3, 1, 0x409) + axisValue.ValueNameID = 297 + axisValue.AxisValueRecord = [] + for tag, value in (("wght", 900), ("wdth", 79)): + rec = otTables.AxisValueRecord() + rec.AxisIndex = next( + i for i, a in enumerate(stat.DesignAxisRecord.Axis) if a.AxisTag == tag + ) + rec.Value = value + axisValue.AxisValueRecord.append(rec) + stat.AxisValueArray.AxisValue.append(axisValue) + + instancer.names.updateNameTable(varfont, {"wdth": 79, "wght": 900}) + expected = { + (1, 3, 1, 0x409): "Test Variable Font Dominant Value", + (2, 3, 1, 0x409): "Regular", + (16, 3, 1, 0x409): "Test Variable Font", + (17, 3, 1, 0x409): "Dominant Value", + } + _test_name_records(varfont, expected, isNonRIBBI=True) + + +def test_updateNameTable_elided_axisValues(varfont): + stat = varfont["STAT"].table + # set ELIDABLE_AXIS_VALUE_NAME flag for all axisValues + for axisValue in stat.AxisValueArray.AxisValue: + axisValue.Flags |= instancer.names.ELIDABLE_AXIS_VALUE_NAME + + stat.ElidedFallbackNameID = 266 # Regular --> Black + instancer.names.updateNameTable(varfont, {"wght": 400}) + # Since all axis values are elided, the elided fallback name + # must be used to construct the style names. Since we + # changed it to Black, we need both a typoSubFamilyName and + # the subFamilyName set so it conforms to the RIBBI model. + expected = {(2, 3, 1, 0x409): "Regular", (17, 3, 1, 0x409): "Black"} + _test_name_records(varfont, expected, isNonRIBBI=True) From fcc02826b47f4cd349d37af7825b8e1da63e0988 Mon Sep 17 00:00:00 2001 From: Marc Foley Date: Tue, 16 Feb 2021 09:36:15 +0000 Subject: [PATCH 23/28] Implement Cosimo feedback from previous pr --- Lib/fontTools/varLib/instancer/__init__.py | 6 +- Lib/fontTools/varLib/instancer/names.py | 156 +++++++-------------- 2 files changed, 55 insertions(+), 107 deletions(-) diff --git a/Lib/fontTools/varLib/instancer/__init__.py b/Lib/fontTools/varLib/instancer/__init__.py index aa64cc798..4f00ed7be 100644 --- a/Lib/fontTools/varLib/instancer/__init__.py +++ b/Lib/fontTools/varLib/instancer/__init__.py @@ -1180,11 +1180,11 @@ def instantiateVariableFont( contours and components, you can pass OverlapMode.REMOVE. Note that this requires the skia-pathops package (available to pip install). The overlap parameter only has effect when generating full static instances. - updateFontNames (bool): if True, update the instantiated font's nametable using + updateFontNames (bool): if True, update the instantiated font's name table using the Axis Value Tables from the STAT table. The name table will be updated so it conforms to the R/I/B/BI model. If the STAT table is missing or - an Axis Value table is missing for a given axis coordinate, an Error will be - raised. + an Axis Value table is missing for a given axis coordinate, a ValueError will + be raised. """ # 'overlap' used to be bool and is now enum; for backward compat keep accepting bool overlap = OverlapMode(int(overlap)) diff --git a/Lib/fontTools/varLib/instancer/names.py b/Lib/fontTools/varLib/instancer/names.py index 7cd7515c5..ed15fe2d0 100644 --- a/Lib/fontTools/varLib/instancer/names.py +++ b/Lib/fontTools/varLib/instancer/names.py @@ -68,99 +68,38 @@ def pruningUnusedNames(varfont): def updateNameTable(varfont, axisLimits): - """Update an instatiated variable font's name table using the Axis - Values from the STAT table. + """Update an instatiated variable font's name table using the + AxisValues from the STAT table. The updated name table will conform to the R/I/B/BI naming model. + R/I/B/BI is an acronym for (Regular, Italic, Bold, Bold Italic) font + styles. + + This task can be split into two parts: + + Task 1: Collect and sort the relevant AxisValues into a new list which + only includes AxisValues whose coordinates match the new default + axis locations. We also skip any AxisValues which are elided. + + Task 2: Update the name table's style and family names records using the + AxisValues found in step 1. The MS spec provides further info for applying + the R/I/B/BI model to each name record: + https://docs.microsoft.com/en-us/typography/opentype/spec/name#name-ids + + Example: Updating a partial variable font: + | >>> ttFont = TTFont("OpenSans[wdth,wght].ttf") + | >>> updateNameTable(ttFont, {"wght": AxisRange(400, 900), "wdth": 75}) + + The name table records will be updated in the following manner: + NameID 1 familyName: "Open Sans" --> "Open Sans Condensed" + NameID 2 subFamilyName: "Regular" --> "Regular" + NameID 3 Unique font identifier: "3.000;GOOG;OpenSans-Regular" --> \ + "3.000;GOOG;OpenSans-Condensed" + NameID 4 Full font name: "Open Sans Regular" --> "Open Sans Condensed" + NameID 6 PostScript name: "OpenSans-Regular" --> "OpenSans-Condensed" + NameID 16 Typographic Family name: None --> "Open Sans" + NameID 17 Typographic Subfamily name: None --> "Condensed" """ - # This task can be split into two parts: - - # Task 1: Collecting and sorting the relevant AxisValues: - # 1. First check the variable font has a STAT table and it contains - # AxisValues. - # 2. Create a dictionary which contains the pinned axes from the - # axisLimits dict and for the unpinned axes, we'll use the fvar - # default coordinates e.g - # axisLimits = {"wght": 500, "wdth": AxisRange(75, 100), our dict will - # be {"wght": 500, "wdth": 100} if the width axis has a default of 100. - # 3. Create a new list of AxisValues whose Values match the dict we just - # created. - # 4. Remove any AxisValues from the list which have the - # Elidable_AXIS_VALUE_NAME flag set. - # 5. Remove and sort AxisValues in the list so format 4 AxisValues take - # precedence. According to the MS Spec "if a format 1, format 2 or - # format 3 table has a (nominal) value used in a format 4 table that - # also has values for other axes, the format 4 table, being the more - # specific match, is used", - # https://docs.microsoft.com/en-us/typography/opentype/spec/stat#axis-value-table-format-4 - - # Task 2: Updating a name table's style and family names from a list of - # AxisValues: - # 1. Sort AxisValues into two groups. For the first group, the names must be - # any of the following ["Regular", "Italic", "Bold", "Bold Italic"]. - # This group of names is often referred to as "RIBBI" names. For the - # other group, names must be non-RIBBI e.g "Medium Italic", "Condensed" - # etc. - # 2. Repeat the next steps for each name table record platform: - # a. Create new subFamily name and Typographic subFamily name from the - # above groups. - # b. Update nameIDs 1, 2, 3, 4, 6, 16, 17 using the new name created - # in the last step. - # - # Step by step example: - # A variable font which has a width and weight axes. - # AxisValues in font (represented as simplified dicts): - # axisValues = [ - # {"name": "Light", "axis": "wght", "value": 300}, - # {"name": "Regular", "axis": "wght", "value": 400}, - # {"name": "Medium", "axis": "wght", "value": 500}, - # {"name": "Bold", "axis": "wght", "value": 600}, - # {"name": "Condensed", "axis": "wdth", "value": 75}, - # {"name": "Normal", "axis": "wdth", "value": 100, "flags": 0x2}, - # ] - # # Let's instantiate a partial font which has a pinned wght axis and an - # unpinned width axis. - # >>> axisLimits = {"wght": 500, "width": AxisRange(75, 100)} - # >>> updateNameTable(varfont, axisLimits) - # - # AxisValues remaining after task 1.3: - # axisValues = [ - # {"name": "Medium", "axis": "wght", "value": 500}, - # {"name": "Normal", "axis": "wdth", "value": 100, "flags": 0x2} - # ] - # - # AxisValues remaining after completing all 1.x tasks: - # axisValues = [{"name": "Medium", "axis": "wght", "value": 500}] - # The Normal AxisValue is removed because it has the - # Elidable_AXIS_VALUE_NAME flag set. - # - # # AxisValues after separating into two groups in task 2.1: - # ribbiAxisValues = [] - # nonRibbiAxisValues = [{"name": "Medium", "axis": "wght", "value": 500}] - # - # # Names created from AxisValues in task 2.2a for Win US English platform: - # subFamilyName = "" - # typoSubFamilyName = "Medium" - # - # NameRecords updated in task 2.2b for Win US English platform: - # NameID 1 familyName: "Open Sans" --> "Open Sans Medium" - # NameID 2 subFamilyName: "Regular" --> "Regular" - # NameID 3 Unique font identifier: "3.000;GOOG;OpenSans-Regular" --> \ - # "3.000;GOOG;OpenSans-Medium" - # NameID 4 Full font name: "Open Sans Regular" --> "Open Sans Medium" - # NameID 6 PostScript name: "OpenSans-Regular" --> "OpenSans-Medium" - # NameID 16 Typographic Family name: None --> "Open Sans" - # NameID 17 Typographic Subfamily name: None --> "Medium" - # - # Notes on name table record updates: - # - Typographic names have been added since Medium is a non-Ribbi name. - # - Neither the before or after name records include the Width AxisValue - # names because the "Normal" AxisValue has the - # Elidable_AXIS_VALUE_NAME flag set. - # If we instantiate the same font but pin the wdth axis to 75, - # the "Condensed" AxisValue will be included. - # - For info regarding how RIBBI and non-RIBBI can be constructed see: - # https://docs.microsoft.com/en-us/typography/opentype/spec/name#name-ids from . import AxisRange, axisValuesFromAxisLimits if "STAT" not in varfont: @@ -170,7 +109,7 @@ def updateNameTable(varfont, axisLimits): raise ValueError("Cannot update name table since there are no STAT Axis Values") fvar = varfont["fvar"] - # The updated name table must reflect the new 'zero origin' of the font. + # The updated name table will reflect the new 'zero origin' of the font. # If we're instantiating a partial font, we will populate the unpinned # axes with their default axis values. fvarDefaults = {a.axisTag: a.defaultValue for a in fvar.axes} @@ -184,8 +123,8 @@ def updateNameTable(varfont, axisLimits): axisValueTables = axisValuesFromAxisLimits(stat, defaultAxisCoords) checkAxisValuesExist(stat, axisValueTables, defaultAxisCoords) - # Remove axis Values which have ELIDABLE_AXIS_VALUE_NAME flag set. - # Axis Values which have this flag enabled won't be visible in + # Ignore axis Values which have ELIDABLE_AXIS_VALUE_NAME flag set. + # AxisValues which have this flag enabled won't be visible in # application font menus. axisValueTables = [ v for v in axisValueTables if not v.Flags & ELIDABLE_AXIS_VALUE_NAME @@ -220,8 +159,12 @@ def checkAxisValuesExist(stat, axisValues, axisCoords): def _sortAxisValues(axisValues): - # Sort and remove duplicates ensuring that format 4 Axis Values - # are dominant + # Sort and remove duplicates and ensure that format 4 AxisValues + # are dominant. We need format 4 AxisValues to be dominant because the + # MS Spec states, "if a format 1, format 2 or format 3 table has a + # (nominal) value used in a format 4 table that also has values for + # other axes, the format 4 table, being the more specific match, is used", + # https://docs.microsoft.com/en-us/typography/opentype/spec/stat#axis-value-table-format-4 results = [] seenAxes = set() # Sort format 4 axes so the tables with the most AxisValueRecords @@ -255,10 +198,10 @@ def _updateNameRecords(varfont, axisValues): stat = varfont["STAT"].table axisValueNameIDs = [a.ValueNameID for a in axisValues] - ribbiNameIDs = [n for n in axisValueNameIDs if nameIDIsRibbi(nametable, n)] + ribbiNameIDs = [n for n in axisValueNameIDs if _isRibbi(nametable, n)] nonRibbiNameIDs = [n for n in axisValueNameIDs if n not in ribbiNameIDs] elidedNameID = stat.ElidedFallbackNameID - elidedNameIsRibbi = nameIDIsRibbi(nametable, elidedNameID) + elidedNameIsRibbi = _isRibbi(nametable, elidedNameID) getName = nametable.getName platforms = set((r.platformID, r.platEncID, r.langID) for r in nametable.names) @@ -271,11 +214,13 @@ def _updateNameRecords(varfont, axisValues): subFamilyName = " ".join( getName(n, *platform).toUnicode() for n in ribbiNameIDs ) - typoSubFamilyName = " ".join( - getName(n, *platform).toUnicode() - for n in axisValueNameIDs - if nonRibbiNameIDs - ) + if nonRibbiNameIDs: + typoSubFamilyName = " ".join( + getName(n, *platform).toUnicode() + for n in axisValueNameIDs + ) + else: + typoSubFamilyName = None # If neither subFamilyName and typographic SubFamilyName exist, # we will use the STAT's elidedFallbackName @@ -298,7 +243,7 @@ def _updateNameRecords(varfont, axisValues): ) -def nameIDIsRibbi(nametable, nameID): +def _isRibbi(nametable, nameID): engNameRecords = any( r for r in nametable.names @@ -306,7 +251,7 @@ def nameIDIsRibbi(nametable, nameID): ) if not engNameRecords: raise ValueError( - f"Canot determine if there are RIBBI Axis Value Tables " + f"Cannot determine if there are RIBBI Axis Value Tables " "since there are no name table Records which have " "platformID=3, platEncID=1, langID=0x409" ) @@ -340,6 +285,9 @@ def _updateNameTableStyleRecords( NameID.TYPOGRAPHIC_SUBFAMILY_NAME, *platform ) or nametable.getName(NameID.SUBFAMILY_NAME, *platform) + if not all([currentFamilyName, currentStyleName]): + raise ValueError("Name table must have NameIDs 1 and 2") + currentFamilyName = currentFamilyName.toUnicode() currentStyleName = currentStyleName.toUnicode() From fcfcb78cc0d5ff946c5ab1adf88e357cbe78a909 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Thu, 18 Feb 2021 17:35:10 +0000 Subject: [PATCH 24/28] make docstring shorter; fix _isRibbi MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit return True only if a corresponding english record is found and matches R/I/B/BI, else return False. We don't really care if there are any other unrelated english records, we care about this specific one, whether it's RIBBI or not minor --- Lib/fontTools/varLib/instancer/names.py | 74 ++++++++++--------------- 1 file changed, 30 insertions(+), 44 deletions(-) diff --git a/Lib/fontTools/varLib/instancer/names.py b/Lib/fontTools/varLib/instancer/names.py index ed15fe2d0..42f9ed4c7 100644 --- a/Lib/fontTools/varLib/instancer/names.py +++ b/Lib/fontTools/varLib/instancer/names.py @@ -68,23 +68,18 @@ def pruningUnusedNames(varfont): def updateNameTable(varfont, axisLimits): - """Update an instatiated variable font's name table using the - AxisValues from the STAT table. + """Update instatiated variable font's name table using STAT AxisValues. - The updated name table will conform to the R/I/B/BI naming model. - R/I/B/BI is an acronym for (Regular, Italic, Bold, Bold Italic) font - styles. + Raises ValueError if the STAT table is missing or an Axis Value table is + missing for requested axis locations. - This task can be split into two parts: - - Task 1: Collect and sort the relevant AxisValues into a new list which - only includes AxisValues whose coordinates match the new default - axis locations. We also skip any AxisValues which are elided. - - Task 2: Update the name table's style and family names records using the - AxisValues found in step 1. The MS spec provides further info for applying - the R/I/B/BI model to each name record: - https://docs.microsoft.com/en-us/typography/opentype/spec/name#name-ids + First, collect all STAT AxisValues that match the new default axis locations + (excluding "elided" ones); concatenate the strings in design axis order, + while giving priority to "synthetic" values (Format 4), to form the + typographic subfamily name associated with the new default instance. + Finally, update all related records in the name table, making sure that + legacy family/sub-family names conform to the the R/I/B/BI (Regular, Italic, + Bold, Bold Italic) naming model. Example: Updating a partial variable font: | >>> ttFont = TTFont("OpenSans[wdth,wght].ttf") @@ -99,6 +94,10 @@ def updateNameTable(varfont, axisLimits): NameID 6 PostScript name: "OpenSans-Regular" --> "OpenSans-Condensed" NameID 16 Typographic Family name: None --> "Open Sans" NameID 17 Typographic Subfamily name: None --> "Condensed" + + References: + https://docs.microsoft.com/en-us/typography/opentype/spec/stat + https://docs.microsoft.com/en-us/typography/opentype/spec/name#name-ids """ from . import AxisRange, axisValuesFromAxisLimits @@ -123,9 +122,7 @@ def updateNameTable(varfont, axisLimits): axisValueTables = axisValuesFromAxisLimits(stat, defaultAxisCoords) checkAxisValuesExist(stat, axisValueTables, defaultAxisCoords) - # Ignore axis Values which have ELIDABLE_AXIS_VALUE_NAME flag set. - # AxisValues which have this flag enabled won't be visible in - # application font menus. + # ignore "elidable" axis values, should be omitted in application font menus. axisValueTables = [ v for v in axisValueTables if not v.Flags & ELIDABLE_AXIS_VALUE_NAME ] @@ -159,22 +156,20 @@ def checkAxisValuesExist(stat, axisValues, axisCoords): def _sortAxisValues(axisValues): - # Sort and remove duplicates and ensure that format 4 AxisValues - # are dominant. We need format 4 AxisValues to be dominant because the - # MS Spec states, "if a format 1, format 2 or format 3 table has a + # Sort by axis index, remove duplicates and ensure that format 4 AxisValues + # are dominant. + # The MS Spec states: "if a format 1, format 2 or format 3 table has a # (nominal) value used in a format 4 table that also has values for # other axes, the format 4 table, being the more specific match, is used", # https://docs.microsoft.com/en-us/typography/opentype/spec/stat#axis-value-table-format-4 results = [] seenAxes = set() - # Sort format 4 axes so the tables with the most AxisValueRecords - # are first + # Sort format 4 axes so the tables with the most AxisValueRecords are first format4 = sorted( [v for v in axisValues if v.Format == 4], key=lambda v: len(v.AxisValueRecord), reverse=True, ) - nonFormat4 = [v for v in axisValues if v not in format4] for val in format4: axisIndexes = set(r.AxisIndex for r in val.AxisValueRecord) @@ -183,7 +178,9 @@ def _sortAxisValues(axisValues): seenAxes |= axisIndexes results.append((minIndex, val)) - for val in nonFormat4: + for val in axisValues: + if val in format4: + continue axisIndex = val.AxisIndex if axisIndex not in seenAxes: seenAxes.add(axisIndex) @@ -216,8 +213,7 @@ def _updateNameRecords(varfont, axisValues): ) if nonRibbiNameIDs: typoSubFamilyName = " ".join( - getName(n, *platform).toUnicode() - for n in axisValueNameIDs + getName(n, *platform).toUnicode() for n in axisValueNameIDs ) else: typoSubFamilyName = None @@ -244,21 +240,11 @@ def _updateNameRecords(varfont, axisValues): def _isRibbi(nametable, nameID): - engNameRecords = any( - r - for r in nametable.names - if (r.platformID, r.platEncID, r.langID) == (3, 1, 0x409) - ) - if not engNameRecords: - raise ValueError( - f"Cannot determine if there are RIBBI Axis Value Tables " - "since there are no name table Records which have " - "platformID=3, platEncID=1, langID=0x409" - ) + englishRecord = nametable.getName(nameID, 3, 1, 0x409) return ( True - if nametable.getName(nameID, 3, 1, 0x409).toUnicode() - in ("Regular", "Italic", "Bold", "Bold Italic") + if englishRecord is not None + and englishRecord.toUnicode() in ("Regular", "Italic", "Bold", "Bold Italic") else False ) @@ -286,7 +272,7 @@ def _updateNameTableStyleRecords( ) or nametable.getName(NameID.SUBFAMILY_NAME, *platform) if not all([currentFamilyName, currentStyleName]): - raise ValueError("Name table must have NameIDs 1 and 2") + raise ValueError(f"Missing required NameIDs 1 and 2 for platform {platform}") currentFamilyName = currentFamilyName.toUnicode() currentStyleName = currentStyleName.toUnicode() @@ -298,10 +284,10 @@ def _updateNameTableStyleRecords( if typoSubFamilyName: nameIDs[NameID.FAMILY_NAME] = f"{currentFamilyName} {familyNameSuffix}".strip() nameIDs[NameID.TYPOGRAPHIC_FAMILY_NAME] = currentFamilyName - nameIDs[NameID.TYPOGRAPHIC_SUBFAMILY_NAME] = f"{typoSubFamilyName}" - # Remove previous Typographic Family and SubFamily names since they're - # no longer required + nameIDs[NameID.TYPOGRAPHIC_SUBFAMILY_NAME] = typoSubFamilyName else: + # Remove previous Typographic Family and SubFamily names since they're + # no longer required for nameID in ( NameID.TYPOGRAPHIC_FAMILY_NAME, NameID.TYPOGRAPHIC_SUBFAMILY_NAME, From 0c92d33bc0bb07904c1210f321ba4c3e030e7846 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Fri, 19 Feb 2021 11:02:26 +0000 Subject: [PATCH 25/28] fix sub-string check in update uniqueID --- Lib/fontTools/varLib/instancer/names.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/fontTools/varLib/instancer/names.py b/Lib/fontTools/varLib/instancer/names.py index 42f9ed4c7..96898eb0b 100644 --- a/Lib/fontTools/varLib/instancer/names.py +++ b/Lib/fontTools/varLib/instancer/names.py @@ -352,10 +352,11 @@ def _updateUniqueIdNameRecord(varfont, nameIDs, platform): nameRecord = nametable.getName(nameID, *platform) if not nameRecord: continue - if currentRecord.toUnicode() in nameRecord.toUnicode(): + if nameRecord.toUnicode() in currentRecord.toUnicode(): return currentRecord.toUnicode().replace( nameRecord.toUnicode(), nameIDs[nameRecord.nameID] ) + # Create a new string since we couldn't find any substrings. fontVersion = _fontVersion(varfont, platform) achVendID = varfont["OS/2"].achVendID From d1a8e1ee760d59f8a1e637a1a93654ab0dbe5867 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Fri, 19 Feb 2021 11:03:41 +0000 Subject: [PATCH 26/28] fall back to 'Regular' when no subFamilyName assert we have some 'string' instead of silently continuing --- Lib/fontTools/varLib/instancer/names.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Lib/fontTools/varLib/instancer/names.py b/Lib/fontTools/varLib/instancer/names.py index 96898eb0b..4129aca89 100644 --- a/Lib/fontTools/varLib/instancer/names.py +++ b/Lib/fontTools/varLib/instancer/names.py @@ -279,7 +279,7 @@ def _updateNameTableStyleRecords( nameIDs = { NameID.FAMILY_NAME: currentFamilyName, - NameID.SUBFAMILY_NAME: subFamilyName, + NameID.SUBFAMILY_NAME: subFamilyName or "Regular", } if typoSubFamilyName: nameIDs[NameID.FAMILY_NAME] = f"{currentFamilyName} {familyNameSuffix}".strip() @@ -305,13 +305,13 @@ def _updateNameTableStyleRecords( nameIDs[NameID.POSTSCRIPT_NAME] = _updatePSNameRecord( varfont, newFamilyName, newStyleName, platform ) - nameIDs[NameID.UNIQUE_FONT_IDENTIFIER] = _updateUniqueIdNameRecord( - varfont, nameIDs, platform - ) + + uniqueID = _updateUniqueIdNameRecord(varfont, nameIDs, platform) + if uniqueID: + nameIDs[NameID.UNIQUE_FONT_IDENTIFIER] = uniqueID for nameID, string in nameIDs.items(): - if not string: - continue + assert string, nameID nametable.setName(string, nameID, *platform) From a7913ef50eb17903f57528decce15f10bdebf7d0 Mon Sep 17 00:00:00 2001 From: Marc Foley Date: Mon, 22 Feb 2021 11:31:48 +0000 Subject: [PATCH 27/28] Add test for Regular fallback --- Tests/varLib/instancer/names_test.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Tests/varLib/instancer/names_test.py b/Tests/varLib/instancer/names_test.py index 27446e373..ae8e4caaf 100644 --- a/Tests/varLib/instancer/names_test.py +++ b/Tests/varLib/instancer/names_test.py @@ -305,3 +305,14 @@ def test_updateNameTable_elided_axisValues(varfont): # the subFamilyName set so it conforms to the RIBBI model. expected = {(2, 3, 1, 0x409): "Regular", (17, 3, 1, 0x409): "Black"} _test_name_records(varfont, expected, isNonRIBBI=True) + + +def test_updateNameTable_existing_subfamily_name_is_not_regular(varfont): + # Check the subFamily name will be set to Regular when we update a name + # table to a non-RIBBI style and the current subFamily name is a RIBBI + # style which isn't Regular. + varfont["name"].setName("Bold", 2, 3, 1, 0x409) # subFamily Regular --> Bold + + instancer.names.updateNameTable(varfont, {"wght": 100}) + expected = {(2, 3, 1, 0x409): "Regular", (17, 3, 1, 0x409): "Thin"} + _test_name_records(varfont, expected, isNonRIBBI=True) From 52fec53d20810f616615e9bc3666a7f81597790b Mon Sep 17 00:00:00 2001 From: Marc Foley Date: Fri, 26 Feb 2021 09:52:30 +0000 Subject: [PATCH 28/28] Drop nameID 25 if instantiating a static font --- Lib/fontTools/varLib/instancer/names.py | 3 +++ Tests/varLib/instancer/names_test.py | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Lib/fontTools/varLib/instancer/names.py b/Lib/fontTools/varLib/instancer/names.py index 4129aca89..cfe12a94d 100644 --- a/Lib/fontTools/varLib/instancer/names.py +++ b/Lib/fontTools/varLib/instancer/names.py @@ -314,6 +314,9 @@ def _updateNameTableStyleRecords( assert string, nameID nametable.setName(string, nameID, *platform) + if "fvar" not in varfont: + nametable.removeNames(NameID.VARIATIONS_POSTSCRIPT_NAME_PREFIX) + def _updatePSNameRecord(varfont, familyName, styleName, platform): # Implementation based on Adobe Technical Note #5902 : diff --git a/Tests/varLib/instancer/names_test.py b/Tests/varLib/instancer/names_test.py index ae8e4caaf..9774458a9 100644 --- a/Tests/varLib/instancer/names_test.py +++ b/Tests/varLib/instancer/names_test.py @@ -39,11 +39,15 @@ def _test_name_records(varfont, expected, isNonRIBBI, platforms=[0x409]): if k[-1] not in platforms: continue assert font_names[k] == expected[k] + + font_nameids = set(i[0] for i in font_names) if isNonRIBBI: - font_nameids = set(i[0] for i in font_names) assert 16 in font_nameids assert 17 in font_nameids + if "fvar" not in varfont: + assert 25 not in font_nameids + @pytest.mark.parametrize( "limits, expected, isNonRIBBI",