| """ |
| 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')) |
| |
| # Align them |
| glyphs, padded = _merge_GlyphOrders(merger.font, |
| [v.Coverage.glyphs for v in lst], |
| [v.PairSet for v in lst]) |
| |
| empty = ot.PairSet() |
| empty.PairValueRecord = [] |
| empty.PairValueCount = 0 |
| |
| for i,values in enumerate(padded): |
| for j,glyph in enumerate(glyphs): |
| if values[j] is not None: continue |
| values[j] = 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: |
| main() |
| #sys.exit(0) |
| import doctest, sys |
| sys.exit(doctest.testmod().failed) |