2016-04-27 01:30:19 -07:00
|
|
|
"""
|
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
|
2016-04-27 01:30:19 -07:00
|
|
|
"""
|
|
|
|
from __future__ import print_function, division, absolute_import
|
|
|
|
from fontTools.misc.py23 import *
|
2018-06-14 17:40:11 +01:00
|
|
|
from fontTools.misc.fixedTools import floatToFixedToFloat, otRound
|
2016-04-27 01:30:19 -07:00
|
|
|
from fontTools.ttLib import TTFont
|
|
|
|
from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates
|
2017-10-19 13:59:43 -07:00
|
|
|
from fontTools.varLib import _GetCoordinates, _SetCoordinates, _DesignspaceAxis
|
2017-08-09 21:43:44 -07:00
|
|
|
from fontTools.varLib.models import supportScalar, normalizeLocation
|
2017-10-20 16:09:40 -04:00
|
|
|
from fontTools.varLib.merger import MutatorMerger
|
2017-10-20 13:50:08 -04:00
|
|
|
from fontTools.varLib.varStore import VarStoreInstancer
|
2017-10-22 12:19:24 +01:00
|
|
|
from fontTools.varLib.mvar import MVAR_ENTRIES
|
2017-10-15 18:16:01 -04:00
|
|
|
from fontTools.varLib.iup import iup_delta
|
2016-04-27 01:30:19 -07:00
|
|
|
import os.path
|
2017-10-15 17:43:06 +02:00
|
|
|
import logging
|
|
|
|
|
|
|
|
|
|
|
|
log = logging.getLogger("fontTools.varlib.mutator")
|
2016-04-27 01:30:19 -07:00
|
|
|
|
2017-05-20 22:13:51 -07:00
|
|
|
|
2018-06-18 19:40:30 +01:00
|
|
|
OS2_WIDTH_CLASS_VALUES = {
|
|
|
|
50.0: 1, # Ultra-condensed
|
|
|
|
62.5: 2, # Extra-condensed
|
|
|
|
75.0: 3, # Condensed
|
|
|
|
87.5: 4, # Semi-condensed
|
|
|
|
100.0: 5, # Medium (normal)
|
|
|
|
112.5: 6, # Semi-expanded
|
|
|
|
125.0: 7, # Expanded
|
|
|
|
150.0: 8, # Extra-expanded
|
|
|
|
200.0: 9, # Ultra-expanded
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-10-15 17:43:06 +02:00
|
|
|
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.:
|
2016-04-27 01:30:19 -07:00
|
|
|
|
2017-10-15 17:43:06 +02:00
|
|
|
{'wght': 400, 'wdth': 100}
|
2016-04-27 01:30:19 -07:00
|
|
|
|
2017-10-15 17:43:06 +02:00
|
|
|
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)
|
2016-04-27 01:30:19 -07:00
|
|
|
|
|
|
|
fvar = varfont['fvar']
|
2016-08-15 16:13:58 -07:00
|
|
|
axes = {a.axisTag:(a.minValue,a.defaultValue,a.maxValue) for a in fvar.axes}
|
2017-10-15 17:43:06 +02:00
|
|
|
loc = normalizeLocation(location, axes)
|
2017-10-19 13:59:43 -07:00
|
|
|
if 'avar' in varfont:
|
|
|
|
maps = varfont['avar'].segments
|
|
|
|
loc = {k:_DesignspaceAxis._map(v, maps[k]) for k,v in loc.items()}
|
2017-10-19 15:33:19 -07:00
|
|
|
# Quantize to F2Dot14, to avoid surprise interpolations.
|
|
|
|
loc = {k:floatToFixedToFloat(v, 14) for k,v in loc.items()}
|
2016-04-27 01:30:19 -07:00
|
|
|
# Location is normalized now
|
2017-10-15 17:43:06 +02:00
|
|
|
log.info("Normalized location: %s", loc)
|
2016-04-27 01:30:19 -07:00
|
|
|
|
2017-10-20 16:09:40 -04:00
|
|
|
log.info("Mutating glyf/gvar tables")
|
2016-04-27 01:30:19 -07:00
|
|
|
gvar = varfont['gvar']
|
2017-05-04 12:08:04 +02:00
|
|
|
glyf = varfont['glyf']
|
|
|
|
# get list of glyph names in gvar sorted by component depth
|
|
|
|
glyphnames = sorted(
|
|
|
|
gvar.variations.keys(),
|
|
|
|
key=lambda name: (
|
2017-05-04 12:28:02 +02:00
|
|
|
glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth
|
2017-05-04 12:08:04 +02:00
|
|
|
if glyf[name].isComposite() else 0,
|
|
|
|
name))
|
2017-05-03 19:13:49 +02:00
|
|
|
for glyphname in glyphnames:
|
|
|
|
variations = gvar.variations[glyphname]
|
2016-04-27 01:30:19 -07:00
|
|
|
coordinates,_ = _GetCoordinates(varfont, glyphname)
|
2017-05-20 21:08:11 -07:00
|
|
|
origCoords, endPts = None, None
|
2016-04-27 01:30:19 -07:00
|
|
|
for var in variations:
|
2017-10-19 10:06:20 -07:00
|
|
|
scalar = supportScalar(loc, var.axes)
|
2016-04-27 01:30:19 -07:00
|
|
|
if not scalar: continue
|
2017-05-20 21:08:11 -07:00
|
|
|
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])))
|
2017-10-15 18:16:01 -04:00
|
|
|
delta = iup_delta(delta, origCoords, endPts)
|
2017-05-20 21:08:11 -07:00
|
|
|
coordinates += GlyphCoordinates(delta) * scalar
|
2016-04-27 01:30:19 -07:00
|
|
|
_SetCoordinates(varfont, glyphname, coordinates)
|
|
|
|
|
2017-10-05 13:32:06 +02:00
|
|
|
if 'cvar' in varfont:
|
2017-10-20 16:09:40 -04:00
|
|
|
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():
|
2018-06-14 17:40:11 +01:00
|
|
|
cvt[i] += otRound(delta)
|
2017-10-05 13:32:06 +02:00
|
|
|
|
2017-10-19 11:12:03 -07:00
|
|
|
if 'MVAR' in varfont:
|
2017-10-20 16:09:40 -04:00
|
|
|
log.info("Mutating MVAR table")
|
2017-10-19 11:12:03 -07:00
|
|
|
mvar = varfont['MVAR'].table
|
2017-10-20 13:50:08 -04:00
|
|
|
varStoreInstancer = VarStoreInstancer(mvar.VarStore, fvar.axes, loc)
|
2017-10-19 11:12:03 -07:00
|
|
|
records = mvar.ValueRecord
|
|
|
|
for rec in records:
|
|
|
|
mvarTag = rec.ValueTag
|
2017-10-22 12:19:24 +01:00
|
|
|
if mvarTag not in MVAR_ENTRIES:
|
2017-10-19 11:12:03 -07:00
|
|
|
continue
|
2017-10-22 12:19:24 +01:00
|
|
|
tableTag, itemName = MVAR_ENTRIES[mvarTag]
|
2018-06-14 17:40:11 +01:00
|
|
|
delta = otRound(varStoreInstancer[rec.VarIdx])
|
2017-10-19 11:12:03 -07:00
|
|
|
if not delta:
|
|
|
|
continue
|
2017-10-20 13:50:08 -04:00
|
|
|
setattr(varfont[tableTag], itemName,
|
|
|
|
getattr(varfont[tableTag], itemName) + delta)
|
2017-10-19 11:12:03 -07:00
|
|
|
|
2017-10-20 16:09:40 -04:00
|
|
|
if 'GDEF' in varfont:
|
|
|
|
log.info("Mutating GDEF/GPOS/GSUB tables")
|
|
|
|
merger = MutatorMerger(varfont, loc)
|
|
|
|
|
|
|
|
log.info("Building interpolated tables")
|
|
|
|
merger.instantiate()
|
|
|
|
|
2018-04-18 12:35:54 +01: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
|
|
|
|
]
|
|
|
|
|
2018-06-18 19:40:30 +01:00
|
|
|
if "wght" in location and "OS/2" in varfont:
|
2018-06-18 19:44:40 +01:00
|
|
|
varfont["OS/2"].usWeightClass = otRound(
|
|
|
|
max(1, min(location["wght"], 1000))
|
|
|
|
)
|
2018-06-18 19:40:30 +01:00
|
|
|
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))
|
|
|
|
|
2017-10-15 17:43:06 +02:00
|
|
|
log.info("Removing variable tables")
|
2017-03-04 23:30:37 -08:00
|
|
|
for tag in ('avar','cvar','fvar','gvar','HVAR','MVAR','VVAR','STAT'):
|
2016-04-27 01:30:19 -07:00
|
|
|
if tag in varfont:
|
|
|
|
del varfont[tag]
|
|
|
|
|
2017-10-15 17:43:06 +02:00
|
|
|
return varfont
|
|
|
|
|
|
|
|
|
|
|
|
def main(args=None):
|
|
|
|
from fontTools import configLogger
|
2018-03-27 14:06:37 +01:00
|
|
|
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).")
|
|
|
|
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.")
|
|
|
|
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"))
|
2017-10-15 17:43:06 +02:00
|
|
|
|
|
|
|
loc = {}
|
2018-03-27 14:06:37 +01:00
|
|
|
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)
|
2017-10-15 17:43:06 +02:00
|
|
|
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)
|
2016-04-27 01:30:19 -07:00
|
|
|
varfont.save(outfile)
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
import sys
|
|
|
|
if len(sys.argv) > 1:
|
2017-01-11 12:10:58 +00:00
|
|
|
sys.exit(main())
|
2017-01-11 12:24:04 +00:00
|
|
|
import doctest
|
2016-04-27 01:30:19 -07:00
|
|
|
sys.exit(doctest.testmod().failed)
|