393 lines
12 KiB
Python
Raw Normal View History

2016-04-15 13:56:37 -07:00
"""
Module for dealing with 'gvar'-style font variations, also known as run-time
interpolation.
2016-04-15 13:56:37 -07:00
The ideas here are very similar to MutatorMath. There is even code to read
MutatorMath .designspace files in the varLib.designspace module.
2016-04-15 13:56:37 -07:00
For now, if you run this file on a designspace file, it tries to find
ttf-interpolatable files for the masters and build a GX variation font from
them. Such ttf-interpolatable and designspace files can be generated from
a Glyphs source, eg., using noto-source as an example:
$ fontmake -o ttf-interpolatable -g NotoSansArabic-MM.glyphs
Then you can make a GX font this way:
$ python fonttools/Lib/fontTools/varLib/__init__.py master_ufo/NotoSansArabic.designspace
API *will* change in near future.
"""
from __future__ import print_function, division, absolute_import
from fontTools.misc.py23 import *
2016-07-01 15:31:00 -07:00
from fontTools.ttLib import TTFont, newTable
from fontTools.ttLib.tables._n_a_m_e import NameRecord
2016-07-01 15:31:00 -07:00
from fontTools.ttLib.tables._f_v_a_r import Axis, NamedInstance
2016-04-27 00:21:46 -07:00
from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates
2016-07-01 15:31:00 -07:00
from fontTools.ttLib.tables._g_v_a_r import GlyphVariation
from fontTools.ttLib.tables import otTables as ot
from fontTools.varLib import designspace, models, builder
2016-04-18 16:48:02 -07:00
import warnings
import os.path
#
# Creation routines
#
# TODO: Move to name table proper; also, is mac_roman ok for ASCII names?
def _AddName(font, name):
"""(font, "Bold") --> NameRecord"""
nameTable = font.get("name")
namerec = NameRecord()
namerec.nameID = 1 + max([n.nameID for n in nameTable.names] + [256])
namerec.string = name
namerec.platformID, namerec.platEncID, namerec.langID = (1, 0, 0)
nameTable.names.append(namerec)
return namerec
# Move to fvar table proper?
def _add_fvar(font, axes, axis_names, instances):
assert "fvar" not in font
2016-07-01 15:31:00 -07:00
font['fvar'] = fvar = newTable('fvar')
for tag in sorted(axes.keys()):
axis = Axis()
2016-08-15 16:22:22 -07:00
axis.axisTag = Tag(tag)
axis.minValue, axis.defaultValue, axis.maxValue = axes[tag]
axis.nameID = _AddName(font, axis_names[tag]).nameID
fvar.axes.append(axis)
for name, coordinates in instances:
inst = NamedInstance()
inst.nameID = _AddName(font, name).nameID
inst.coordinates = coordinates
fvar.instances.append(inst)
2016-04-14 23:55:11 -07:00
# TODO Move to glyf or gvar table proper
def _GetCoordinates(font, glyphName):
"""font, glyphName --> glyph coordinates as expected by "gvar" table
The result includes four "phantom points" for the glyph metrics,
as mandated by the "gvar" spec.
"""
2016-04-14 23:55:11 -07:00
glyf = font["glyf"]
if glyphName not in glyf.glyphs: return None
glyph = glyf[glyphName]
if glyph.isComposite():
coord = GlyphCoordinates([(getattr(c, 'x', 0),getattr(c, 'y', 0)) for c in glyph.components])
2016-04-14 23:55:11 -07:00
control = [c.glyphName for c in glyph.components]
else:
2016-04-14 23:55:11 -07:00
allData = glyph.getCoordinates(glyf)
coord = allData[0]
control = allData[1:]
# Add phantom points for (left, right, top, bottom) positions.
horizontalAdvanceWidth, leftSideBearing = font["hmtx"].metrics[glyphName]
if not hasattr(glyph, 'xMin'):
2016-04-14 23:55:11 -07:00
glyph.recalcBounds(glyf)
leftSideX = glyph.xMin - leftSideBearing
rightSideX = leftSideX + horizontalAdvanceWidth
# XXX these are incorrect. Load vmtx and fix.
topSideY = glyph.yMax
bottomSideY = -glyph.yMin
coord = coord.copy()
coord.extend([(leftSideX, 0),
(rightSideX, 0),
(0, topSideY),
(0, bottomSideY)])
2016-04-14 23:55:11 -07:00
return coord, control
2016-04-27 01:17:09 -07:00
# TODO Move to glyf or gvar table proper
def _SetCoordinates(font, glyphName, coord):
glyf = font["glyf"]
assert glyphName in glyf.glyphs
glyph = glyf[glyphName]
# Handle phantom points for (left, right, top, bottom) positions.
assert len(coord) >= 4
if not hasattr(glyph, 'xMin'):
glyph.recalcBounds(glyf)
leftSideX = coord[-4][0]
rightSideX = coord[-3][0]
topSideY = coord[-2][1]
bottomSideY = coord[-1][1]
for _ in range(4):
del coord[-1]
if glyph.isComposite():
assert len(coord) == len(glyph.components)
for p,comp in zip(coord, glyph.components):
if hasattr(comp, 'x'):
comp.x,comp.y = p
2016-04-27 01:17:09 -07:00
elif glyph.numberOfContours is 0:
assert len(coord) == 0
else:
assert len(coord) == len(glyph.coordinates)
glyph.coordinates = coord
glyph.recalcBounds(glyf)
horizontalAdvanceWidth = rightSideX - leftSideX
leftSideBearing = glyph.xMin - leftSideX
# XXX Handle vertical
# XXX Remove the round when https://github.com/behdad/fonttools/issues/593 is fixed
font["hmtx"].metrics[glyphName] = int(round(horizontalAdvanceWidth)), int(round(leftSideBearing))
2016-08-09 20:53:19 -07:00
def _build_model(axes, master_locs, base_idx):
master_locs = [models.normalizeLocation(m, axes) for m in master_locs]
print("Normalized master positions:")
print(master_locs)
2016-08-09 20:53:19 -07:00
# Assume single-model for now.
model = models.VariationModel(master_locs)
2016-08-09 20:53:19 -07:00
model_base_idx = model.mapping[base_idx]
assert 0 == model_base_idx
return model
def _add_gvar(font, model, master_ttfs):
2016-04-14 23:55:11 -07:00
print("Generating gvar")
assert "gvar" not in font
2016-07-01 15:31:00 -07:00
gvar = font["gvar"] = newTable('gvar')
gvar.version = 1
gvar.reserved = 0
gvar.variations = {}
2016-04-14 23:55:11 -07:00
for glyph in font.getGlyphOrder():
2016-04-14 23:55:11 -07:00
allData = [_GetCoordinates(m, glyph) for m in master_ttfs]
allCoords = [d[0] for d in allData]
allControls = [d[1] for d in allData]
control = allControls[0]
if (any(c != control for c in allControls)):
warnings.warn("glyph %s has incompatible masters; skipping" % glyph)
continue
2016-04-14 23:55:11 -07:00
del allControls
2016-07-01 15:31:00 -07:00
# Update gvar
gvar.variations[glyph] = []
deltas = model.getDeltas(allCoords)
supports = model.supports
assert len(deltas) == len(supports)
2016-07-01 15:31:00 -07:00
for i,(delta,support) in enumerate(zip(deltas[1:], supports[1:])):
var = GlyphVariation(support, delta)
gvar.variations[glyph].append(var)
2016-08-10 01:48:09 -07:00
def _add_HVAR(font, model, master_ttfs, axes):
2016-07-01 15:31:00 -07:00
2016-08-09 20:53:19 -07:00
print("Generating HVAR")
hAdvanceDeltas = {}
metricses = [m["hmtx"].metrics for m in master_ttfs]
for glyph in font.getGlyphOrder():
hAdvances = [metrics[glyph][0] for metrics in metricses]
# TODO move round somewhere else?
hAdvanceDeltas[glyph] = tuple(round(d) for d in model.getDeltas(hAdvances)[1:])
2016-07-01 15:31:00 -07:00
# We only support the direct mapping right now.
supports = model.supports[1:]
2016-08-10 01:48:09 -07:00
varTupleList = builder.buildVarRegionList(supports, axes.keys())
2016-07-01 15:31:00 -07:00
varTupleIndexes = list(range(len(supports)))
n = len(supports)
items = []
zeroes = [0]*n
for glyphName in font.getGlyphOrder():
items.append(hAdvanceDeltas.get(glyphName, zeroes))
while items and items[-1] is zeroes:
del items[-1]
advanceMapping = None
# Add indirect mapping to save on duplicates
uniq = set(items)
# TODO Improve heuristic
if (len(items) - len(uniq)) * len(varTupleIndexes) > len(items):
newItems = sorted(uniq)
mapper = {v:i for i,v in enumerate(newItems)}
mapping = [mapper[item] for item in items]
while len(mapping) > 1 and mapping[-1] == mapping[-2]:
del mapping[-1]
advanceMapping = builder.buildVarIdxMap(mapping)
items = newItems
del mapper, mapping, newItems
del uniq
varData = builder.buildVarData(varTupleIndexes, items)
varStore = builder.buildVarStore(varTupleList, [varData])
2016-07-01 15:31:00 -07:00
assert "HVAR" not in font
HVAR = font["HVAR"] = newTable('HVAR')
hvar = HVAR.table = ot.HVAR()
hvar.Version = 1.0
hvar.VarStore = varStore
hvar.AdvWidthMap = advanceMapping
hvar.LsbMap = hvar.RsbMap = None
2016-07-01 15:31:00 -07:00
def _all_equal(lst):
it = iter(lst)
v0 = next(it)
for v in it:
if v0 != v:
return False
return True
def buildVarDevTable(store, master_values):
if _all_equal(master_values):
return None
deltas = master_values
return builder.buildVarDevTable(0xdeadbeef)
def _merge_OTL(font, model, master_ttfs, axes, base_idx):
print("Merging OpenType Layout tables")
GDEFs = [m['GDEF'].table for m in master_ttfs]
GPOSs = [m['GPOS'].table for m in master_ttfs]
GSUBs = [m['GSUB'].table for m in master_ttfs]
# Reuse the base font's tables
for tag in 'GDEF', 'GPOS', 'GSUB':
font[tag] = master_ttfs[base_idx][tag]
GPOS = font['GPOS'].table
getAnchor = lambda GPOS: GPOS.LookupList.Lookup[4].SubTable[0].MarkArray.MarkRecord[28].MarkAnchor
2016-08-15 11:14:52 -07:00
store_builder = builder.OnlineVarStoreBuilder(axes.keys())
store_builder.setModel(model)
anchors = [getAnchor(G) for G in GPOSs]
anchor = getAnchor(GPOS)
XDeviceTable = buildVarDevTable(store_builder, [a.XCoordinate for a in anchors])
YDeviceTable = buildVarDevTable(store_builder, [a.YCoordinate for a in anchors])
if XDeviceTable or YDeviceTable:
anchor.Format = 3
anchor.XDeviceTable = XDeviceTable
anchor.YDeviceTable = YDeviceTable
2016-08-15 11:14:52 -07:00
store = store_builder.finish()
# TODO insert in GDEF
2016-04-14 00:31:17 -07:00
def main(args=None):
import sys
if args is None:
args = sys.argv[1:]
(designspace_filename,) = args
finder = lambda s: s.replace('master_ufo', 'master_ttf_interpolatable').replace('.ufo', '.ttf')
axisMap = None # dict mapping axis id to (axis tag, axis name)
outfile = os.path.splitext(designspace_filename)[0] + '-GX.ttf'
masters, instances = designspace.load(designspace_filename)
base_idx = None
for i,m in enumerate(masters):
if 'info' in m and m['info']['copy']:
assert base_idx is None
base_idx = i
assert base_idx is not None, "Cannot find 'base' master; Add <info> element to one of the masters in the .designspace document."
2016-04-14 00:31:17 -07:00
from pprint import pprint
print("Masters:")
2016-04-14 00:31:17 -07:00
pprint(masters)
print("Instances:")
2016-04-14 00:31:17 -07:00
pprint(instances)
print("Index of base master:", base_idx)
print("Building GX")
print("Loading TTF masters")
basedir = os.path.dirname(designspace_filename)
master_ttfs = [finder(os.path.join(basedir, m['filename'])) for m in masters]
master_fonts = [TTFont(ttf_path) for ttf_path in master_ttfs]
standard_axis_map = {
'weight': ('wght', 'Weight'),
'width': ('wdth', 'Width'),
'slant': ('slnt', 'Slant'),
'optical': ('opsz', 'Optical Size'),
'custom': ('xxxx', 'Custom'),
}
axis_map = standard_axis_map
if axisMap:
axis_map = axis_map.copy()
axis_map.update(axisMap)
# TODO: For weight & width, use OS/2 values and setup 'avar' mapping.
# Set up master locations
master_locs = []
instance_locs = []
out = []
for loc in [m['location'] for m in masters+instances]:
# Apply modifications for default axes; and apply tags
l = {}
for axis,value in loc.items():
tag,name = axis_map[axis]
l[tag] = value
out.append(l)
master_locs = out[:len(masters)]
instance_locs = out[len(masters):]
axis_tags = set(master_locs[0].keys())
assert all(axis_tags == set(m.keys()) for m in master_locs)
print("Axis tags:", axis_tags)
print("Master positions:")
pprint(master_locs)
# Set up axes
axes = {}
axis_names = {}
for tag,name in axis_map.values():
if tag not in axis_tags: continue
axis_names[tag] = name
for tag in axis_tags:
default = master_locs[base_idx][tag]
lower = min(m[tag] for m in master_locs)
upper = max(m[tag] for m in master_locs)
axes[tag] = (lower, default, upper)
print("Axes:")
pprint(axes)
# Set up named instances
instance_list = []
for loc,instance in zip(instance_locs,instances):
style = instance['stylename']
instance_list.append((style, loc))
# TODO append masters as named-instances as well; needs .designspace change.
# We can use the base font straight, but it's faster to load it again since
# then we won't be recompiling the existing ('glyf', 'hmtx', ...) tables.
#gx = master_fonts[base_idx]
gx = TTFont(master_ttfs[base_idx])
_add_fvar(gx, axes, axis_names, instance_list)
2016-08-09 20:53:19 -07:00
model = _build_model(axes, master_locs, base_idx)
print("Building variations tables")
_add_gvar(gx, model, master_fonts)
2016-08-10 01:48:09 -07:00
_add_HVAR(gx, model, master_fonts, axes)
#_merge_OTL(gx, model, master_fonts, axes, base_idx)
print("Saving GX font", outfile)
gx.save(outfile)
2016-04-14 00:31:17 -07:00
2016-04-13 23:51:54 -07:00
if __name__ == "__main__":
2016-04-14 00:31:17 -07:00
import sys
if len(sys.argv) > 1:
main()
#sys.exit(0)
2016-04-13 23:51:54 -07:00
import doctest, sys
sys.exit(doctest.testmod().failed)