328 lines
10 KiB
Python
Raw Normal View History

"""
2016-04-27 01:41:48 -07:00
Instantiate a variation font. Run, eg:
2018-02-21 14:54:27 -08:00
$ fonttools varLib.mutator ./NotoSansArabic-VF.ttf wght=140 wdth=85
"""
from __future__ import print_function, division, absolute_import
from fontTools.misc.py23 import *
2018-11-15 15:18:03 -05:00
from fontTools.misc.fixedTools import floatToFixedToFloat, otRound, floatToFixed
from fontTools.ttLib import TTFont, newTable
from fontTools.ttLib.tables import ttProgram
from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates
from fontTools.varLib import _GetCoordinates, _SetCoordinates
from fontTools.varLib.models import (
2018-11-29 14:08:53 -05:00
supportScalar,
normalizeLocation,
piecewiseLinearMap,
)
from fontTools.varLib.merger import MutatorMerger
from fontTools.varLib.varStore import VarStoreInstancer
from fontTools.varLib.mvar import MVAR_ENTRIES
from fontTools.varLib.iup import iup_delta
from fontTools.subset.cff import (
2018-11-29 14:08:53 -05:00
interpolate_cff2_PrivateDict,
interpolate_cff2_charstrings,
interpolate_cff2_metrics,
)
import os.path
import logging
log = logging.getLogger("fontTools.varlib.mutator")
# map 'wdth' axis (1..200) to OS/2.usWidthClass (1..9), rounding to closest
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
def instantiateVariableFont(varfont, location, inplace=False):
""" 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.
"""
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)
2017-10-19 13:59:43 -07:00
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']
# 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,_ = _GetCoordinates(varfont, glyphname)
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,control = _GetCoordinates(varfont, glyphname)
endPts = control[1] if control[0] >= 1 else list(range(len(control[1])))
delta = iup_delta(delta, origCoords, endPts)
coordinates += GlyphCoordinates(delta) * scalar
_SetCoordinates(varfont, glyphname, coordinates)
else:
glyf = None
2017-10-05 13:32:06 +02:00
if 'cvar' in varfont:
log.info("Mutating cvt/cvar tables")
2017-10-05 13:32:06 +02:00
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)
2017-10-05 13:32:06 +02:00
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(varfont)
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,
2018-11-16 18:11:25 -05:00
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 hasattr(table, 'FeatureVariations'):
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, loc)
2018-11-19 16:42:53 -05:00
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']
2018-11-16 14:12:26 -05:00
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
2018-11-16 14:12:26 -05:00
if addidef:
log.info("Adding IDEF to fpgm table for GETVARIATION opcode")
asm = []
2018-11-16 17:49:17 -05:00
if 'fpgm' in varfont:
2018-11-16 14:12:26 -05:00
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)))
2018-11-16 14:12:26 -05:00
asm.append("NPUSHW[ ] " + ' '.join(args))
asm.append("ENDF[ ]")
fpgm.program.fromAssembly(asm)
# Change maxp attributes as IDEF is added
2018-11-16 17:49:17 -05:00
if 'maxp' in varfont:
2018-11-16 14:12:26 -05:00
maxp = varfont['maxp']
if hasattr(maxp, "maxInstructionDefs"):
maxp.maxInstructionDefs += 1
else:
setattr(maxp, "maxInstructionDefs", 1)
if hasattr(maxp, "maxStackElements"):
maxp.maxStackElements = max(len(loc), maxp.maxStackElements)
2018-11-16 14:12:26 -05:00
else:
setattr(maxp, "maxInstructionDefs", len(loc))
2018-11-15 15:18:03 -05:00
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)
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(
2018-11-16 18:11:25 -05:00
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):
from fontTools import configLogger
import argparse
parser = argparse.ArgumentParser(
2018-11-16 18:11:25 -05:00
"fonttools varLib.mutator", description="Instantiate a variable font")
parser.add_argument(
2018-11-16 18:11:25 -05:00
"input", metavar="INPUT.ttf", help="Input variable TTF file.")
parser.add_argument(
2018-11-16 18:11:25 -05:00
"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(
2018-11-16 18:11:25 -05:00
"-o", "--output", metavar="OUTPUT.ttf", default=None,
help="Output instance TTF file (default: INPUT-instance.ttf).")
logging_group = parser.add_mutually_exclusive_group(required=False)
logging_group.add_argument(
2018-11-16 18:11:25 -05:00
"-v", "--verbose", action="store_true", help="Run more verbosely.")
logging_group.add_argument(
2018-11-16 18:11:25 -05:00
"-q", "--quiet", action="store_true", help="Turn verbosity off.")
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)
instantiateVariableFont(varfont, loc, inplace=True)
log.info("Saving instance font %s", outfile)
varfont.save(outfile)
if __name__ == "__main__":
import sys
if len(sys.argv) > 1:
sys.exit(main())
2017-01-11 12:24:04 +00:00
import doctest
sys.exit(doctest.testmod().failed)