339 lines
10 KiB
Python
339 lines
10 KiB
Python
"""
|
|
Interpolate OpenType Layout tables (GDEF / GPOS / GSUB).
|
|
"""
|
|
from __future__ import print_function, division, absolute_import
|
|
from fontTools.misc.py23 import *
|
|
from fontTools.ttLib import TTFont
|
|
from fontTools.ttLib.tables import otTables as ot
|
|
from fontTools.ttLib.tables import otBase as otBase
|
|
from fontTools.ttLib.tables.DefaultTable import DefaultTable
|
|
from fontTools.varLib import designspace, models, builder
|
|
from fontTools.varLib.merger import merge_tables, Merger
|
|
from functools import reduce
|
|
import os.path
|
|
|
|
class InstancerMerger(Merger):
|
|
|
|
def __init__(self, font, model, location):
|
|
Merger.__init__(self, font)
|
|
self.model = model
|
|
self.location = location
|
|
|
|
@InstancerMerger.merger(ot.Anchor)
|
|
def merge(merger, self, lst):
|
|
XCoords = [a.XCoordinate for a in lst]
|
|
YCoords = [a.YCoordinate for a in lst]
|
|
model = merger.model
|
|
location = merger.location
|
|
self.XCoordinate = round(model.interpolateFromMasters(location, XCoords))
|
|
self.YCoordinate = round(model.interpolateFromMasters(location, YCoords))
|
|
|
|
@InstancerMerger.merger(otBase.ValueRecord)
|
|
def merge(merger, self, lst):
|
|
model = merger.model
|
|
location = merger.location
|
|
# TODO Handle differing valueformats
|
|
for name, tableName in [('XAdvance','XAdvDevice'),
|
|
('YAdvance','YAdvDevice'),
|
|
('XPlacement','XPlaDevice'),
|
|
('YPlacement','YPlaDevice')]:
|
|
|
|
assert not hasattr(self, tableName)
|
|
|
|
if hasattr(self, name):
|
|
values = [getattr(a, name, 0) for a in lst]
|
|
value = round(model.interpolateFromMasters(location, values))
|
|
setattr(self, name, value)
|
|
|
|
def _SinglePosUpgradeToFormat2(self):
|
|
if self.Format == 2: return self
|
|
|
|
ret = ot.SinglePos()
|
|
ret.Format = 2
|
|
ret.Coverage = self.Coverage
|
|
ret.ValueFormat = self.ValueFormat
|
|
ret.Value = [self.Value for g in ret.Coverage.glyphs]
|
|
ret.ValueCount = len(ret.Value)
|
|
|
|
return ret
|
|
|
|
def _merge_GlyphOrders(font, lst, values_lst=None, default=None):
|
|
"""Takes font and list of glyph lists (must be sorted by glyph id), and returns
|
|
two things:
|
|
- Combined glyph list,
|
|
- If values_lst is None, return input glyph lists, but padded with None when a glyph
|
|
was missing in a list. Otherwise, return values_lst list-of-list, padded with None
|
|
to match combined glyph lists.
|
|
"""
|
|
if values_lst is None:
|
|
dict_sets = [set(l) for l in lst]
|
|
else:
|
|
dict_sets = [{g:v for g,v in zip(l,vs)} for l,vs in zip(lst,values_lst)]
|
|
combined = set()
|
|
combined.update(*dict_sets)
|
|
|
|
sortKey = font.getReverseGlyphMap().__getitem__
|
|
order = sorted(combined, key=sortKey)
|
|
# Make sure all input glyphsets were in proper order
|
|
assert all(sorted(vs, key=sortKey) == vs for vs in lst)
|
|
del combined
|
|
|
|
paddedValues = None
|
|
if values_lst is None:
|
|
padded = [[glyph if glyph in dict_set else default
|
|
for glyph in order]
|
|
for dict_set in dict_sets]
|
|
else:
|
|
assert len(lst) == len(values_lst)
|
|
padded = [[dict_set[glyph] if glyph in dict_set else default
|
|
for glyph in order]
|
|
for dict_set in dict_sets]
|
|
return order, padded
|
|
|
|
def _Lookup_SinglePos_get_effective_value(self, glyph):
|
|
if self is None: return None
|
|
subtables = self.SubTable
|
|
for self in subtables:
|
|
if self is None or \
|
|
type(self) != ot.SinglePos or \
|
|
self.Coverage is None or \
|
|
glyph not in self.Coverage.glyphs:
|
|
continue
|
|
if self.Format == 1:
|
|
return self.Value
|
|
elif self.Format == 2:
|
|
return self.Value[self.Coverage.glyphs.index(glyph)]
|
|
else:
|
|
assert 0
|
|
return None
|
|
|
|
def _Lookup_PairPos_get_effective_value_pair(self, firstGlyph, secondGlyph):
|
|
if self is None: return None
|
|
subtables = self.SubTable
|
|
for self in subtables:
|
|
if self is None or \
|
|
type(self) != ot.PairPos or \
|
|
self.Coverage is None or \
|
|
firstGlyph not in self.Coverage.glyphs:
|
|
continue
|
|
if self.Format == 1:
|
|
ps = self.PairSet[self.Coverage.glyphs.index(firstGlyph)]
|
|
pvr = ps.PairValueRecord
|
|
for rec in pvr: # TODO Speed up
|
|
if rec.SecondGlyph == secondGlyph:
|
|
return rec
|
|
continue
|
|
elif self.Format == 2:
|
|
klass1 = self.ClassDef1.classDefs.get(firstGlyph, 0)
|
|
klass2 = self.ClassDef2.classDefs.get(secondGlyph, 0)
|
|
return self.Class1Record[klass1].Class2Record[klass2]
|
|
else:
|
|
assert 0
|
|
return None
|
|
|
|
@InstancerMerger.merger(ot.SinglePos)
|
|
def merge(merger, self, lst):
|
|
self.ValueFormat = valueFormat = reduce(int.__or__, [l.ValueFormat for l in lst])
|
|
assert valueFormat & ~0xF == 0, valueFormat
|
|
|
|
# If all have same coverage table and all are format 1,
|
|
if all(v.Format == 1 for v in lst) and all(self.Coverage.glyphs == v.Coverage.glyphs for v in lst):
|
|
self.Value = otBase.ValueRecord(valueFormat)
|
|
merger.mergeThings(self.Value, [v.Value for v in lst])
|
|
return
|
|
|
|
# Upgrade everything to Format=2
|
|
self.Format = 2
|
|
lst = [_SinglePosUpgradeToFormat2(v) for v in lst]
|
|
|
|
# Align them
|
|
glyphs, padded = _merge_GlyphOrders(merger.font,
|
|
[v.Coverage.glyphs for v in lst],
|
|
[v.Value for v in lst])
|
|
|
|
self.Coverage.glyphs = glyphs
|
|
self.Value = [otBase.ValueRecord(valueFormat) for g in glyphs]
|
|
self.ValueCount = len(self.Value)
|
|
|
|
for i,values in enumerate(padded):
|
|
for j,glyph in enumerate(glyphs):
|
|
if values[j] is not None: continue
|
|
# Fill in value from other subtables
|
|
v = _Lookup_SinglePos_get_effective_value(merger.lookups[i], glyph)
|
|
if v is None:
|
|
v = otBase.ValueRecord(valueFormat)
|
|
values[j] = v
|
|
|
|
merger.mergeLists(self.Value, padded)
|
|
|
|
# Merge everything else; though, there shouldn't be anything else. :)
|
|
merger.mergeObjects(self, lst,
|
|
exclude=('Format', 'Coverage', 'ValueRecord', 'Value', 'ValueCount'))
|
|
|
|
@InstancerMerger.merger(ot.PairSet)
|
|
def merge(merger, self, lst):
|
|
# Align them
|
|
glyphs, padded = _merge_GlyphOrders(merger.font,
|
|
[[v.SecondGlyph for v in vs.PairValueRecord] for vs in lst],
|
|
[vs.PairValueRecord for vs in lst])
|
|
|
|
self.PairValueRecord = pvrs = []
|
|
for glyph in glyphs:
|
|
pvr = ot.PairValueRecord()
|
|
pvr.SecondGlyph = glyph
|
|
pvr.Value1 = otBase.ValueRecord(merger.valueFormat1) if merger.valueFormat1 else None
|
|
pvr.Value2 = otBase.ValueRecord(merger.valueFormat2) if merger.valueFormat2 else None
|
|
pvrs.append(pvr)
|
|
self.PairValueCount = len(self.PairValueRecord)
|
|
|
|
for i,values in enumerate(padded):
|
|
for j,glyph in enumerate(glyphs):
|
|
if values[j] is not None: continue
|
|
# Fill in value from other subtables
|
|
v = ot.PairValueRecord()
|
|
v.SecondGlyph = glyph
|
|
vpair = _Lookup_PairPos_get_effective_value_pair(merger.lookups[i], self._firstGlyph, glyph)
|
|
if vpair is None:
|
|
v.Value1 = otBase.ValueRecord(merger.valueFormat1) if merger.valueFormat1 else None
|
|
v.Value2 = otBase.ValueRecord(merger.valueFormat2) if merger.valueFormat2 else None
|
|
else:
|
|
v.Value1, v.Value2 = vpair.Value1, vpair.Value2
|
|
values[j] = v
|
|
del self._firstGlyph
|
|
|
|
merger.mergeThings(self.PairValueRecord, padded)
|
|
|
|
@InstancerMerger.merger(ot.PairPos)
|
|
def merge(merger, self, lst):
|
|
# TODO Support differing ValueFormats.
|
|
merger.valueFormat1 = self.ValueFormat1
|
|
merger.valueFormat2 = self.ValueFormat2
|
|
|
|
if self.Format == 2:
|
|
# Everything must match; we don't support smart merge yet.
|
|
merger.mergeObjects(self, lst)
|
|
del merger.valueFormat1, merger.valueFormat2
|
|
return
|
|
|
|
assert self.Format == 1
|
|
# Merge everything else; makes sure Format is the same.
|
|
merger.mergeObjects(self, lst,
|
|
exclude=('Coverage',
|
|
'PairSet', 'PairSetCount'))
|
|
|
|
empty = ot.PairSet()
|
|
empty.PairValueRecord = []
|
|
empty.PairValueCount = 0
|
|
|
|
# Align them
|
|
glyphs, padded = _merge_GlyphOrders(merger.font,
|
|
[v.Coverage.glyphs for v in lst],
|
|
[v.PairSet for v in lst],
|
|
default=empty)
|
|
|
|
self.Coverage.glyphs = glyphs
|
|
self.PairSet = [ot.PairSet() for g in glyphs]
|
|
self.PairSetCount = len(self.PairSet)
|
|
for glyph, ps in zip(glyphs, self.PairSet):
|
|
ps._firstGlyph = glyph
|
|
|
|
merger.mergeThings(self.PairSet, padded)
|
|
|
|
del merger.valueFormat1, merger.valueFormat2
|
|
|
|
@InstancerMerger.merger(ot.Lookup)
|
|
def merge(merger, self, lst):
|
|
merger.lookups = lst
|
|
merger.mergeObjects(self, lst)
|
|
del merger.lookups
|
|
|
|
|
|
def interpolate_layout(designspace_filename, loc, finder):
|
|
|
|
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."
|
|
|
|
from pprint import pprint
|
|
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]
|
|
|
|
#font = master_fonts[base_idx]
|
|
font = TTFont(master_ttfs[base_idx])
|
|
|
|
master_locs = [o['location'] for o in masters]
|
|
|
|
axis_tags = set(master_locs[0].keys())
|
|
assert all(axis_tags == set(m.keys()) for m in master_locs)
|
|
|
|
# Set up axes
|
|
axes = {}
|
|
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)
|
|
|
|
print("Location:", loc)
|
|
print("Master locations:")
|
|
pprint(master_locs)
|
|
|
|
# Normalize locations
|
|
loc = models.normalizeLocation(loc, axes)
|
|
master_locs = [models.normalizeLocation(m, axes) for m in master_locs]
|
|
|
|
print("Normalized location:", loc)
|
|
print("Normalized master locations:")
|
|
pprint(master_locs)
|
|
|
|
# Assume single-model for now.
|
|
model = models.VariationModel(master_locs)
|
|
assert 0 == model.mapping[base_idx]
|
|
|
|
merger = InstancerMerger(font, model, loc)
|
|
|
|
print("Building variations tables")
|
|
merge_tables(font, merger, master_fonts, axes, base_idx, ['GPOS'])
|
|
return font
|
|
|
|
|
|
def main(args=None):
|
|
|
|
import sys
|
|
if args is None:
|
|
args = sys.argv[1:]
|
|
|
|
designspace_filename = args[0]
|
|
locargs = args[1:]
|
|
outfile = os.path.splitext(designspace_filename)[0] + '-instance.ttf'
|
|
|
|
finder = lambda s: s.replace('master_ufo', 'master_ttf_interpolatable').replace('.ufo', '.ttf')
|
|
|
|
loc = {}
|
|
for arg in locargs:
|
|
tag,val = arg.split('=')
|
|
loc[tag] = float(val)
|
|
|
|
font = interpolate_layout(designspace_filename, loc, finder)
|
|
print("Saving font", outfile)
|
|
font.save(outfile)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
if len(sys.argv) > 1:
|
|
sys.exit(main())
|
|
import doctest
|
|
sys.exit(doctest.testmod().failed)
|