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
This commit is contained in:
parent
0280eb36cc
commit
2be13d50ac
@ -84,6 +84,7 @@ from fontTools import subset # noqa: F401
|
|||||||
from fontTools.varLib import builder
|
from fontTools.varLib import builder
|
||||||
from fontTools.varLib.mvar import MVAR_ENTRIES
|
from fontTools.varLib.mvar import MVAR_ENTRIES
|
||||||
from fontTools.varLib.merger import MutatorMerger
|
from fontTools.varLib.merger import MutatorMerger
|
||||||
|
from fontTools.varLib.instancer import names
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
import collections
|
import collections
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
@ -128,21 +129,6 @@ class OverlapMode(IntEnum):
|
|||||||
REMOVE = 2
|
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(
|
def instantiateTupleVariationStore(
|
||||||
variations, axisLimits, origCoords=None, endPts=None
|
variations, axisLimits, origCoords=None, endPts=None
|
||||||
):
|
):
|
||||||
@ -1073,423 +1059,6 @@ def axisValuesFromAxisLimits(stat, axisLimits):
|
|||||||
return newAxisValueTables
|
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):
|
def setMacOverlapFlags(glyfTable):
|
||||||
flagOverlapCompound = _g_l_y_f.OVERLAP_COMPOUND
|
flagOverlapCompound = _g_l_y_f.OVERLAP_COMPOUND
|
||||||
flagOverlapSimple = _g_l_y_f.flagOverlapSimple
|
flagOverlapSimple = _g_l_y_f.flagOverlapSimple
|
||||||
@ -1633,7 +1202,7 @@ def instantiateVariableFont(
|
|||||||
|
|
||||||
if updateFontNames:
|
if updateFontNames:
|
||||||
log.info("Updating name table")
|
log.info("Updating name table")
|
||||||
updateNameTable(varfont, axisLimits)
|
names.updateNameTable(varfont, axisLimits)
|
||||||
|
|
||||||
if "gvar" in varfont:
|
if "gvar" in varfont:
|
||||||
instantiateGvar(varfont, normalizedLimits, optimize=optimize)
|
instantiateGvar(varfont, normalizedLimits, optimize=optimize)
|
||||||
@ -1657,7 +1226,7 @@ def instantiateVariableFont(
|
|||||||
if "avar" in varfont:
|
if "avar" in varfont:
|
||||||
instantiateAvar(varfont, axisLimits)
|
instantiateAvar(varfont, axisLimits)
|
||||||
|
|
||||||
with pruningUnusedNames(varfont):
|
with names.pruningUnusedNames(varfont):
|
||||||
if "STAT" in varfont:
|
if "STAT" in varfont:
|
||||||
instantiateSTAT(varfont, axisLimits)
|
instantiateSTAT(varfont, axisLimits)
|
||||||
|
|
||||||
@ -1851,9 +1420,3 @@ def main(args=None):
|
|||||||
outfile,
|
outfile,
|
||||||
)
|
)
|
||||||
varfont.save(outfile)
|
varfont.save(outfile)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
import sys
|
|
||||||
|
|
||||||
sys.exit(main())
|
|
5
Lib/fontTools/varLib/instancer/__main__.py
Normal file
5
Lib/fontTools/varLib/instancer/__main__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import sys
|
||||||
|
from fontTools.varLib.instancer import main
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
441
Lib/fontTools/varLib/instancer/names.py
Normal file
441
Lib/fontTools/varLib/instancer/names.py
Normal file
@ -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()
|
13
Tests/varLib/instancer/conftest.py
Normal file
13
Tests/varLib/instancer/conftest.py
Normal file
@ -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
|
@ -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 import otTables
|
||||||
from fontTools.ttLib.tables.TupleVariation import TupleVariation
|
from fontTools.ttLib.tables.TupleVariation import TupleVariation
|
||||||
from fontTools import varLib
|
from fontTools import varLib
|
||||||
from fontTools.otlLib.builder import buildStatTable
|
|
||||||
from fontTools.varLib import instancer
|
from fontTools.varLib import instancer
|
||||||
from fontTools.varLib.mvar import MVAR_ENTRIES
|
from fontTools.varLib.mvar import MVAR_ENTRIES
|
||||||
from fontTools.varLib import builder
|
from fontTools.varLib import builder
|
||||||
@ -21,16 +20,11 @@ import re
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
# see Tests/varLib/instancer/conftest.py for "varfont" fixture definition
|
||||||
|
|
||||||
TESTDATA = os.path.join(os.path.dirname(__file__), "data")
|
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"])
|
@pytest.fixture(params=[True, False], ids=["optimize", "no-optimize"])
|
||||||
def optimize(request):
|
def optimize(request):
|
||||||
return request.param
|
return request.param
|
||||||
@ -1342,30 +1336,6 @@ class InstantiateSTATTest(object):
|
|||||||
assert axisValue in varfont2["STAT"].table.AxisValueArray.AxisValue
|
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():
|
def test_setMacOverlapFlags():
|
||||||
flagOverlapCompound = _g_l_y_f.OVERLAP_COMPOUND
|
flagOverlapCompound = _g_l_y_f.OVERLAP_COMPOUND
|
||||||
flagOverlapSimple = _g_l_y_f.flagOverlapSimple
|
flagOverlapSimple = _g_l_y_f.flagOverlapSimple
|
||||||
@ -1917,284 +1887,6 @@ def test_normalizeAxisLimits_missing_from_fvar(varfont):
|
|||||||
instancer.normalizeAxisLimits(varfont, {"ZZZZ": 1000})
|
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):
|
def test_sanityCheckVariableTables(varfont):
|
||||||
font = ttLib.TTFont()
|
font = ttLib.TTFont()
|
||||||
with pytest.raises(ValueError, match="Missing required table fvar"):
|
with pytest.raises(ValueError, match="Missing required table fvar"):
|
307
Tests/varLib/instancer/names_test.py
Normal file
307
Tests/varLib/instancer/names_test.py
Normal file
@ -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)
|
Loading…
x
Reference in New Issue
Block a user