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