instancer: implement Cosimo feedback

This commit is contained in:
Marc Foley 2020-10-19 16:32:30 +01:00
parent bef1d08c0b
commit 29e4ff987c
2 changed files with 151 additions and 62 deletions

View File

@ -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):

View File

@ -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"):