xref: /aosp_15_r20/external/fonttools/Lib/fontTools/varLib/instancer/names.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1*e1fe3e4aSElliott Hughes"""Helpers for instantiating name table records."""
2*e1fe3e4aSElliott Hughes
3*e1fe3e4aSElliott Hughesfrom contextlib import contextmanager
4*e1fe3e4aSElliott Hughesfrom copy import deepcopy
5*e1fe3e4aSElliott Hughesfrom enum import IntEnum
6*e1fe3e4aSElliott Hughesimport re
7*e1fe3e4aSElliott Hughes
8*e1fe3e4aSElliott Hughes
9*e1fe3e4aSElliott Hughesclass NameID(IntEnum):
10*e1fe3e4aSElliott Hughes    FAMILY_NAME = 1
11*e1fe3e4aSElliott Hughes    SUBFAMILY_NAME = 2
12*e1fe3e4aSElliott Hughes    UNIQUE_FONT_IDENTIFIER = 3
13*e1fe3e4aSElliott Hughes    FULL_FONT_NAME = 4
14*e1fe3e4aSElliott Hughes    VERSION_STRING = 5
15*e1fe3e4aSElliott Hughes    POSTSCRIPT_NAME = 6
16*e1fe3e4aSElliott Hughes    TYPOGRAPHIC_FAMILY_NAME = 16
17*e1fe3e4aSElliott Hughes    TYPOGRAPHIC_SUBFAMILY_NAME = 17
18*e1fe3e4aSElliott Hughes    VARIATIONS_POSTSCRIPT_NAME_PREFIX = 25
19*e1fe3e4aSElliott Hughes
20*e1fe3e4aSElliott Hughes
21*e1fe3e4aSElliott HughesELIDABLE_AXIS_VALUE_NAME = 2
22*e1fe3e4aSElliott Hughes
23*e1fe3e4aSElliott Hughes
24*e1fe3e4aSElliott Hughesdef getVariationNameIDs(varfont):
25*e1fe3e4aSElliott Hughes    used = []
26*e1fe3e4aSElliott Hughes    if "fvar" in varfont:
27*e1fe3e4aSElliott Hughes        fvar = varfont["fvar"]
28*e1fe3e4aSElliott Hughes        for axis in fvar.axes:
29*e1fe3e4aSElliott Hughes            used.append(axis.axisNameID)
30*e1fe3e4aSElliott Hughes        for instance in fvar.instances:
31*e1fe3e4aSElliott Hughes            used.append(instance.subfamilyNameID)
32*e1fe3e4aSElliott Hughes            if instance.postscriptNameID != 0xFFFF:
33*e1fe3e4aSElliott Hughes                used.append(instance.postscriptNameID)
34*e1fe3e4aSElliott Hughes    if "STAT" in varfont:
35*e1fe3e4aSElliott Hughes        stat = varfont["STAT"].table
36*e1fe3e4aSElliott Hughes        for axis in stat.DesignAxisRecord.Axis if stat.DesignAxisRecord else ():
37*e1fe3e4aSElliott Hughes            used.append(axis.AxisNameID)
38*e1fe3e4aSElliott Hughes        for value in stat.AxisValueArray.AxisValue if stat.AxisValueArray else ():
39*e1fe3e4aSElliott Hughes            used.append(value.ValueNameID)
40*e1fe3e4aSElliott Hughes        elidedFallbackNameID = getattr(stat, "ElidedFallbackNameID", None)
41*e1fe3e4aSElliott Hughes        if elidedFallbackNameID is not None:
42*e1fe3e4aSElliott Hughes            used.append(elidedFallbackNameID)
43*e1fe3e4aSElliott Hughes    # nameIDs <= 255 are reserved by OT spec so we don't touch them
44*e1fe3e4aSElliott Hughes    return {nameID for nameID in used if nameID > 255}
45*e1fe3e4aSElliott Hughes
46*e1fe3e4aSElliott Hughes
47*e1fe3e4aSElliott Hughes@contextmanager
48*e1fe3e4aSElliott Hughesdef pruningUnusedNames(varfont):
49*e1fe3e4aSElliott Hughes    from . import log
50*e1fe3e4aSElliott Hughes
51*e1fe3e4aSElliott Hughes    origNameIDs = getVariationNameIDs(varfont)
52*e1fe3e4aSElliott Hughes
53*e1fe3e4aSElliott Hughes    yield
54*e1fe3e4aSElliott Hughes
55*e1fe3e4aSElliott Hughes    log.info("Pruning name table")
56*e1fe3e4aSElliott Hughes    exclude = origNameIDs - getVariationNameIDs(varfont)
57*e1fe3e4aSElliott Hughes    varfont["name"].names[:] = [
58*e1fe3e4aSElliott Hughes        record for record in varfont["name"].names if record.nameID not in exclude
59*e1fe3e4aSElliott Hughes    ]
60*e1fe3e4aSElliott Hughes    if "ltag" in varfont:
61*e1fe3e4aSElliott Hughes        # Drop the whole 'ltag' table if all the language-dependent Unicode name
62*e1fe3e4aSElliott Hughes        # records that reference it have been dropped.
63*e1fe3e4aSElliott Hughes        # TODO: Only prune unused ltag tags, renumerating langIDs accordingly.
64*e1fe3e4aSElliott Hughes        # Note ltag can also be used by feat or morx tables, so check those too.
65*e1fe3e4aSElliott Hughes        if not any(
66*e1fe3e4aSElliott Hughes            record
67*e1fe3e4aSElliott Hughes            for record in varfont["name"].names
68*e1fe3e4aSElliott Hughes            if record.platformID == 0 and record.langID != 0xFFFF
69*e1fe3e4aSElliott Hughes        ):
70*e1fe3e4aSElliott Hughes            del varfont["ltag"]
71*e1fe3e4aSElliott Hughes
72*e1fe3e4aSElliott Hughes
73*e1fe3e4aSElliott Hughesdef updateNameTable(varfont, axisLimits):
74*e1fe3e4aSElliott Hughes    """Update instatiated variable font's name table using STAT AxisValues.
75*e1fe3e4aSElliott Hughes
76*e1fe3e4aSElliott Hughes    Raises ValueError if the STAT table is missing or an Axis Value table is
77*e1fe3e4aSElliott Hughes    missing for requested axis locations.
78*e1fe3e4aSElliott Hughes
79*e1fe3e4aSElliott Hughes    First, collect all STAT AxisValues that match the new default axis locations
80*e1fe3e4aSElliott Hughes    (excluding "elided" ones); concatenate the strings in design axis order,
81*e1fe3e4aSElliott Hughes    while giving priority to "synthetic" values (Format 4), to form the
82*e1fe3e4aSElliott Hughes    typographic subfamily name associated with the new default instance.
83*e1fe3e4aSElliott Hughes    Finally, update all related records in the name table, making sure that
84*e1fe3e4aSElliott Hughes    legacy family/sub-family names conform to the the R/I/B/BI (Regular, Italic,
85*e1fe3e4aSElliott Hughes    Bold, Bold Italic) naming model.
86*e1fe3e4aSElliott Hughes
87*e1fe3e4aSElliott Hughes    Example: Updating a partial variable font:
88*e1fe3e4aSElliott Hughes    | >>> ttFont = TTFont("OpenSans[wdth,wght].ttf")
89*e1fe3e4aSElliott Hughes    | >>> updateNameTable(ttFont, {"wght": (400, 900), "wdth": 75})
90*e1fe3e4aSElliott Hughes
91*e1fe3e4aSElliott Hughes    The name table records will be updated in the following manner:
92*e1fe3e4aSElliott Hughes    NameID 1 familyName: "Open Sans" --> "Open Sans Condensed"
93*e1fe3e4aSElliott Hughes    NameID 2 subFamilyName: "Regular" --> "Regular"
94*e1fe3e4aSElliott Hughes    NameID 3 Unique font identifier: "3.000;GOOG;OpenSans-Regular" --> \
95*e1fe3e4aSElliott Hughes        "3.000;GOOG;OpenSans-Condensed"
96*e1fe3e4aSElliott Hughes    NameID 4 Full font name: "Open Sans Regular" --> "Open Sans Condensed"
97*e1fe3e4aSElliott Hughes    NameID 6 PostScript name: "OpenSans-Regular" --> "OpenSans-Condensed"
98*e1fe3e4aSElliott Hughes    NameID 16 Typographic Family name: None --> "Open Sans"
99*e1fe3e4aSElliott Hughes    NameID 17 Typographic Subfamily name: None --> "Condensed"
100*e1fe3e4aSElliott Hughes
101*e1fe3e4aSElliott Hughes    References:
102*e1fe3e4aSElliott Hughes    https://docs.microsoft.com/en-us/typography/opentype/spec/stat
103*e1fe3e4aSElliott Hughes    https://docs.microsoft.com/en-us/typography/opentype/spec/name#name-ids
104*e1fe3e4aSElliott Hughes    """
105*e1fe3e4aSElliott Hughes    from . import AxisLimits, axisValuesFromAxisLimits
106*e1fe3e4aSElliott Hughes
107*e1fe3e4aSElliott Hughes    if "STAT" not in varfont:
108*e1fe3e4aSElliott Hughes        raise ValueError("Cannot update name table since there is no STAT table.")
109*e1fe3e4aSElliott Hughes    stat = varfont["STAT"].table
110*e1fe3e4aSElliott Hughes    if not stat.AxisValueArray:
111*e1fe3e4aSElliott Hughes        raise ValueError("Cannot update name table since there are no STAT Axis Values")
112*e1fe3e4aSElliott Hughes    fvar = varfont["fvar"]
113*e1fe3e4aSElliott Hughes
114*e1fe3e4aSElliott Hughes    # The updated name table will reflect the new 'zero origin' of the font.
115*e1fe3e4aSElliott Hughes    # If we're instantiating a partial font, we will populate the unpinned
116*e1fe3e4aSElliott Hughes    # axes with their default axis values from fvar.
117*e1fe3e4aSElliott Hughes    axisLimits = AxisLimits(axisLimits).limitAxesAndPopulateDefaults(varfont)
118*e1fe3e4aSElliott Hughes    partialDefaults = axisLimits.defaultLocation()
119*e1fe3e4aSElliott Hughes    fvarDefaults = {a.axisTag: a.defaultValue for a in fvar.axes}
120*e1fe3e4aSElliott Hughes    defaultAxisCoords = AxisLimits({**fvarDefaults, **partialDefaults})
121*e1fe3e4aSElliott Hughes    assert all(v.minimum == v.maximum for v in defaultAxisCoords.values())
122*e1fe3e4aSElliott Hughes
123*e1fe3e4aSElliott Hughes    axisValueTables = axisValuesFromAxisLimits(stat, defaultAxisCoords)
124*e1fe3e4aSElliott Hughes    checkAxisValuesExist(stat, axisValueTables, defaultAxisCoords.pinnedLocation())
125*e1fe3e4aSElliott Hughes
126*e1fe3e4aSElliott Hughes    # ignore "elidable" axis values, should be omitted in application font menus.
127*e1fe3e4aSElliott Hughes    axisValueTables = [
128*e1fe3e4aSElliott Hughes        v for v in axisValueTables if not v.Flags & ELIDABLE_AXIS_VALUE_NAME
129*e1fe3e4aSElliott Hughes    ]
130*e1fe3e4aSElliott Hughes    axisValueTables = _sortAxisValues(axisValueTables)
131*e1fe3e4aSElliott Hughes    _updateNameRecords(varfont, axisValueTables)
132*e1fe3e4aSElliott Hughes
133*e1fe3e4aSElliott Hughes
134*e1fe3e4aSElliott Hughesdef checkAxisValuesExist(stat, axisValues, axisCoords):
135*e1fe3e4aSElliott Hughes    seen = set()
136*e1fe3e4aSElliott Hughes    designAxes = stat.DesignAxisRecord.Axis
137*e1fe3e4aSElliott Hughes    hasValues = set()
138*e1fe3e4aSElliott Hughes    for value in stat.AxisValueArray.AxisValue:
139*e1fe3e4aSElliott Hughes        if value.Format in (1, 2, 3):
140*e1fe3e4aSElliott Hughes            hasValues.add(designAxes[value.AxisIndex].AxisTag)
141*e1fe3e4aSElliott Hughes        elif value.Format == 4:
142*e1fe3e4aSElliott Hughes            for rec in value.AxisValueRecord:
143*e1fe3e4aSElliott Hughes                hasValues.add(designAxes[rec.AxisIndex].AxisTag)
144*e1fe3e4aSElliott Hughes
145*e1fe3e4aSElliott Hughes    for axisValueTable in axisValues:
146*e1fe3e4aSElliott Hughes        axisValueFormat = axisValueTable.Format
147*e1fe3e4aSElliott Hughes        if axisValueTable.Format in (1, 2, 3):
148*e1fe3e4aSElliott Hughes            axisTag = designAxes[axisValueTable.AxisIndex].AxisTag
149*e1fe3e4aSElliott Hughes            if axisValueFormat == 2:
150*e1fe3e4aSElliott Hughes                axisValue = axisValueTable.NominalValue
151*e1fe3e4aSElliott Hughes            else:
152*e1fe3e4aSElliott Hughes                axisValue = axisValueTable.Value
153*e1fe3e4aSElliott Hughes            if axisTag in axisCoords and axisValue == axisCoords[axisTag]:
154*e1fe3e4aSElliott Hughes                seen.add(axisTag)
155*e1fe3e4aSElliott Hughes        elif axisValueTable.Format == 4:
156*e1fe3e4aSElliott Hughes            for rec in axisValueTable.AxisValueRecord:
157*e1fe3e4aSElliott Hughes                axisTag = designAxes[rec.AxisIndex].AxisTag
158*e1fe3e4aSElliott Hughes                if axisTag in axisCoords and rec.Value == axisCoords[axisTag]:
159*e1fe3e4aSElliott Hughes                    seen.add(axisTag)
160*e1fe3e4aSElliott Hughes
161*e1fe3e4aSElliott Hughes    missingAxes = (set(axisCoords) - seen) & hasValues
162*e1fe3e4aSElliott Hughes    if missingAxes:
163*e1fe3e4aSElliott Hughes        missing = ", ".join(f"'{i}': {axisCoords[i]}" for i in missingAxes)
164*e1fe3e4aSElliott Hughes        raise ValueError(f"Cannot find Axis Values {{{missing}}}")
165*e1fe3e4aSElliott Hughes
166*e1fe3e4aSElliott Hughes
167*e1fe3e4aSElliott Hughesdef _sortAxisValues(axisValues):
168*e1fe3e4aSElliott Hughes    # Sort by axis index, remove duplicates and ensure that format 4 AxisValues
169*e1fe3e4aSElliott Hughes    # are dominant.
170*e1fe3e4aSElliott Hughes    # The MS Spec states: "if a format 1, format 2 or format 3 table has a
171*e1fe3e4aSElliott Hughes    # (nominal) value used in a format 4 table that also has values for
172*e1fe3e4aSElliott Hughes    # other axes, the format 4 table, being the more specific match, is used",
173*e1fe3e4aSElliott Hughes    # https://docs.microsoft.com/en-us/typography/opentype/spec/stat#axis-value-table-format-4
174*e1fe3e4aSElliott Hughes    results = []
175*e1fe3e4aSElliott Hughes    seenAxes = set()
176*e1fe3e4aSElliott Hughes    # Sort format 4 axes so the tables with the most AxisValueRecords are first
177*e1fe3e4aSElliott Hughes    format4 = sorted(
178*e1fe3e4aSElliott Hughes        [v for v in axisValues if v.Format == 4],
179*e1fe3e4aSElliott Hughes        key=lambda v: len(v.AxisValueRecord),
180*e1fe3e4aSElliott Hughes        reverse=True,
181*e1fe3e4aSElliott Hughes    )
182*e1fe3e4aSElliott Hughes
183*e1fe3e4aSElliott Hughes    for val in format4:
184*e1fe3e4aSElliott Hughes        axisIndexes = set(r.AxisIndex for r in val.AxisValueRecord)
185*e1fe3e4aSElliott Hughes        minIndex = min(axisIndexes)
186*e1fe3e4aSElliott Hughes        if not seenAxes & axisIndexes:
187*e1fe3e4aSElliott Hughes            seenAxes |= axisIndexes
188*e1fe3e4aSElliott Hughes            results.append((minIndex, val))
189*e1fe3e4aSElliott Hughes
190*e1fe3e4aSElliott Hughes    for val in axisValues:
191*e1fe3e4aSElliott Hughes        if val in format4:
192*e1fe3e4aSElliott Hughes            continue
193*e1fe3e4aSElliott Hughes        axisIndex = val.AxisIndex
194*e1fe3e4aSElliott Hughes        if axisIndex not in seenAxes:
195*e1fe3e4aSElliott Hughes            seenAxes.add(axisIndex)
196*e1fe3e4aSElliott Hughes            results.append((axisIndex, val))
197*e1fe3e4aSElliott Hughes
198*e1fe3e4aSElliott Hughes    return [axisValue for _, axisValue in sorted(results)]
199*e1fe3e4aSElliott Hughes
200*e1fe3e4aSElliott Hughes
201*e1fe3e4aSElliott Hughesdef _updateNameRecords(varfont, axisValues):
202*e1fe3e4aSElliott Hughes    # Update nametable based on the axisValues using the R/I/B/BI model.
203*e1fe3e4aSElliott Hughes    nametable = varfont["name"]
204*e1fe3e4aSElliott Hughes    stat = varfont["STAT"].table
205*e1fe3e4aSElliott Hughes
206*e1fe3e4aSElliott Hughes    axisValueNameIDs = [a.ValueNameID for a in axisValues]
207*e1fe3e4aSElliott Hughes    ribbiNameIDs = [n for n in axisValueNameIDs if _isRibbi(nametable, n)]
208*e1fe3e4aSElliott Hughes    nonRibbiNameIDs = [n for n in axisValueNameIDs if n not in ribbiNameIDs]
209*e1fe3e4aSElliott Hughes    elidedNameID = stat.ElidedFallbackNameID
210*e1fe3e4aSElliott Hughes    elidedNameIsRibbi = _isRibbi(nametable, elidedNameID)
211*e1fe3e4aSElliott Hughes
212*e1fe3e4aSElliott Hughes    getName = nametable.getName
213*e1fe3e4aSElliott Hughes    platforms = set((r.platformID, r.platEncID, r.langID) for r in nametable.names)
214*e1fe3e4aSElliott Hughes    for platform in platforms:
215*e1fe3e4aSElliott Hughes        if not all(getName(i, *platform) for i in (1, 2, elidedNameID)):
216*e1fe3e4aSElliott Hughes            # Since no family name and subfamily name records were found,
217*e1fe3e4aSElliott Hughes            # we cannot update this set of name Records.
218*e1fe3e4aSElliott Hughes            continue
219*e1fe3e4aSElliott Hughes
220*e1fe3e4aSElliott Hughes        subFamilyName = " ".join(
221*e1fe3e4aSElliott Hughes            getName(n, *platform).toUnicode() for n in ribbiNameIDs
222*e1fe3e4aSElliott Hughes        )
223*e1fe3e4aSElliott Hughes        if nonRibbiNameIDs:
224*e1fe3e4aSElliott Hughes            typoSubFamilyName = " ".join(
225*e1fe3e4aSElliott Hughes                getName(n, *platform).toUnicode() for n in axisValueNameIDs
226*e1fe3e4aSElliott Hughes            )
227*e1fe3e4aSElliott Hughes        else:
228*e1fe3e4aSElliott Hughes            typoSubFamilyName = None
229*e1fe3e4aSElliott Hughes
230*e1fe3e4aSElliott Hughes        # If neither subFamilyName and typographic SubFamilyName exist,
231*e1fe3e4aSElliott Hughes        # we will use the STAT's elidedFallbackName
232*e1fe3e4aSElliott Hughes        if not typoSubFamilyName and not subFamilyName:
233*e1fe3e4aSElliott Hughes            if elidedNameIsRibbi:
234*e1fe3e4aSElliott Hughes                subFamilyName = getName(elidedNameID, *platform).toUnicode()
235*e1fe3e4aSElliott Hughes            else:
236*e1fe3e4aSElliott Hughes                typoSubFamilyName = getName(elidedNameID, *platform).toUnicode()
237*e1fe3e4aSElliott Hughes
238*e1fe3e4aSElliott Hughes        familyNameSuffix = " ".join(
239*e1fe3e4aSElliott Hughes            getName(n, *platform).toUnicode() for n in nonRibbiNameIDs
240*e1fe3e4aSElliott Hughes        )
241*e1fe3e4aSElliott Hughes
242*e1fe3e4aSElliott Hughes        _updateNameTableStyleRecords(
243*e1fe3e4aSElliott Hughes            varfont,
244*e1fe3e4aSElliott Hughes            familyNameSuffix,
245*e1fe3e4aSElliott Hughes            subFamilyName,
246*e1fe3e4aSElliott Hughes            typoSubFamilyName,
247*e1fe3e4aSElliott Hughes            *platform,
248*e1fe3e4aSElliott Hughes        )
249*e1fe3e4aSElliott Hughes
250*e1fe3e4aSElliott Hughes
251*e1fe3e4aSElliott Hughesdef _isRibbi(nametable, nameID):
252*e1fe3e4aSElliott Hughes    englishRecord = nametable.getName(nameID, 3, 1, 0x409)
253*e1fe3e4aSElliott Hughes    return (
254*e1fe3e4aSElliott Hughes        True
255*e1fe3e4aSElliott Hughes        if englishRecord is not None
256*e1fe3e4aSElliott Hughes        and englishRecord.toUnicode() in ("Regular", "Italic", "Bold", "Bold Italic")
257*e1fe3e4aSElliott Hughes        else False
258*e1fe3e4aSElliott Hughes    )
259*e1fe3e4aSElliott Hughes
260*e1fe3e4aSElliott Hughes
261*e1fe3e4aSElliott Hughesdef _updateNameTableStyleRecords(
262*e1fe3e4aSElliott Hughes    varfont,
263*e1fe3e4aSElliott Hughes    familyNameSuffix,
264*e1fe3e4aSElliott Hughes    subFamilyName,
265*e1fe3e4aSElliott Hughes    typoSubFamilyName,
266*e1fe3e4aSElliott Hughes    platformID=3,
267*e1fe3e4aSElliott Hughes    platEncID=1,
268*e1fe3e4aSElliott Hughes    langID=0x409,
269*e1fe3e4aSElliott Hughes):
270*e1fe3e4aSElliott Hughes    # TODO (Marc F) It may be nice to make this part a standalone
271*e1fe3e4aSElliott Hughes    # font renamer in the future.
272*e1fe3e4aSElliott Hughes    nametable = varfont["name"]
273*e1fe3e4aSElliott Hughes    platform = (platformID, platEncID, langID)
274*e1fe3e4aSElliott Hughes
275*e1fe3e4aSElliott Hughes    currentFamilyName = nametable.getName(
276*e1fe3e4aSElliott Hughes        NameID.TYPOGRAPHIC_FAMILY_NAME, *platform
277*e1fe3e4aSElliott Hughes    ) or nametable.getName(NameID.FAMILY_NAME, *platform)
278*e1fe3e4aSElliott Hughes
279*e1fe3e4aSElliott Hughes    currentStyleName = nametable.getName(
280*e1fe3e4aSElliott Hughes        NameID.TYPOGRAPHIC_SUBFAMILY_NAME, *platform
281*e1fe3e4aSElliott Hughes    ) or nametable.getName(NameID.SUBFAMILY_NAME, *platform)
282*e1fe3e4aSElliott Hughes
283*e1fe3e4aSElliott Hughes    if not all([currentFamilyName, currentStyleName]):
284*e1fe3e4aSElliott Hughes        raise ValueError(f"Missing required NameIDs 1 and 2 for platform {platform}")
285*e1fe3e4aSElliott Hughes
286*e1fe3e4aSElliott Hughes    currentFamilyName = currentFamilyName.toUnicode()
287*e1fe3e4aSElliott Hughes    currentStyleName = currentStyleName.toUnicode()
288*e1fe3e4aSElliott Hughes
289*e1fe3e4aSElliott Hughes    nameIDs = {
290*e1fe3e4aSElliott Hughes        NameID.FAMILY_NAME: currentFamilyName,
291*e1fe3e4aSElliott Hughes        NameID.SUBFAMILY_NAME: subFamilyName or "Regular",
292*e1fe3e4aSElliott Hughes    }
293*e1fe3e4aSElliott Hughes    if typoSubFamilyName:
294*e1fe3e4aSElliott Hughes        nameIDs[NameID.FAMILY_NAME] = f"{currentFamilyName} {familyNameSuffix}".strip()
295*e1fe3e4aSElliott Hughes        nameIDs[NameID.TYPOGRAPHIC_FAMILY_NAME] = currentFamilyName
296*e1fe3e4aSElliott Hughes        nameIDs[NameID.TYPOGRAPHIC_SUBFAMILY_NAME] = typoSubFamilyName
297*e1fe3e4aSElliott Hughes    else:
298*e1fe3e4aSElliott Hughes        # Remove previous Typographic Family and SubFamily names since they're
299*e1fe3e4aSElliott Hughes        # no longer required
300*e1fe3e4aSElliott Hughes        for nameID in (
301*e1fe3e4aSElliott Hughes            NameID.TYPOGRAPHIC_FAMILY_NAME,
302*e1fe3e4aSElliott Hughes            NameID.TYPOGRAPHIC_SUBFAMILY_NAME,
303*e1fe3e4aSElliott Hughes        ):
304*e1fe3e4aSElliott Hughes            nametable.removeNames(nameID=nameID)
305*e1fe3e4aSElliott Hughes
306*e1fe3e4aSElliott Hughes    newFamilyName = (
307*e1fe3e4aSElliott Hughes        nameIDs.get(NameID.TYPOGRAPHIC_FAMILY_NAME) or nameIDs[NameID.FAMILY_NAME]
308*e1fe3e4aSElliott Hughes    )
309*e1fe3e4aSElliott Hughes    newStyleName = (
310*e1fe3e4aSElliott Hughes        nameIDs.get(NameID.TYPOGRAPHIC_SUBFAMILY_NAME) or nameIDs[NameID.SUBFAMILY_NAME]
311*e1fe3e4aSElliott Hughes    )
312*e1fe3e4aSElliott Hughes
313*e1fe3e4aSElliott Hughes    nameIDs[NameID.FULL_FONT_NAME] = f"{newFamilyName} {newStyleName}"
314*e1fe3e4aSElliott Hughes    nameIDs[NameID.POSTSCRIPT_NAME] = _updatePSNameRecord(
315*e1fe3e4aSElliott Hughes        varfont, newFamilyName, newStyleName, platform
316*e1fe3e4aSElliott Hughes    )
317*e1fe3e4aSElliott Hughes
318*e1fe3e4aSElliott Hughes    uniqueID = _updateUniqueIdNameRecord(varfont, nameIDs, platform)
319*e1fe3e4aSElliott Hughes    if uniqueID:
320*e1fe3e4aSElliott Hughes        nameIDs[NameID.UNIQUE_FONT_IDENTIFIER] = uniqueID
321*e1fe3e4aSElliott Hughes
322*e1fe3e4aSElliott Hughes    for nameID, string in nameIDs.items():
323*e1fe3e4aSElliott Hughes        assert string, nameID
324*e1fe3e4aSElliott Hughes        nametable.setName(string, nameID, *platform)
325*e1fe3e4aSElliott Hughes
326*e1fe3e4aSElliott Hughes    if "fvar" not in varfont:
327*e1fe3e4aSElliott Hughes        nametable.removeNames(NameID.VARIATIONS_POSTSCRIPT_NAME_PREFIX)
328*e1fe3e4aSElliott Hughes
329*e1fe3e4aSElliott Hughes
330*e1fe3e4aSElliott Hughesdef _updatePSNameRecord(varfont, familyName, styleName, platform):
331*e1fe3e4aSElliott Hughes    # Implementation based on Adobe Technical Note #5902 :
332*e1fe3e4aSElliott Hughes    # https://wwwimages2.adobe.com/content/dam/acom/en/devnet/font/pdfs/5902.AdobePSNameGeneration.pdf
333*e1fe3e4aSElliott Hughes    nametable = varfont["name"]
334*e1fe3e4aSElliott Hughes
335*e1fe3e4aSElliott Hughes    family_prefix = nametable.getName(
336*e1fe3e4aSElliott Hughes        NameID.VARIATIONS_POSTSCRIPT_NAME_PREFIX, *platform
337*e1fe3e4aSElliott Hughes    )
338*e1fe3e4aSElliott Hughes    if family_prefix:
339*e1fe3e4aSElliott Hughes        family_prefix = family_prefix.toUnicode()
340*e1fe3e4aSElliott Hughes    else:
341*e1fe3e4aSElliott Hughes        family_prefix = familyName
342*e1fe3e4aSElliott Hughes
343*e1fe3e4aSElliott Hughes    psName = f"{family_prefix}-{styleName}"
344*e1fe3e4aSElliott Hughes    # Remove any characters other than uppercase Latin letters, lowercase
345*e1fe3e4aSElliott Hughes    # Latin letters, digits and hyphens.
346*e1fe3e4aSElliott Hughes    psName = re.sub(r"[^A-Za-z0-9-]", r"", psName)
347*e1fe3e4aSElliott Hughes
348*e1fe3e4aSElliott Hughes    if len(psName) > 127:
349*e1fe3e4aSElliott Hughes        # Abbreviating the stylename so it fits within 127 characters whilst
350*e1fe3e4aSElliott Hughes        # conforming to every vendor's specification is too complex. Instead
351*e1fe3e4aSElliott Hughes        # we simply truncate the psname and add the required "..."
352*e1fe3e4aSElliott Hughes        return f"{psName[:124]}..."
353*e1fe3e4aSElliott Hughes    return psName
354*e1fe3e4aSElliott Hughes
355*e1fe3e4aSElliott Hughes
356*e1fe3e4aSElliott Hughesdef _updateUniqueIdNameRecord(varfont, nameIDs, platform):
357*e1fe3e4aSElliott Hughes    nametable = varfont["name"]
358*e1fe3e4aSElliott Hughes    currentRecord = nametable.getName(NameID.UNIQUE_FONT_IDENTIFIER, *platform)
359*e1fe3e4aSElliott Hughes    if not currentRecord:
360*e1fe3e4aSElliott Hughes        return None
361*e1fe3e4aSElliott Hughes
362*e1fe3e4aSElliott Hughes    # Check if full name and postscript name are a substring of currentRecord
363*e1fe3e4aSElliott Hughes    for nameID in (NameID.FULL_FONT_NAME, NameID.POSTSCRIPT_NAME):
364*e1fe3e4aSElliott Hughes        nameRecord = nametable.getName(nameID, *platform)
365*e1fe3e4aSElliott Hughes        if not nameRecord:
366*e1fe3e4aSElliott Hughes            continue
367*e1fe3e4aSElliott Hughes        if nameRecord.toUnicode() in currentRecord.toUnicode():
368*e1fe3e4aSElliott Hughes            return currentRecord.toUnicode().replace(
369*e1fe3e4aSElliott Hughes                nameRecord.toUnicode(), nameIDs[nameRecord.nameID]
370*e1fe3e4aSElliott Hughes            )
371*e1fe3e4aSElliott Hughes
372*e1fe3e4aSElliott Hughes    # Create a new string since we couldn't find any substrings.
373*e1fe3e4aSElliott Hughes    fontVersion = _fontVersion(varfont, platform)
374*e1fe3e4aSElliott Hughes    achVendID = varfont["OS/2"].achVendID
375*e1fe3e4aSElliott Hughes    # Remove non-ASCII characers and trailing spaces
376*e1fe3e4aSElliott Hughes    vendor = re.sub(r"[^\x00-\x7F]", "", achVendID).strip()
377*e1fe3e4aSElliott Hughes    psName = nameIDs[NameID.POSTSCRIPT_NAME]
378*e1fe3e4aSElliott Hughes    return f"{fontVersion};{vendor};{psName}"
379*e1fe3e4aSElliott Hughes
380*e1fe3e4aSElliott Hughes
381*e1fe3e4aSElliott Hughesdef _fontVersion(font, platform=(3, 1, 0x409)):
382*e1fe3e4aSElliott Hughes    nameRecord = font["name"].getName(NameID.VERSION_STRING, *platform)
383*e1fe3e4aSElliott Hughes    if nameRecord is None:
384*e1fe3e4aSElliott Hughes        return f'{font["head"].fontRevision:.3f}'
385*e1fe3e4aSElliott Hughes    # "Version 1.101; ttfautohint (v1.8.1.43-b0c9)" --> "1.101"
386*e1fe3e4aSElliott Hughes    # Also works fine with inputs "Version 1.101" or "1.101" etc
387*e1fe3e4aSElliott Hughes    versionNumber = nameRecord.toUnicode().split(";")[0]
388*e1fe3e4aSElliott Hughes    return versionNumber.lstrip("Version ").strip()
389