aboutsummaryrefslogtreecommitdiff
path: root/Lib/fontTools/varLib/mutator.py
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/fontTools/varLib/mutator.py')
-rw-r--r--Lib/fontTools/varLib/mutator.py881
1 files changed, 463 insertions, 418 deletions
diff --git a/Lib/fontTools/varLib/mutator.py b/Lib/fontTools/varLib/mutator.py
index 2e674798..d1d123ab 100644
--- a/Lib/fontTools/varLib/mutator.py
+++ b/Lib/fontTools/varLib/mutator.py
@@ -8,11 +8,15 @@ from fontTools.misc.roundTools import otRound
from fontTools.pens.boundsPen import BoundsPen
from fontTools.ttLib import TTFont, newTable
from fontTools.ttLib.tables import ttProgram
-from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates, flagOverlapSimple, OVERLAP_COMPOUND
+from fontTools.ttLib.tables._g_l_y_f import (
+ GlyphCoordinates,
+ flagOverlapSimple,
+ OVERLAP_COMPOUND,
+)
from fontTools.varLib.models import (
- supportScalar,
- normalizeLocation,
- piecewiseLinearMap,
+ supportScalar,
+ normalizeLocation,
+ piecewiseLinearMap,
)
from fontTools.varLib.merger import MutatorMerger
from fontTools.varLib.varStore import VarStoreInstancer
@@ -30,435 +34,476 @@ log = logging.getLogger("fontTools.varlib.mutator")
OS2_WIDTH_CLASS_VALUES = {}
percents = [50.0, 62.5, 75.0, 87.5, 100.0, 112.5, 125.0, 150.0, 200.0]
for i, (prev, curr) in enumerate(zip(percents[:-1], percents[1:]), start=1):
- half = (prev + curr) / 2
- OS2_WIDTH_CLASS_VALUES[half] = i
+ half = (prev + curr) / 2
+ OS2_WIDTH_CLASS_VALUES[half] = i
def interpolate_cff2_PrivateDict(topDict, interpolateFromDeltas):
- pd_blend_lists = ("BlueValues", "OtherBlues", "FamilyBlues",
- "FamilyOtherBlues", "StemSnapH",
- "StemSnapV")
- pd_blend_values = ("BlueScale", "BlueShift",
- "BlueFuzz", "StdHW", "StdVW")
- for fontDict in topDict.FDArray:
- pd = fontDict.Private
- vsindex = pd.vsindex if (hasattr(pd, 'vsindex')) else 0
- for key, value in pd.rawDict.items():
- if (key in pd_blend_values) and isinstance(value, list):
- delta = interpolateFromDeltas(vsindex, value[1:])
- pd.rawDict[key] = otRound(value[0] + delta)
- elif (key in pd_blend_lists) and isinstance(value[0], list):
- """If any argument in a BlueValues list is a blend list,
- then they all are. The first value of each list is an
- absolute value. The delta tuples are calculated from
- relative master values, hence we need to append all the
- deltas to date to each successive absolute value."""
- delta = 0
- for i, val_list in enumerate(value):
- delta += otRound(interpolateFromDeltas(vsindex,
- val_list[1:]))
- value[i] = val_list[0] + delta
+ pd_blend_lists = (
+ "BlueValues",
+ "OtherBlues",
+ "FamilyBlues",
+ "FamilyOtherBlues",
+ "StemSnapH",
+ "StemSnapV",
+ )
+ pd_blend_values = ("BlueScale", "BlueShift", "BlueFuzz", "StdHW", "StdVW")
+ for fontDict in topDict.FDArray:
+ pd = fontDict.Private
+ vsindex = pd.vsindex if (hasattr(pd, "vsindex")) else 0
+ for key, value in pd.rawDict.items():
+ if (key in pd_blend_values) and isinstance(value, list):
+ delta = interpolateFromDeltas(vsindex, value[1:])
+ pd.rawDict[key] = otRound(value[0] + delta)
+ elif (key in pd_blend_lists) and isinstance(value[0], list):
+ """If any argument in a BlueValues list is a blend list,
+ then they all are. The first value of each list is an
+ absolute value. The delta tuples are calculated from
+ relative master values, hence we need to append all the
+ deltas to date to each successive absolute value."""
+ delta = 0
+ for i, val_list in enumerate(value):
+ delta += otRound(interpolateFromDeltas(vsindex, val_list[1:]))
+ value[i] = val_list[0] + delta
def interpolate_cff2_charstrings(topDict, interpolateFromDeltas, glyphOrder):
- charstrings = topDict.CharStrings
- for gname in glyphOrder:
- # Interpolate charstring
- # e.g replace blend op args with regular args,
- # and use and discard vsindex op.
- charstring = charstrings[gname]
- new_program = []
- vsindex = 0
- last_i = 0
- for i, token in enumerate(charstring.program):
- if token == 'vsindex':
- vsindex = charstring.program[i - 1]
- if last_i != 0:
- new_program.extend(charstring.program[last_i:i - 1])
- last_i = i + 1
- elif token == 'blend':
- num_regions = charstring.getNumRegions(vsindex)
- numMasters = 1 + num_regions
- num_args = charstring.program[i - 1]
- # The program list starting at program[i] is now:
- # ..args for following operations
- # num_args values from the default font
- # num_args tuples, each with numMasters-1 delta values
- # num_blend_args
- # 'blend'
- argi = i - (num_args * numMasters + 1)
- end_args = tuplei = argi + num_args
- while argi < end_args:
- next_ti = tuplei + num_regions
- deltas = charstring.program[tuplei:next_ti]
- delta = interpolateFromDeltas(vsindex, deltas)
- charstring.program[argi] += otRound(delta)
- tuplei = next_ti
- argi += 1
- new_program.extend(charstring.program[last_i:end_args])
- last_i = i + 1
- if last_i != 0:
- new_program.extend(charstring.program[last_i:])
- charstring.program = new_program
+ charstrings = topDict.CharStrings
+ for gname in glyphOrder:
+ # Interpolate charstring
+ # e.g replace blend op args with regular args,
+ # and use and discard vsindex op.
+ charstring = charstrings[gname]
+ new_program = []
+ vsindex = 0
+ last_i = 0
+ for i, token in enumerate(charstring.program):
+ if token == "vsindex":
+ vsindex = charstring.program[i - 1]
+ if last_i != 0:
+ new_program.extend(charstring.program[last_i : i - 1])
+ last_i = i + 1
+ elif token == "blend":
+ num_regions = charstring.getNumRegions(vsindex)
+ numMasters = 1 + num_regions
+ num_args = charstring.program[i - 1]
+ # The program list starting at program[i] is now:
+ # ..args for following operations
+ # num_args values from the default font
+ # num_args tuples, each with numMasters-1 delta values
+ # num_blend_args
+ # 'blend'
+ argi = i - (num_args * numMasters + 1)
+ end_args = tuplei = argi + num_args
+ while argi < end_args:
+ next_ti = tuplei + num_regions
+ deltas = charstring.program[tuplei:next_ti]
+ delta = interpolateFromDeltas(vsindex, deltas)
+ charstring.program[argi] += otRound(delta)
+ tuplei = next_ti
+ argi += 1
+ new_program.extend(charstring.program[last_i:end_args])
+ last_i = i + 1
+ if last_i != 0:
+ new_program.extend(charstring.program[last_i:])
+ charstring.program = new_program
def interpolate_cff2_metrics(varfont, topDict, glyphOrder, loc):
- """Unlike TrueType glyphs, neither advance width nor bounding box
- info is stored in a CFF2 charstring. The width data exists only in
- the hmtx and HVAR tables. Since LSB data cannot be interpolated
- reliably from the master LSB values in the hmtx table, we traverse
- the charstring to determine the actual bound box. """
-
- charstrings = topDict.CharStrings
- boundsPen = BoundsPen(glyphOrder)
- hmtx = varfont['hmtx']
- hvar_table = None
- if 'HVAR' in varfont:
- hvar_table = varfont['HVAR'].table
- fvar = varfont['fvar']
- varStoreInstancer = VarStoreInstancer(hvar_table.VarStore, fvar.axes, loc)
-
- for gid, gname in enumerate(glyphOrder):
- entry = list(hmtx[gname])
- # get width delta.
- if hvar_table:
- if hvar_table.AdvWidthMap:
- width_idx = hvar_table.AdvWidthMap.mapping[gname]
- else:
- width_idx = gid
- width_delta = otRound(varStoreInstancer[width_idx])
- else:
- width_delta = 0
-
- # get LSB.
- boundsPen.init()
- charstring = charstrings[gname]
- charstring.draw(boundsPen)
- if boundsPen.bounds is None:
- # Happens with non-marking glyphs
- lsb_delta = 0
- else:
- lsb = otRound(boundsPen.bounds[0])
- lsb_delta = entry[1] - lsb
-
- if lsb_delta or width_delta:
- if width_delta:
- entry[0] += width_delta
- if lsb_delta:
- entry[1] = lsb
- hmtx[gname] = tuple(entry)
+ """Unlike TrueType glyphs, neither advance width nor bounding box
+ info is stored in a CFF2 charstring. The width data exists only in
+ the hmtx and HVAR tables. Since LSB data cannot be interpolated
+ reliably from the master LSB values in the hmtx table, we traverse
+ the charstring to determine the actual bound box."""
+
+ charstrings = topDict.CharStrings
+ boundsPen = BoundsPen(glyphOrder)
+ hmtx = varfont["hmtx"]
+ hvar_table = None
+ if "HVAR" in varfont:
+ hvar_table = varfont["HVAR"].table
+ fvar = varfont["fvar"]
+ varStoreInstancer = VarStoreInstancer(hvar_table.VarStore, fvar.axes, loc)
+
+ for gid, gname in enumerate(glyphOrder):
+ entry = list(hmtx[gname])
+ # get width delta.
+ if hvar_table:
+ if hvar_table.AdvWidthMap:
+ width_idx = hvar_table.AdvWidthMap.mapping[gname]
+ else:
+ width_idx = gid
+ width_delta = otRound(varStoreInstancer[width_idx])
+ else:
+ width_delta = 0
+
+ # get LSB.
+ boundsPen.init()
+ charstring = charstrings[gname]
+ charstring.draw(boundsPen)
+ if boundsPen.bounds is None:
+ # Happens with non-marking glyphs
+ lsb_delta = 0
+ else:
+ lsb = otRound(boundsPen.bounds[0])
+ lsb_delta = entry[1] - lsb
+
+ if lsb_delta or width_delta:
+ if width_delta:
+ entry[0] = max(0, entry[0] + width_delta)
+ if lsb_delta:
+ entry[1] = lsb
+ hmtx[gname] = tuple(entry)
def instantiateVariableFont(varfont, location, inplace=False, overlap=True):
- """ Generate a static instance from a variable TTFont and a dictionary
- defining the desired location along the variable font's axes.
- The location values must be specified as user-space coordinates, e.g.:
-
- {'wght': 400, 'wdth': 100}
-
- By default, a new TTFont object is returned. If ``inplace`` is True, the
- input varfont is modified and reduced to a static font.
-
- When the overlap parameter is defined as True,
- OVERLAP_SIMPLE and OVERLAP_COMPOUND bits are set to 1. See
- https://docs.microsoft.com/en-us/typography/opentype/spec/glyf
- """
- if not inplace:
- # make a copy to leave input varfont unmodified
- stream = BytesIO()
- varfont.save(stream)
- stream.seek(0)
- varfont = TTFont(stream)
-
- fvar = varfont['fvar']
- axes = {a.axisTag:(a.minValue,a.defaultValue,a.maxValue) for a in fvar.axes}
- loc = normalizeLocation(location, axes)
- if 'avar' in varfont:
- maps = varfont['avar'].segments
- loc = {k: piecewiseLinearMap(v, maps[k]) for k,v in loc.items()}
- # Quantize to F2Dot14, to avoid surprise interpolations.
- loc = {k:floatToFixedToFloat(v, 14) for k,v in loc.items()}
- # Location is normalized now
- log.info("Normalized location: %s", loc)
-
- if 'gvar' in varfont:
- log.info("Mutating glyf/gvar tables")
- gvar = varfont['gvar']
- glyf = varfont['glyf']
- hMetrics = varfont['hmtx'].metrics
- vMetrics = getattr(varfont.get('vmtx'), 'metrics', None)
- # get list of glyph names in gvar sorted by component depth
- glyphnames = sorted(
- gvar.variations.keys(),
- key=lambda name: (
- glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth
- if glyf[name].isComposite() else 0,
- name))
- for glyphname in glyphnames:
- variations = gvar.variations[glyphname]
- coordinates, _ = glyf._getCoordinatesAndControls(glyphname, hMetrics, vMetrics)
- origCoords, endPts = None, None
- for var in variations:
- scalar = supportScalar(loc, var.axes)
- if not scalar: continue
- delta = var.coordinates
- if None in delta:
- if origCoords is None:
- origCoords, g = glyf._getCoordinatesAndControls(glyphname, hMetrics, vMetrics)
- delta = iup_delta(delta, origCoords, g.endPts)
- coordinates += GlyphCoordinates(delta) * scalar
- glyf._setCoordinates(glyphname, coordinates, hMetrics, vMetrics)
- else:
- glyf = None
-
- if 'cvar' in varfont:
- log.info("Mutating cvt/cvar tables")
- cvar = varfont['cvar']
- cvt = varfont['cvt ']
- deltas = {}
- for var in cvar.variations:
- scalar = supportScalar(loc, var.axes)
- if not scalar: continue
- for i, c in enumerate(var.coordinates):
- if c is not None:
- deltas[i] = deltas.get(i, 0) + scalar * c
- for i, delta in deltas.items():
- cvt[i] += otRound(delta)
-
- if 'CFF2' in varfont:
- log.info("Mutating CFF2 table")
- glyphOrder = varfont.getGlyphOrder()
- CFF2 = varfont['CFF2']
- topDict = CFF2.cff.topDictIndex[0]
- vsInstancer = VarStoreInstancer(topDict.VarStore.otVarStore, fvar.axes, loc)
- interpolateFromDeltas = vsInstancer.interpolateFromDeltas
- interpolate_cff2_PrivateDict(topDict, interpolateFromDeltas)
- CFF2.desubroutinize()
- interpolate_cff2_charstrings(topDict, interpolateFromDeltas, glyphOrder)
- interpolate_cff2_metrics(varfont, topDict, glyphOrder, loc)
- del topDict.rawDict['VarStore']
- del topDict.VarStore
-
- if 'MVAR' in varfont:
- log.info("Mutating MVAR table")
- mvar = varfont['MVAR'].table
- varStoreInstancer = VarStoreInstancer(mvar.VarStore, fvar.axes, loc)
- records = mvar.ValueRecord
- for rec in records:
- mvarTag = rec.ValueTag
- if mvarTag not in MVAR_ENTRIES:
- continue
- tableTag, itemName = MVAR_ENTRIES[mvarTag]
- delta = otRound(varStoreInstancer[rec.VarIdx])
- if not delta:
- continue
- setattr(varfont[tableTag], itemName,
- getattr(varfont[tableTag], itemName) + delta)
-
- log.info("Mutating FeatureVariations")
- for tableTag in 'GSUB','GPOS':
- if not tableTag in varfont:
- continue
- table = varfont[tableTag].table
- if not getattr(table, 'FeatureVariations', None):
- continue
- variations = table.FeatureVariations
- for record in variations.FeatureVariationRecord:
- applies = True
- for condition in record.ConditionSet.ConditionTable:
- if condition.Format == 1:
- axisIdx = condition.AxisIndex
- axisTag = fvar.axes[axisIdx].axisTag
- Min = condition.FilterRangeMinValue
- Max = condition.FilterRangeMaxValue
- v = loc[axisTag]
- if not (Min <= v <= Max):
- applies = False
- else:
- applies = False
- if not applies:
- break
-
- if applies:
- assert record.FeatureTableSubstitution.Version == 0x00010000
- for rec in record.FeatureTableSubstitution.SubstitutionRecord:
- table.FeatureList.FeatureRecord[rec.FeatureIndex].Feature = rec.Feature
- break
- del table.FeatureVariations
-
- if 'GDEF' in varfont and varfont['GDEF'].table.Version >= 0x00010003:
- log.info("Mutating GDEF/GPOS/GSUB tables")
- gdef = varfont['GDEF'].table
- instancer = VarStoreInstancer(gdef.VarStore, fvar.axes, loc)
-
- merger = MutatorMerger(varfont, instancer)
- merger.mergeTables(varfont, [varfont], ['GDEF', 'GPOS'])
-
- # Downgrade GDEF.
- del gdef.VarStore
- gdef.Version = 0x00010002
- if gdef.MarkGlyphSetsDef is None:
- del gdef.MarkGlyphSetsDef
- gdef.Version = 0x00010000
-
- if not (gdef.LigCaretList or
- gdef.MarkAttachClassDef or
- gdef.GlyphClassDef or
- gdef.AttachList or
- (gdef.Version >= 0x00010002 and gdef.MarkGlyphSetsDef)):
- del varfont['GDEF']
-
- addidef = False
- if glyf:
- for glyph in glyf.glyphs.values():
- if hasattr(glyph, "program"):
- instructions = glyph.program.getAssembly()
- # If GETVARIATION opcode is used in bytecode of any glyph add IDEF
- addidef = any(op.startswith("GETVARIATION") for op in instructions)
- if addidef:
- break
- if overlap:
- for glyph_name in glyf.keys():
- glyph = glyf[glyph_name]
- # Set OVERLAP_COMPOUND bit for compound glyphs
- if glyph.isComposite():
- glyph.components[0].flags |= OVERLAP_COMPOUND
- # Set OVERLAP_SIMPLE bit for simple glyphs
- elif glyph.numberOfContours > 0:
- glyph.flags[0] |= flagOverlapSimple
- if addidef:
- log.info("Adding IDEF to fpgm table for GETVARIATION opcode")
- asm = []
- if 'fpgm' in varfont:
- fpgm = varfont['fpgm']
- asm = fpgm.program.getAssembly()
- else:
- fpgm = newTable('fpgm')
- fpgm.program = ttProgram.Program()
- varfont['fpgm'] = fpgm
- asm.append("PUSHB[000] 145")
- asm.append("IDEF[ ]")
- args = [str(len(loc))]
- for a in fvar.axes:
- args.append(str(floatToFixed(loc[a.axisTag], 14)))
- asm.append("NPUSHW[ ] " + ' '.join(args))
- asm.append("ENDF[ ]")
- fpgm.program.fromAssembly(asm)
-
- # Change maxp attributes as IDEF is added
- if 'maxp' in varfont:
- maxp = varfont['maxp']
- setattr(maxp, "maxInstructionDefs", 1 + getattr(maxp, "maxInstructionDefs", 0))
- setattr(maxp, "maxStackElements", max(len(loc), getattr(maxp, "maxStackElements", 0)))
-
- if 'name' in varfont:
- log.info("Pruning name table")
- exclude = {a.axisNameID for a in fvar.axes}
- for i in fvar.instances:
- exclude.add(i.subfamilyNameID)
- exclude.add(i.postscriptNameID)
- if 'ltag' in varfont:
- # Drop the whole 'ltag' table if all its language tags are referenced by
- # name records to be pruned.
- # TODO: prune unused ltag tags and re-enumerate langIDs accordingly
- excludedUnicodeLangIDs = [
- n.langID for n in varfont['name'].names
- if n.nameID in exclude and n.platformID == 0 and n.langID != 0xFFFF
- ]
- if set(excludedUnicodeLangIDs) == set(range(len((varfont['ltag'].tags)))):
- del varfont['ltag']
- varfont['name'].names[:] = [
- n for n in varfont['name'].names
- if n.nameID not in exclude
- ]
-
- if "wght" in location and "OS/2" in varfont:
- varfont["OS/2"].usWeightClass = otRound(
- max(1, min(location["wght"], 1000))
- )
- if "wdth" in location:
- wdth = location["wdth"]
- for percent, widthClass in sorted(OS2_WIDTH_CLASS_VALUES.items()):
- if wdth < percent:
- varfont["OS/2"].usWidthClass = widthClass
- break
- else:
- varfont["OS/2"].usWidthClass = 9
- if "slnt" in location and "post" in varfont:
- varfont["post"].italicAngle = max(-90, min(location["slnt"], 90))
-
- log.info("Removing variable tables")
- for tag in ('avar','cvar','fvar','gvar','HVAR','MVAR','VVAR','STAT'):
- if tag in varfont:
- del varfont[tag]
-
- return varfont
+ """Generate a static instance from a variable TTFont and a dictionary
+ defining the desired location along the variable font's axes.
+ The location values must be specified as user-space coordinates, e.g.:
+
+ {'wght': 400, 'wdth': 100}
+
+ By default, a new TTFont object is returned. If ``inplace`` is True, the
+ input varfont is modified and reduced to a static font.
+
+ When the overlap parameter is defined as True,
+ OVERLAP_SIMPLE and OVERLAP_COMPOUND bits are set to 1. See
+ https://docs.microsoft.com/en-us/typography/opentype/spec/glyf
+ """
+ if not inplace:
+ # make a copy to leave input varfont unmodified
+ stream = BytesIO()
+ varfont.save(stream)
+ stream.seek(0)
+ varfont = TTFont(stream)
+
+ fvar = varfont["fvar"]
+ axes = {a.axisTag: (a.minValue, a.defaultValue, a.maxValue) for a in fvar.axes}
+ loc = normalizeLocation(location, axes)
+ if "avar" in varfont:
+ maps = varfont["avar"].segments
+ loc = {k: piecewiseLinearMap(v, maps[k]) for k, v in loc.items()}
+ # Quantize to F2Dot14, to avoid surprise interpolations.
+ loc = {k: floatToFixedToFloat(v, 14) for k, v in loc.items()}
+ # Location is normalized now
+ log.info("Normalized location: %s", loc)
+
+ if "gvar" in varfont:
+ log.info("Mutating glyf/gvar tables")
+ gvar = varfont["gvar"]
+ glyf = varfont["glyf"]
+ hMetrics = varfont["hmtx"].metrics
+ vMetrics = getattr(varfont.get("vmtx"), "metrics", None)
+ # get list of glyph names in gvar sorted by component depth
+ glyphnames = sorted(
+ gvar.variations.keys(),
+ key=lambda name: (
+ glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth
+ if glyf[name].isComposite() or glyf[name].isVarComposite()
+ else 0,
+ name,
+ ),
+ )
+ for glyphname in glyphnames:
+ variations = gvar.variations[glyphname]
+ coordinates, _ = glyf._getCoordinatesAndControls(
+ glyphname, hMetrics, vMetrics
+ )
+ origCoords, endPts = None, None
+ for var in variations:
+ scalar = supportScalar(loc, var.axes)
+ if not scalar:
+ continue
+ delta = var.coordinates
+ if None in delta:
+ if origCoords is None:
+ origCoords, g = glyf._getCoordinatesAndControls(
+ glyphname, hMetrics, vMetrics
+ )
+ delta = iup_delta(delta, origCoords, g.endPts)
+ coordinates += GlyphCoordinates(delta) * scalar
+ glyf._setCoordinates(glyphname, coordinates, hMetrics, vMetrics)
+ else:
+ glyf = None
+
+ if "DSIG" in varfont:
+ del varfont["DSIG"]
+
+ if "cvar" in varfont:
+ log.info("Mutating cvt/cvar tables")
+ cvar = varfont["cvar"]
+ cvt = varfont["cvt "]
+ deltas = {}
+ for var in cvar.variations:
+ scalar = supportScalar(loc, var.axes)
+ if not scalar:
+ continue
+ for i, c in enumerate(var.coordinates):
+ if c is not None:
+ deltas[i] = deltas.get(i, 0) + scalar * c
+ for i, delta in deltas.items():
+ cvt[i] += otRound(delta)
+
+ if "CFF2" in varfont:
+ log.info("Mutating CFF2 table")
+ glyphOrder = varfont.getGlyphOrder()
+ CFF2 = varfont["CFF2"]
+ topDict = CFF2.cff.topDictIndex[0]
+ vsInstancer = VarStoreInstancer(topDict.VarStore.otVarStore, fvar.axes, loc)
+ interpolateFromDeltas = vsInstancer.interpolateFromDeltas
+ interpolate_cff2_PrivateDict(topDict, interpolateFromDeltas)
+ CFF2.desubroutinize()
+ interpolate_cff2_charstrings(topDict, interpolateFromDeltas, glyphOrder)
+ interpolate_cff2_metrics(varfont, topDict, glyphOrder, loc)
+ del topDict.rawDict["VarStore"]
+ del topDict.VarStore
+
+ if "MVAR" in varfont:
+ log.info("Mutating MVAR table")
+ mvar = varfont["MVAR"].table
+ varStoreInstancer = VarStoreInstancer(mvar.VarStore, fvar.axes, loc)
+ records = mvar.ValueRecord
+ for rec in records:
+ mvarTag = rec.ValueTag
+ if mvarTag not in MVAR_ENTRIES:
+ continue
+ tableTag, itemName = MVAR_ENTRIES[mvarTag]
+ delta = otRound(varStoreInstancer[rec.VarIdx])
+ if not delta:
+ continue
+ setattr(
+ varfont[tableTag],
+ itemName,
+ getattr(varfont[tableTag], itemName) + delta,
+ )
+
+ log.info("Mutating FeatureVariations")
+ for tableTag in "GSUB", "GPOS":
+ if not tableTag in varfont:
+ continue
+ table = varfont[tableTag].table
+ if not getattr(table, "FeatureVariations", None):
+ continue
+ variations = table.FeatureVariations
+ for record in variations.FeatureVariationRecord:
+ applies = True
+ for condition in record.ConditionSet.ConditionTable:
+ if condition.Format == 1:
+ axisIdx = condition.AxisIndex
+ axisTag = fvar.axes[axisIdx].axisTag
+ Min = condition.FilterRangeMinValue
+ Max = condition.FilterRangeMaxValue
+ v = loc[axisTag]
+ if not (Min <= v <= Max):
+ applies = False
+ else:
+ applies = False
+ if not applies:
+ break
+
+ if applies:
+ assert record.FeatureTableSubstitution.Version == 0x00010000
+ for rec in record.FeatureTableSubstitution.SubstitutionRecord:
+ table.FeatureList.FeatureRecord[
+ rec.FeatureIndex
+ ].Feature = rec.Feature
+ break
+ del table.FeatureVariations
+
+ if "GDEF" in varfont and varfont["GDEF"].table.Version >= 0x00010003:
+ log.info("Mutating GDEF/GPOS/GSUB tables")
+ gdef = varfont["GDEF"].table
+ instancer = VarStoreInstancer(gdef.VarStore, fvar.axes, loc)
+
+ merger = MutatorMerger(varfont, instancer)
+ merger.mergeTables(varfont, [varfont], ["GDEF", "GPOS"])
+
+ # Downgrade GDEF.
+ del gdef.VarStore
+ gdef.Version = 0x00010002
+ if gdef.MarkGlyphSetsDef is None:
+ del gdef.MarkGlyphSetsDef
+ gdef.Version = 0x00010000
+
+ if not (
+ gdef.LigCaretList
+ or gdef.MarkAttachClassDef
+ or gdef.GlyphClassDef
+ or gdef.AttachList
+ or (gdef.Version >= 0x00010002 and gdef.MarkGlyphSetsDef)
+ ):
+ del varfont["GDEF"]
+
+ addidef = False
+ if glyf:
+ for glyph in glyf.glyphs.values():
+ if hasattr(glyph, "program"):
+ instructions = glyph.program.getAssembly()
+ # If GETVARIATION opcode is used in bytecode of any glyph add IDEF
+ addidef = any(op.startswith("GETVARIATION") for op in instructions)
+ if addidef:
+ break
+ if overlap:
+ for glyph_name in glyf.keys():
+ glyph = glyf[glyph_name]
+ # Set OVERLAP_COMPOUND bit for compound glyphs
+ if glyph.isComposite():
+ glyph.components[0].flags |= OVERLAP_COMPOUND
+ # Set OVERLAP_SIMPLE bit for simple glyphs
+ elif glyph.numberOfContours > 0:
+ glyph.flags[0] |= flagOverlapSimple
+ if addidef:
+ log.info("Adding IDEF to fpgm table for GETVARIATION opcode")
+ asm = []
+ if "fpgm" in varfont:
+ fpgm = varfont["fpgm"]
+ asm = fpgm.program.getAssembly()
+ else:
+ fpgm = newTable("fpgm")
+ fpgm.program = ttProgram.Program()
+ varfont["fpgm"] = fpgm
+ asm.append("PUSHB[000] 145")
+ asm.append("IDEF[ ]")
+ args = [str(len(loc))]
+ for a in fvar.axes:
+ args.append(str(floatToFixed(loc[a.axisTag], 14)))
+ asm.append("NPUSHW[ ] " + " ".join(args))
+ asm.append("ENDF[ ]")
+ fpgm.program.fromAssembly(asm)
+
+ # Change maxp attributes as IDEF is added
+ if "maxp" in varfont:
+ maxp = varfont["maxp"]
+ setattr(
+ maxp, "maxInstructionDefs", 1 + getattr(maxp, "maxInstructionDefs", 0)
+ )
+ setattr(
+ maxp,
+ "maxStackElements",
+ max(len(loc), getattr(maxp, "maxStackElements", 0)),
+ )
+
+ if "name" in varfont:
+ log.info("Pruning name table")
+ exclude = {a.axisNameID for a in fvar.axes}
+ for i in fvar.instances:
+ exclude.add(i.subfamilyNameID)
+ exclude.add(i.postscriptNameID)
+ if "ltag" in varfont:
+ # Drop the whole 'ltag' table if all its language tags are referenced by
+ # name records to be pruned.
+ # TODO: prune unused ltag tags and re-enumerate langIDs accordingly
+ excludedUnicodeLangIDs = [
+ n.langID
+ for n in varfont["name"].names
+ if n.nameID in exclude and n.platformID == 0 and n.langID != 0xFFFF
+ ]
+ if set(excludedUnicodeLangIDs) == set(range(len((varfont["ltag"].tags)))):
+ del varfont["ltag"]
+ varfont["name"].names[:] = [
+ n for n in varfont["name"].names if n.nameID not in exclude
+ ]
+
+ if "wght" in location and "OS/2" in varfont:
+ varfont["OS/2"].usWeightClass = otRound(max(1, min(location["wght"], 1000)))
+ if "wdth" in location:
+ wdth = location["wdth"]
+ for percent, widthClass in sorted(OS2_WIDTH_CLASS_VALUES.items()):
+ if wdth < percent:
+ varfont["OS/2"].usWidthClass = widthClass
+ break
+ else:
+ varfont["OS/2"].usWidthClass = 9
+ if "slnt" in location and "post" in varfont:
+ varfont["post"].italicAngle = max(-90, min(location["slnt"], 90))
+
+ log.info("Removing variable tables")
+ for tag in ("avar", "cvar", "fvar", "gvar", "HVAR", "MVAR", "VVAR", "STAT"):
+ if tag in varfont:
+ del varfont[tag]
+
+ return varfont
def main(args=None):
- """Instantiate a variation font"""
- from fontTools import configLogger
- import argparse
-
- parser = argparse.ArgumentParser(
- "fonttools varLib.mutator", description="Instantiate a variable font")
- parser.add_argument(
- "input", metavar="INPUT.ttf", help="Input variable TTF file.")
- parser.add_argument(
- "locargs", metavar="AXIS=LOC", nargs="*",
- help="List of space separated locations. A location consist in "
- "the name of a variation axis, followed by '=' and a number. E.g.: "
- " wght=700 wdth=80. The default is the location of the base master.")
- parser.add_argument(
- "-o", "--output", metavar="OUTPUT.ttf", default=None,
- help="Output instance TTF file (default: INPUT-instance.ttf).")
- parser.add_argument(
- "--no-recalc-timestamp", dest="recalc_timestamp", action='store_false',
- help="Don't set the output font's timestamp to the current time.")
- logging_group = parser.add_mutually_exclusive_group(required=False)
- logging_group.add_argument(
- "-v", "--verbose", action="store_true", help="Run more verbosely.")
- logging_group.add_argument(
- "-q", "--quiet", action="store_true", help="Turn verbosity off.")
- parser.add_argument(
- "--no-overlap",
- dest="overlap",
- action="store_false",
- help="Don't set OVERLAP_SIMPLE/OVERLAP_COMPOUND glyf flags."
- )
- options = parser.parse_args(args)
-
- varfilename = options.input
- outfile = (
- os.path.splitext(varfilename)[0] + '-instance.ttf'
- if not options.output else options.output)
- configLogger(level=(
- "DEBUG" if options.verbose else
- "ERROR" if options.quiet else
- "INFO"))
-
- loc = {}
- for arg in options.locargs:
- try:
- tag, val = arg.split('=')
- assert len(tag) <= 4
- loc[tag.ljust(4)] = float(val)
- except (ValueError, AssertionError):
- parser.error("invalid location argument format: %r" % arg)
- log.info("Location: %s", loc)
-
- log.info("Loading variable font")
- varfont = TTFont(varfilename, recalcTimestamp=options.recalc_timestamp)
-
- instantiateVariableFont(varfont, loc, inplace=True, overlap=options.overlap)
-
- log.info("Saving instance font %s", outfile)
- varfont.save(outfile)
+ """Instantiate a variation font"""
+ from fontTools import configLogger
+ import argparse
+
+ parser = argparse.ArgumentParser(
+ "fonttools varLib.mutator", description="Instantiate a variable font"
+ )
+ parser.add_argument("input", metavar="INPUT.ttf", help="Input variable TTF file.")
+ parser.add_argument(
+ "locargs",
+ metavar="AXIS=LOC",
+ nargs="*",
+ help="List of space separated locations. A location consist in "
+ "the name of a variation axis, followed by '=' and a number. E.g.: "
+ " wght=700 wdth=80. The default is the location of the base master.",
+ )
+ parser.add_argument(
+ "-o",
+ "--output",
+ metavar="OUTPUT.ttf",
+ default=None,
+ help="Output instance TTF file (default: INPUT-instance.ttf).",
+ )
+ parser.add_argument(
+ "--no-recalc-timestamp",
+ dest="recalc_timestamp",
+ action="store_false",
+ help="Don't set the output font's timestamp to the current time.",
+ )
+ logging_group = parser.add_mutually_exclusive_group(required=False)
+ logging_group.add_argument(
+ "-v", "--verbose", action="store_true", help="Run more verbosely."
+ )
+ logging_group.add_argument(
+ "-q", "--quiet", action="store_true", help="Turn verbosity off."
+ )
+ parser.add_argument(
+ "--no-overlap",
+ dest="overlap",
+ action="store_false",
+ help="Don't set OVERLAP_SIMPLE/OVERLAP_COMPOUND glyf flags.",
+ )
+ options = parser.parse_args(args)
+
+ varfilename = options.input
+ outfile = (
+ os.path.splitext(varfilename)[0] + "-instance.ttf"
+ if not options.output
+ else options.output
+ )
+ configLogger(
+ level=("DEBUG" if options.verbose else "ERROR" if options.quiet else "INFO")
+ )
+
+ loc = {}
+ for arg in options.locargs:
+ try:
+ tag, val = arg.split("=")
+ assert len(tag) <= 4
+ loc[tag.ljust(4)] = float(val)
+ except (ValueError, AssertionError):
+ parser.error("invalid location argument format: %r" % arg)
+ log.info("Location: %s", loc)
+
+ log.info("Loading variable font")
+ varfont = TTFont(varfilename, recalcTimestamp=options.recalc_timestamp)
+
+ instantiateVariableFont(varfont, loc, inplace=True, overlap=options.overlap)
+
+ log.info("Saving instance font %s", outfile)
+ varfont.save(outfile)
if __name__ == "__main__":
- import sys
- if len(sys.argv) > 1:
- sys.exit(main())
- import doctest
- sys.exit(doctest.testmod().failed)
+ import sys
+
+ if len(sys.argv) > 1:
+ sys.exit(main())
+ import doctest
+
+ sys.exit(doctest.testmod().failed)