1*e1fe3e4aSElliott Hughes""" 2*e1fe3e4aSElliott HughesInstantiate a variation font. Run, eg: 3*e1fe3e4aSElliott Hughes 4*e1fe3e4aSElliott Hughes$ fonttools varLib.mutator ./NotoSansArabic-VF.ttf wght=140 wdth=85 5*e1fe3e4aSElliott Hughes""" 6*e1fe3e4aSElliott Hughes 7*e1fe3e4aSElliott Hughesfrom fontTools.misc.fixedTools import floatToFixedToFloat, floatToFixed 8*e1fe3e4aSElliott Hughesfrom fontTools.misc.roundTools import otRound 9*e1fe3e4aSElliott Hughesfrom fontTools.pens.boundsPen import BoundsPen 10*e1fe3e4aSElliott Hughesfrom fontTools.ttLib import TTFont, newTable 11*e1fe3e4aSElliott Hughesfrom fontTools.ttLib.tables import ttProgram 12*e1fe3e4aSElliott Hughesfrom fontTools.ttLib.tables._g_l_y_f import ( 13*e1fe3e4aSElliott Hughes GlyphCoordinates, 14*e1fe3e4aSElliott Hughes flagOverlapSimple, 15*e1fe3e4aSElliott Hughes OVERLAP_COMPOUND, 16*e1fe3e4aSElliott Hughes) 17*e1fe3e4aSElliott Hughesfrom fontTools.varLib.models import ( 18*e1fe3e4aSElliott Hughes supportScalar, 19*e1fe3e4aSElliott Hughes normalizeLocation, 20*e1fe3e4aSElliott Hughes piecewiseLinearMap, 21*e1fe3e4aSElliott Hughes) 22*e1fe3e4aSElliott Hughesfrom fontTools.varLib.merger import MutatorMerger 23*e1fe3e4aSElliott Hughesfrom fontTools.varLib.varStore import VarStoreInstancer 24*e1fe3e4aSElliott Hughesfrom fontTools.varLib.mvar import MVAR_ENTRIES 25*e1fe3e4aSElliott Hughesfrom fontTools.varLib.iup import iup_delta 26*e1fe3e4aSElliott Hughesimport fontTools.subset.cff 27*e1fe3e4aSElliott Hughesimport os.path 28*e1fe3e4aSElliott Hughesimport logging 29*e1fe3e4aSElliott Hughesfrom io import BytesIO 30*e1fe3e4aSElliott Hughes 31*e1fe3e4aSElliott Hughes 32*e1fe3e4aSElliott Hugheslog = logging.getLogger("fontTools.varlib.mutator") 33*e1fe3e4aSElliott Hughes 34*e1fe3e4aSElliott Hughes# map 'wdth' axis (1..200) to OS/2.usWidthClass (1..9), rounding to closest 35*e1fe3e4aSElliott HughesOS2_WIDTH_CLASS_VALUES = {} 36*e1fe3e4aSElliott Hughespercents = [50.0, 62.5, 75.0, 87.5, 100.0, 112.5, 125.0, 150.0, 200.0] 37*e1fe3e4aSElliott Hughesfor i, (prev, curr) in enumerate(zip(percents[:-1], percents[1:]), start=1): 38*e1fe3e4aSElliott Hughes half = (prev + curr) / 2 39*e1fe3e4aSElliott Hughes OS2_WIDTH_CLASS_VALUES[half] = i 40*e1fe3e4aSElliott Hughes 41*e1fe3e4aSElliott Hughes 42*e1fe3e4aSElliott Hughesdef interpolate_cff2_PrivateDict(topDict, interpolateFromDeltas): 43*e1fe3e4aSElliott Hughes pd_blend_lists = ( 44*e1fe3e4aSElliott Hughes "BlueValues", 45*e1fe3e4aSElliott Hughes "OtherBlues", 46*e1fe3e4aSElliott Hughes "FamilyBlues", 47*e1fe3e4aSElliott Hughes "FamilyOtherBlues", 48*e1fe3e4aSElliott Hughes "StemSnapH", 49*e1fe3e4aSElliott Hughes "StemSnapV", 50*e1fe3e4aSElliott Hughes ) 51*e1fe3e4aSElliott Hughes pd_blend_values = ("BlueScale", "BlueShift", "BlueFuzz", "StdHW", "StdVW") 52*e1fe3e4aSElliott Hughes for fontDict in topDict.FDArray: 53*e1fe3e4aSElliott Hughes pd = fontDict.Private 54*e1fe3e4aSElliott Hughes vsindex = pd.vsindex if (hasattr(pd, "vsindex")) else 0 55*e1fe3e4aSElliott Hughes for key, value in pd.rawDict.items(): 56*e1fe3e4aSElliott Hughes if (key in pd_blend_values) and isinstance(value, list): 57*e1fe3e4aSElliott Hughes delta = interpolateFromDeltas(vsindex, value[1:]) 58*e1fe3e4aSElliott Hughes pd.rawDict[key] = otRound(value[0] + delta) 59*e1fe3e4aSElliott Hughes elif (key in pd_blend_lists) and isinstance(value[0], list): 60*e1fe3e4aSElliott Hughes """If any argument in a BlueValues list is a blend list, 61*e1fe3e4aSElliott Hughes then they all are. The first value of each list is an 62*e1fe3e4aSElliott Hughes absolute value. The delta tuples are calculated from 63*e1fe3e4aSElliott Hughes relative master values, hence we need to append all the 64*e1fe3e4aSElliott Hughes deltas to date to each successive absolute value.""" 65*e1fe3e4aSElliott Hughes delta = 0 66*e1fe3e4aSElliott Hughes for i, val_list in enumerate(value): 67*e1fe3e4aSElliott Hughes delta += otRound(interpolateFromDeltas(vsindex, val_list[1:])) 68*e1fe3e4aSElliott Hughes value[i] = val_list[0] + delta 69*e1fe3e4aSElliott Hughes 70*e1fe3e4aSElliott Hughes 71*e1fe3e4aSElliott Hughesdef interpolate_cff2_charstrings(topDict, interpolateFromDeltas, glyphOrder): 72*e1fe3e4aSElliott Hughes charstrings = topDict.CharStrings 73*e1fe3e4aSElliott Hughes for gname in glyphOrder: 74*e1fe3e4aSElliott Hughes # Interpolate charstring 75*e1fe3e4aSElliott Hughes # e.g replace blend op args with regular args, 76*e1fe3e4aSElliott Hughes # and use and discard vsindex op. 77*e1fe3e4aSElliott Hughes charstring = charstrings[gname] 78*e1fe3e4aSElliott Hughes new_program = [] 79*e1fe3e4aSElliott Hughes vsindex = 0 80*e1fe3e4aSElliott Hughes last_i = 0 81*e1fe3e4aSElliott Hughes for i, token in enumerate(charstring.program): 82*e1fe3e4aSElliott Hughes if token == "vsindex": 83*e1fe3e4aSElliott Hughes vsindex = charstring.program[i - 1] 84*e1fe3e4aSElliott Hughes if last_i != 0: 85*e1fe3e4aSElliott Hughes new_program.extend(charstring.program[last_i : i - 1]) 86*e1fe3e4aSElliott Hughes last_i = i + 1 87*e1fe3e4aSElliott Hughes elif token == "blend": 88*e1fe3e4aSElliott Hughes num_regions = charstring.getNumRegions(vsindex) 89*e1fe3e4aSElliott Hughes numMasters = 1 + num_regions 90*e1fe3e4aSElliott Hughes num_args = charstring.program[i - 1] 91*e1fe3e4aSElliott Hughes # The program list starting at program[i] is now: 92*e1fe3e4aSElliott Hughes # ..args for following operations 93*e1fe3e4aSElliott Hughes # num_args values from the default font 94*e1fe3e4aSElliott Hughes # num_args tuples, each with numMasters-1 delta values 95*e1fe3e4aSElliott Hughes # num_blend_args 96*e1fe3e4aSElliott Hughes # 'blend' 97*e1fe3e4aSElliott Hughes argi = i - (num_args * numMasters + 1) 98*e1fe3e4aSElliott Hughes end_args = tuplei = argi + num_args 99*e1fe3e4aSElliott Hughes while argi < end_args: 100*e1fe3e4aSElliott Hughes next_ti = tuplei + num_regions 101*e1fe3e4aSElliott Hughes deltas = charstring.program[tuplei:next_ti] 102*e1fe3e4aSElliott Hughes delta = interpolateFromDeltas(vsindex, deltas) 103*e1fe3e4aSElliott Hughes charstring.program[argi] += otRound(delta) 104*e1fe3e4aSElliott Hughes tuplei = next_ti 105*e1fe3e4aSElliott Hughes argi += 1 106*e1fe3e4aSElliott Hughes new_program.extend(charstring.program[last_i:end_args]) 107*e1fe3e4aSElliott Hughes last_i = i + 1 108*e1fe3e4aSElliott Hughes if last_i != 0: 109*e1fe3e4aSElliott Hughes new_program.extend(charstring.program[last_i:]) 110*e1fe3e4aSElliott Hughes charstring.program = new_program 111*e1fe3e4aSElliott Hughes 112*e1fe3e4aSElliott Hughes 113*e1fe3e4aSElliott Hughesdef interpolate_cff2_metrics(varfont, topDict, glyphOrder, loc): 114*e1fe3e4aSElliott Hughes """Unlike TrueType glyphs, neither advance width nor bounding box 115*e1fe3e4aSElliott Hughes info is stored in a CFF2 charstring. The width data exists only in 116*e1fe3e4aSElliott Hughes the hmtx and HVAR tables. Since LSB data cannot be interpolated 117*e1fe3e4aSElliott Hughes reliably from the master LSB values in the hmtx table, we traverse 118*e1fe3e4aSElliott Hughes the charstring to determine the actual bound box.""" 119*e1fe3e4aSElliott Hughes 120*e1fe3e4aSElliott Hughes charstrings = topDict.CharStrings 121*e1fe3e4aSElliott Hughes boundsPen = BoundsPen(glyphOrder) 122*e1fe3e4aSElliott Hughes hmtx = varfont["hmtx"] 123*e1fe3e4aSElliott Hughes hvar_table = None 124*e1fe3e4aSElliott Hughes if "HVAR" in varfont: 125*e1fe3e4aSElliott Hughes hvar_table = varfont["HVAR"].table 126*e1fe3e4aSElliott Hughes fvar = varfont["fvar"] 127*e1fe3e4aSElliott Hughes varStoreInstancer = VarStoreInstancer(hvar_table.VarStore, fvar.axes, loc) 128*e1fe3e4aSElliott Hughes 129*e1fe3e4aSElliott Hughes for gid, gname in enumerate(glyphOrder): 130*e1fe3e4aSElliott Hughes entry = list(hmtx[gname]) 131*e1fe3e4aSElliott Hughes # get width delta. 132*e1fe3e4aSElliott Hughes if hvar_table: 133*e1fe3e4aSElliott Hughes if hvar_table.AdvWidthMap: 134*e1fe3e4aSElliott Hughes width_idx = hvar_table.AdvWidthMap.mapping[gname] 135*e1fe3e4aSElliott Hughes else: 136*e1fe3e4aSElliott Hughes width_idx = gid 137*e1fe3e4aSElliott Hughes width_delta = otRound(varStoreInstancer[width_idx]) 138*e1fe3e4aSElliott Hughes else: 139*e1fe3e4aSElliott Hughes width_delta = 0 140*e1fe3e4aSElliott Hughes 141*e1fe3e4aSElliott Hughes # get LSB. 142*e1fe3e4aSElliott Hughes boundsPen.init() 143*e1fe3e4aSElliott Hughes charstring = charstrings[gname] 144*e1fe3e4aSElliott Hughes charstring.draw(boundsPen) 145*e1fe3e4aSElliott Hughes if boundsPen.bounds is None: 146*e1fe3e4aSElliott Hughes # Happens with non-marking glyphs 147*e1fe3e4aSElliott Hughes lsb_delta = 0 148*e1fe3e4aSElliott Hughes else: 149*e1fe3e4aSElliott Hughes lsb = otRound(boundsPen.bounds[0]) 150*e1fe3e4aSElliott Hughes lsb_delta = entry[1] - lsb 151*e1fe3e4aSElliott Hughes 152*e1fe3e4aSElliott Hughes if lsb_delta or width_delta: 153*e1fe3e4aSElliott Hughes if width_delta: 154*e1fe3e4aSElliott Hughes entry[0] = max(0, entry[0] + width_delta) 155*e1fe3e4aSElliott Hughes if lsb_delta: 156*e1fe3e4aSElliott Hughes entry[1] = lsb 157*e1fe3e4aSElliott Hughes hmtx[gname] = tuple(entry) 158*e1fe3e4aSElliott Hughes 159*e1fe3e4aSElliott Hughes 160*e1fe3e4aSElliott Hughesdef instantiateVariableFont(varfont, location, inplace=False, overlap=True): 161*e1fe3e4aSElliott Hughes """Generate a static instance from a variable TTFont and a dictionary 162*e1fe3e4aSElliott Hughes defining the desired location along the variable font's axes. 163*e1fe3e4aSElliott Hughes The location values must be specified as user-space coordinates, e.g.: 164*e1fe3e4aSElliott Hughes 165*e1fe3e4aSElliott Hughes {'wght': 400, 'wdth': 100} 166*e1fe3e4aSElliott Hughes 167*e1fe3e4aSElliott Hughes By default, a new TTFont object is returned. If ``inplace`` is True, the 168*e1fe3e4aSElliott Hughes input varfont is modified and reduced to a static font. 169*e1fe3e4aSElliott Hughes 170*e1fe3e4aSElliott Hughes When the overlap parameter is defined as True, 171*e1fe3e4aSElliott Hughes OVERLAP_SIMPLE and OVERLAP_COMPOUND bits are set to 1. See 172*e1fe3e4aSElliott Hughes https://docs.microsoft.com/en-us/typography/opentype/spec/glyf 173*e1fe3e4aSElliott Hughes """ 174*e1fe3e4aSElliott Hughes if not inplace: 175*e1fe3e4aSElliott Hughes # make a copy to leave input varfont unmodified 176*e1fe3e4aSElliott Hughes stream = BytesIO() 177*e1fe3e4aSElliott Hughes varfont.save(stream) 178*e1fe3e4aSElliott Hughes stream.seek(0) 179*e1fe3e4aSElliott Hughes varfont = TTFont(stream) 180*e1fe3e4aSElliott Hughes 181*e1fe3e4aSElliott Hughes fvar = varfont["fvar"] 182*e1fe3e4aSElliott Hughes axes = {a.axisTag: (a.minValue, a.defaultValue, a.maxValue) for a in fvar.axes} 183*e1fe3e4aSElliott Hughes loc = normalizeLocation(location, axes) 184*e1fe3e4aSElliott Hughes if "avar" in varfont: 185*e1fe3e4aSElliott Hughes maps = varfont["avar"].segments 186*e1fe3e4aSElliott Hughes loc = {k: piecewiseLinearMap(v, maps[k]) for k, v in loc.items()} 187*e1fe3e4aSElliott Hughes # Quantize to F2Dot14, to avoid surprise interpolations. 188*e1fe3e4aSElliott Hughes loc = {k: floatToFixedToFloat(v, 14) for k, v in loc.items()} 189*e1fe3e4aSElliott Hughes # Location is normalized now 190*e1fe3e4aSElliott Hughes log.info("Normalized location: %s", loc) 191*e1fe3e4aSElliott Hughes 192*e1fe3e4aSElliott Hughes if "gvar" in varfont: 193*e1fe3e4aSElliott Hughes log.info("Mutating glyf/gvar tables") 194*e1fe3e4aSElliott Hughes gvar = varfont["gvar"] 195*e1fe3e4aSElliott Hughes glyf = varfont["glyf"] 196*e1fe3e4aSElliott Hughes hMetrics = varfont["hmtx"].metrics 197*e1fe3e4aSElliott Hughes vMetrics = getattr(varfont.get("vmtx"), "metrics", None) 198*e1fe3e4aSElliott Hughes # get list of glyph names in gvar sorted by component depth 199*e1fe3e4aSElliott Hughes glyphnames = sorted( 200*e1fe3e4aSElliott Hughes gvar.variations.keys(), 201*e1fe3e4aSElliott Hughes key=lambda name: ( 202*e1fe3e4aSElliott Hughes ( 203*e1fe3e4aSElliott Hughes glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth 204*e1fe3e4aSElliott Hughes if glyf[name].isComposite() or glyf[name].isVarComposite() 205*e1fe3e4aSElliott Hughes else 0 206*e1fe3e4aSElliott Hughes ), 207*e1fe3e4aSElliott Hughes name, 208*e1fe3e4aSElliott Hughes ), 209*e1fe3e4aSElliott Hughes ) 210*e1fe3e4aSElliott Hughes for glyphname in glyphnames: 211*e1fe3e4aSElliott Hughes variations = gvar.variations[glyphname] 212*e1fe3e4aSElliott Hughes coordinates, _ = glyf._getCoordinatesAndControls( 213*e1fe3e4aSElliott Hughes glyphname, hMetrics, vMetrics 214*e1fe3e4aSElliott Hughes ) 215*e1fe3e4aSElliott Hughes origCoords, endPts = None, None 216*e1fe3e4aSElliott Hughes for var in variations: 217*e1fe3e4aSElliott Hughes scalar = supportScalar(loc, var.axes) 218*e1fe3e4aSElliott Hughes if not scalar: 219*e1fe3e4aSElliott Hughes continue 220*e1fe3e4aSElliott Hughes delta = var.coordinates 221*e1fe3e4aSElliott Hughes if None in delta: 222*e1fe3e4aSElliott Hughes if origCoords is None: 223*e1fe3e4aSElliott Hughes origCoords, g = glyf._getCoordinatesAndControls( 224*e1fe3e4aSElliott Hughes glyphname, hMetrics, vMetrics 225*e1fe3e4aSElliott Hughes ) 226*e1fe3e4aSElliott Hughes delta = iup_delta(delta, origCoords, g.endPts) 227*e1fe3e4aSElliott Hughes coordinates += GlyphCoordinates(delta) * scalar 228*e1fe3e4aSElliott Hughes glyf._setCoordinates(glyphname, coordinates, hMetrics, vMetrics) 229*e1fe3e4aSElliott Hughes else: 230*e1fe3e4aSElliott Hughes glyf = None 231*e1fe3e4aSElliott Hughes 232*e1fe3e4aSElliott Hughes if "DSIG" in varfont: 233*e1fe3e4aSElliott Hughes del varfont["DSIG"] 234*e1fe3e4aSElliott Hughes 235*e1fe3e4aSElliott Hughes if "cvar" in varfont: 236*e1fe3e4aSElliott Hughes log.info("Mutating cvt/cvar tables") 237*e1fe3e4aSElliott Hughes cvar = varfont["cvar"] 238*e1fe3e4aSElliott Hughes cvt = varfont["cvt "] 239*e1fe3e4aSElliott Hughes deltas = {} 240*e1fe3e4aSElliott Hughes for var in cvar.variations: 241*e1fe3e4aSElliott Hughes scalar = supportScalar(loc, var.axes) 242*e1fe3e4aSElliott Hughes if not scalar: 243*e1fe3e4aSElliott Hughes continue 244*e1fe3e4aSElliott Hughes for i, c in enumerate(var.coordinates): 245*e1fe3e4aSElliott Hughes if c is not None: 246*e1fe3e4aSElliott Hughes deltas[i] = deltas.get(i, 0) + scalar * c 247*e1fe3e4aSElliott Hughes for i, delta in deltas.items(): 248*e1fe3e4aSElliott Hughes cvt[i] += otRound(delta) 249*e1fe3e4aSElliott Hughes 250*e1fe3e4aSElliott Hughes if "CFF2" in varfont: 251*e1fe3e4aSElliott Hughes log.info("Mutating CFF2 table") 252*e1fe3e4aSElliott Hughes glyphOrder = varfont.getGlyphOrder() 253*e1fe3e4aSElliott Hughes CFF2 = varfont["CFF2"] 254*e1fe3e4aSElliott Hughes topDict = CFF2.cff.topDictIndex[0] 255*e1fe3e4aSElliott Hughes vsInstancer = VarStoreInstancer(topDict.VarStore.otVarStore, fvar.axes, loc) 256*e1fe3e4aSElliott Hughes interpolateFromDeltas = vsInstancer.interpolateFromDeltas 257*e1fe3e4aSElliott Hughes interpolate_cff2_PrivateDict(topDict, interpolateFromDeltas) 258*e1fe3e4aSElliott Hughes CFF2.desubroutinize() 259*e1fe3e4aSElliott Hughes interpolate_cff2_charstrings(topDict, interpolateFromDeltas, glyphOrder) 260*e1fe3e4aSElliott Hughes interpolate_cff2_metrics(varfont, topDict, glyphOrder, loc) 261*e1fe3e4aSElliott Hughes del topDict.rawDict["VarStore"] 262*e1fe3e4aSElliott Hughes del topDict.VarStore 263*e1fe3e4aSElliott Hughes 264*e1fe3e4aSElliott Hughes if "MVAR" in varfont: 265*e1fe3e4aSElliott Hughes log.info("Mutating MVAR table") 266*e1fe3e4aSElliott Hughes mvar = varfont["MVAR"].table 267*e1fe3e4aSElliott Hughes varStoreInstancer = VarStoreInstancer(mvar.VarStore, fvar.axes, loc) 268*e1fe3e4aSElliott Hughes records = mvar.ValueRecord 269*e1fe3e4aSElliott Hughes for rec in records: 270*e1fe3e4aSElliott Hughes mvarTag = rec.ValueTag 271*e1fe3e4aSElliott Hughes if mvarTag not in MVAR_ENTRIES: 272*e1fe3e4aSElliott Hughes continue 273*e1fe3e4aSElliott Hughes tableTag, itemName = MVAR_ENTRIES[mvarTag] 274*e1fe3e4aSElliott Hughes delta = otRound(varStoreInstancer[rec.VarIdx]) 275*e1fe3e4aSElliott Hughes if not delta: 276*e1fe3e4aSElliott Hughes continue 277*e1fe3e4aSElliott Hughes setattr( 278*e1fe3e4aSElliott Hughes varfont[tableTag], 279*e1fe3e4aSElliott Hughes itemName, 280*e1fe3e4aSElliott Hughes getattr(varfont[tableTag], itemName) + delta, 281*e1fe3e4aSElliott Hughes ) 282*e1fe3e4aSElliott Hughes 283*e1fe3e4aSElliott Hughes log.info("Mutating FeatureVariations") 284*e1fe3e4aSElliott Hughes for tableTag in "GSUB", "GPOS": 285*e1fe3e4aSElliott Hughes if not tableTag in varfont: 286*e1fe3e4aSElliott Hughes continue 287*e1fe3e4aSElliott Hughes table = varfont[tableTag].table 288*e1fe3e4aSElliott Hughes if not getattr(table, "FeatureVariations", None): 289*e1fe3e4aSElliott Hughes continue 290*e1fe3e4aSElliott Hughes variations = table.FeatureVariations 291*e1fe3e4aSElliott Hughes for record in variations.FeatureVariationRecord: 292*e1fe3e4aSElliott Hughes applies = True 293*e1fe3e4aSElliott Hughes for condition in record.ConditionSet.ConditionTable: 294*e1fe3e4aSElliott Hughes if condition.Format == 1: 295*e1fe3e4aSElliott Hughes axisIdx = condition.AxisIndex 296*e1fe3e4aSElliott Hughes axisTag = fvar.axes[axisIdx].axisTag 297*e1fe3e4aSElliott Hughes Min = condition.FilterRangeMinValue 298*e1fe3e4aSElliott Hughes Max = condition.FilterRangeMaxValue 299*e1fe3e4aSElliott Hughes v = loc[axisTag] 300*e1fe3e4aSElliott Hughes if not (Min <= v <= Max): 301*e1fe3e4aSElliott Hughes applies = False 302*e1fe3e4aSElliott Hughes else: 303*e1fe3e4aSElliott Hughes applies = False 304*e1fe3e4aSElliott Hughes if not applies: 305*e1fe3e4aSElliott Hughes break 306*e1fe3e4aSElliott Hughes 307*e1fe3e4aSElliott Hughes if applies: 308*e1fe3e4aSElliott Hughes assert record.FeatureTableSubstitution.Version == 0x00010000 309*e1fe3e4aSElliott Hughes for rec in record.FeatureTableSubstitution.SubstitutionRecord: 310*e1fe3e4aSElliott Hughes table.FeatureList.FeatureRecord[rec.FeatureIndex].Feature = ( 311*e1fe3e4aSElliott Hughes rec.Feature 312*e1fe3e4aSElliott Hughes ) 313*e1fe3e4aSElliott Hughes break 314*e1fe3e4aSElliott Hughes del table.FeatureVariations 315*e1fe3e4aSElliott Hughes 316*e1fe3e4aSElliott Hughes if "GDEF" in varfont and varfont["GDEF"].table.Version >= 0x00010003: 317*e1fe3e4aSElliott Hughes log.info("Mutating GDEF/GPOS/GSUB tables") 318*e1fe3e4aSElliott Hughes gdef = varfont["GDEF"].table 319*e1fe3e4aSElliott Hughes instancer = VarStoreInstancer(gdef.VarStore, fvar.axes, loc) 320*e1fe3e4aSElliott Hughes 321*e1fe3e4aSElliott Hughes merger = MutatorMerger(varfont, instancer) 322*e1fe3e4aSElliott Hughes merger.mergeTables(varfont, [varfont], ["GDEF", "GPOS"]) 323*e1fe3e4aSElliott Hughes 324*e1fe3e4aSElliott Hughes # Downgrade GDEF. 325*e1fe3e4aSElliott Hughes del gdef.VarStore 326*e1fe3e4aSElliott Hughes gdef.Version = 0x00010002 327*e1fe3e4aSElliott Hughes if gdef.MarkGlyphSetsDef is None: 328*e1fe3e4aSElliott Hughes del gdef.MarkGlyphSetsDef 329*e1fe3e4aSElliott Hughes gdef.Version = 0x00010000 330*e1fe3e4aSElliott Hughes 331*e1fe3e4aSElliott Hughes if not ( 332*e1fe3e4aSElliott Hughes gdef.LigCaretList 333*e1fe3e4aSElliott Hughes or gdef.MarkAttachClassDef 334*e1fe3e4aSElliott Hughes or gdef.GlyphClassDef 335*e1fe3e4aSElliott Hughes or gdef.AttachList 336*e1fe3e4aSElliott Hughes or (gdef.Version >= 0x00010002 and gdef.MarkGlyphSetsDef) 337*e1fe3e4aSElliott Hughes ): 338*e1fe3e4aSElliott Hughes del varfont["GDEF"] 339*e1fe3e4aSElliott Hughes 340*e1fe3e4aSElliott Hughes addidef = False 341*e1fe3e4aSElliott Hughes if glyf: 342*e1fe3e4aSElliott Hughes for glyph in glyf.glyphs.values(): 343*e1fe3e4aSElliott Hughes if hasattr(glyph, "program"): 344*e1fe3e4aSElliott Hughes instructions = glyph.program.getAssembly() 345*e1fe3e4aSElliott Hughes # If GETVARIATION opcode is used in bytecode of any glyph add IDEF 346*e1fe3e4aSElliott Hughes addidef = any(op.startswith("GETVARIATION") for op in instructions) 347*e1fe3e4aSElliott Hughes if addidef: 348*e1fe3e4aSElliott Hughes break 349*e1fe3e4aSElliott Hughes if overlap: 350*e1fe3e4aSElliott Hughes for glyph_name in glyf.keys(): 351*e1fe3e4aSElliott Hughes glyph = glyf[glyph_name] 352*e1fe3e4aSElliott Hughes # Set OVERLAP_COMPOUND bit for compound glyphs 353*e1fe3e4aSElliott Hughes if glyph.isComposite(): 354*e1fe3e4aSElliott Hughes glyph.components[0].flags |= OVERLAP_COMPOUND 355*e1fe3e4aSElliott Hughes # Set OVERLAP_SIMPLE bit for simple glyphs 356*e1fe3e4aSElliott Hughes elif glyph.numberOfContours > 0: 357*e1fe3e4aSElliott Hughes glyph.flags[0] |= flagOverlapSimple 358*e1fe3e4aSElliott Hughes if addidef: 359*e1fe3e4aSElliott Hughes log.info("Adding IDEF to fpgm table for GETVARIATION opcode") 360*e1fe3e4aSElliott Hughes asm = [] 361*e1fe3e4aSElliott Hughes if "fpgm" in varfont: 362*e1fe3e4aSElliott Hughes fpgm = varfont["fpgm"] 363*e1fe3e4aSElliott Hughes asm = fpgm.program.getAssembly() 364*e1fe3e4aSElliott Hughes else: 365*e1fe3e4aSElliott Hughes fpgm = newTable("fpgm") 366*e1fe3e4aSElliott Hughes fpgm.program = ttProgram.Program() 367*e1fe3e4aSElliott Hughes varfont["fpgm"] = fpgm 368*e1fe3e4aSElliott Hughes asm.append("PUSHB[000] 145") 369*e1fe3e4aSElliott Hughes asm.append("IDEF[ ]") 370*e1fe3e4aSElliott Hughes args = [str(len(loc))] 371*e1fe3e4aSElliott Hughes for a in fvar.axes: 372*e1fe3e4aSElliott Hughes args.append(str(floatToFixed(loc[a.axisTag], 14))) 373*e1fe3e4aSElliott Hughes asm.append("NPUSHW[ ] " + " ".join(args)) 374*e1fe3e4aSElliott Hughes asm.append("ENDF[ ]") 375*e1fe3e4aSElliott Hughes fpgm.program.fromAssembly(asm) 376*e1fe3e4aSElliott Hughes 377*e1fe3e4aSElliott Hughes # Change maxp attributes as IDEF is added 378*e1fe3e4aSElliott Hughes if "maxp" in varfont: 379*e1fe3e4aSElliott Hughes maxp = varfont["maxp"] 380*e1fe3e4aSElliott Hughes setattr( 381*e1fe3e4aSElliott Hughes maxp, "maxInstructionDefs", 1 + getattr(maxp, "maxInstructionDefs", 0) 382*e1fe3e4aSElliott Hughes ) 383*e1fe3e4aSElliott Hughes setattr( 384*e1fe3e4aSElliott Hughes maxp, 385*e1fe3e4aSElliott Hughes "maxStackElements", 386*e1fe3e4aSElliott Hughes max(len(loc), getattr(maxp, "maxStackElements", 0)), 387*e1fe3e4aSElliott Hughes ) 388*e1fe3e4aSElliott Hughes 389*e1fe3e4aSElliott Hughes if "name" in varfont: 390*e1fe3e4aSElliott Hughes log.info("Pruning name table") 391*e1fe3e4aSElliott Hughes exclude = {a.axisNameID for a in fvar.axes} 392*e1fe3e4aSElliott Hughes for i in fvar.instances: 393*e1fe3e4aSElliott Hughes exclude.add(i.subfamilyNameID) 394*e1fe3e4aSElliott Hughes exclude.add(i.postscriptNameID) 395*e1fe3e4aSElliott Hughes if "ltag" in varfont: 396*e1fe3e4aSElliott Hughes # Drop the whole 'ltag' table if all its language tags are referenced by 397*e1fe3e4aSElliott Hughes # name records to be pruned. 398*e1fe3e4aSElliott Hughes # TODO: prune unused ltag tags and re-enumerate langIDs accordingly 399*e1fe3e4aSElliott Hughes excludedUnicodeLangIDs = [ 400*e1fe3e4aSElliott Hughes n.langID 401*e1fe3e4aSElliott Hughes for n in varfont["name"].names 402*e1fe3e4aSElliott Hughes if n.nameID in exclude and n.platformID == 0 and n.langID != 0xFFFF 403*e1fe3e4aSElliott Hughes ] 404*e1fe3e4aSElliott Hughes if set(excludedUnicodeLangIDs) == set(range(len((varfont["ltag"].tags)))): 405*e1fe3e4aSElliott Hughes del varfont["ltag"] 406*e1fe3e4aSElliott Hughes varfont["name"].names[:] = [ 407*e1fe3e4aSElliott Hughes n for n in varfont["name"].names if n.nameID not in exclude 408*e1fe3e4aSElliott Hughes ] 409*e1fe3e4aSElliott Hughes 410*e1fe3e4aSElliott Hughes if "wght" in location and "OS/2" in varfont: 411*e1fe3e4aSElliott Hughes varfont["OS/2"].usWeightClass = otRound(max(1, min(location["wght"], 1000))) 412*e1fe3e4aSElliott Hughes if "wdth" in location: 413*e1fe3e4aSElliott Hughes wdth = location["wdth"] 414*e1fe3e4aSElliott Hughes for percent, widthClass in sorted(OS2_WIDTH_CLASS_VALUES.items()): 415*e1fe3e4aSElliott Hughes if wdth < percent: 416*e1fe3e4aSElliott Hughes varfont["OS/2"].usWidthClass = widthClass 417*e1fe3e4aSElliott Hughes break 418*e1fe3e4aSElliott Hughes else: 419*e1fe3e4aSElliott Hughes varfont["OS/2"].usWidthClass = 9 420*e1fe3e4aSElliott Hughes if "slnt" in location and "post" in varfont: 421*e1fe3e4aSElliott Hughes varfont["post"].italicAngle = max(-90, min(location["slnt"], 90)) 422*e1fe3e4aSElliott Hughes 423*e1fe3e4aSElliott Hughes log.info("Removing variable tables") 424*e1fe3e4aSElliott Hughes for tag in ("avar", "cvar", "fvar", "gvar", "HVAR", "MVAR", "VVAR", "STAT"): 425*e1fe3e4aSElliott Hughes if tag in varfont: 426*e1fe3e4aSElliott Hughes del varfont[tag] 427*e1fe3e4aSElliott Hughes 428*e1fe3e4aSElliott Hughes return varfont 429*e1fe3e4aSElliott Hughes 430*e1fe3e4aSElliott Hughes 431*e1fe3e4aSElliott Hughesdef main(args=None): 432*e1fe3e4aSElliott Hughes """Instantiate a variation font""" 433*e1fe3e4aSElliott Hughes from fontTools import configLogger 434*e1fe3e4aSElliott Hughes import argparse 435*e1fe3e4aSElliott Hughes 436*e1fe3e4aSElliott Hughes parser = argparse.ArgumentParser( 437*e1fe3e4aSElliott Hughes "fonttools varLib.mutator", description="Instantiate a variable font" 438*e1fe3e4aSElliott Hughes ) 439*e1fe3e4aSElliott Hughes parser.add_argument("input", metavar="INPUT.ttf", help="Input variable TTF file.") 440*e1fe3e4aSElliott Hughes parser.add_argument( 441*e1fe3e4aSElliott Hughes "locargs", 442*e1fe3e4aSElliott Hughes metavar="AXIS=LOC", 443*e1fe3e4aSElliott Hughes nargs="*", 444*e1fe3e4aSElliott Hughes help="List of space separated locations. A location consist in " 445*e1fe3e4aSElliott Hughes "the name of a variation axis, followed by '=' and a number. E.g.: " 446*e1fe3e4aSElliott Hughes " wght=700 wdth=80. The default is the location of the base master.", 447*e1fe3e4aSElliott Hughes ) 448*e1fe3e4aSElliott Hughes parser.add_argument( 449*e1fe3e4aSElliott Hughes "-o", 450*e1fe3e4aSElliott Hughes "--output", 451*e1fe3e4aSElliott Hughes metavar="OUTPUT.ttf", 452*e1fe3e4aSElliott Hughes default=None, 453*e1fe3e4aSElliott Hughes help="Output instance TTF file (default: INPUT-instance.ttf).", 454*e1fe3e4aSElliott Hughes ) 455*e1fe3e4aSElliott Hughes parser.add_argument( 456*e1fe3e4aSElliott Hughes "--no-recalc-timestamp", 457*e1fe3e4aSElliott Hughes dest="recalc_timestamp", 458*e1fe3e4aSElliott Hughes action="store_false", 459*e1fe3e4aSElliott Hughes help="Don't set the output font's timestamp to the current time.", 460*e1fe3e4aSElliott Hughes ) 461*e1fe3e4aSElliott Hughes logging_group = parser.add_mutually_exclusive_group(required=False) 462*e1fe3e4aSElliott Hughes logging_group.add_argument( 463*e1fe3e4aSElliott Hughes "-v", "--verbose", action="store_true", help="Run more verbosely." 464*e1fe3e4aSElliott Hughes ) 465*e1fe3e4aSElliott Hughes logging_group.add_argument( 466*e1fe3e4aSElliott Hughes "-q", "--quiet", action="store_true", help="Turn verbosity off." 467*e1fe3e4aSElliott Hughes ) 468*e1fe3e4aSElliott Hughes parser.add_argument( 469*e1fe3e4aSElliott Hughes "--no-overlap", 470*e1fe3e4aSElliott Hughes dest="overlap", 471*e1fe3e4aSElliott Hughes action="store_false", 472*e1fe3e4aSElliott Hughes help="Don't set OVERLAP_SIMPLE/OVERLAP_COMPOUND glyf flags.", 473*e1fe3e4aSElliott Hughes ) 474*e1fe3e4aSElliott Hughes options = parser.parse_args(args) 475*e1fe3e4aSElliott Hughes 476*e1fe3e4aSElliott Hughes varfilename = options.input 477*e1fe3e4aSElliott Hughes outfile = ( 478*e1fe3e4aSElliott Hughes os.path.splitext(varfilename)[0] + "-instance.ttf" 479*e1fe3e4aSElliott Hughes if not options.output 480*e1fe3e4aSElliott Hughes else options.output 481*e1fe3e4aSElliott Hughes ) 482*e1fe3e4aSElliott Hughes configLogger( 483*e1fe3e4aSElliott Hughes level=("DEBUG" if options.verbose else "ERROR" if options.quiet else "INFO") 484*e1fe3e4aSElliott Hughes ) 485*e1fe3e4aSElliott Hughes 486*e1fe3e4aSElliott Hughes loc = {} 487*e1fe3e4aSElliott Hughes for arg in options.locargs: 488*e1fe3e4aSElliott Hughes try: 489*e1fe3e4aSElliott Hughes tag, val = arg.split("=") 490*e1fe3e4aSElliott Hughes assert len(tag) <= 4 491*e1fe3e4aSElliott Hughes loc[tag.ljust(4)] = float(val) 492*e1fe3e4aSElliott Hughes except (ValueError, AssertionError): 493*e1fe3e4aSElliott Hughes parser.error("invalid location argument format: %r" % arg) 494*e1fe3e4aSElliott Hughes log.info("Location: %s", loc) 495*e1fe3e4aSElliott Hughes 496*e1fe3e4aSElliott Hughes log.info("Loading variable font") 497*e1fe3e4aSElliott Hughes varfont = TTFont(varfilename, recalcTimestamp=options.recalc_timestamp) 498*e1fe3e4aSElliott Hughes 499*e1fe3e4aSElliott Hughes instantiateVariableFont(varfont, loc, inplace=True, overlap=options.overlap) 500*e1fe3e4aSElliott Hughes 501*e1fe3e4aSElliott Hughes log.info("Saving instance font %s", outfile) 502*e1fe3e4aSElliott Hughes varfont.save(outfile) 503*e1fe3e4aSElliott Hughes 504*e1fe3e4aSElliott Hughes 505*e1fe3e4aSElliott Hughesif __name__ == "__main__": 506*e1fe3e4aSElliott Hughes import sys 507*e1fe3e4aSElliott Hughes 508*e1fe3e4aSElliott Hughes if len(sys.argv) > 1: 509*e1fe3e4aSElliott Hughes sys.exit(main()) 510*e1fe3e4aSElliott Hughes import doctest 511*e1fe3e4aSElliott Hughes 512*e1fe3e4aSElliott Hughes sys.exit(doctest.testmod().failed) 513