<NME> interpolate_layout.py
<BEF> """
Interpolate OpenType Layout tables (GDEF / GPOS / GSUB).
"""
from fontTools.ttLib import TTFont
from fontTools.varLib import models, VarLibError, load_designspace, load_masters
from fontTools.varLib.merger import InstancerMerger
import os.path
import logging
from copy import deepcopy
from pprint import pformat

log = logging.getLogger("fontTools.varLib.interpolate_layout")


def interpolate_layout(designspace, loc, master_finder=lambda s:s, mapped=False):
	"""
	Interpolate GPOS from a designspace file and location.

	If master_finder is set, it should be a callable that takes master
	filename as found in designspace file and map it to master font
	binary as to be opened (eg. .ttf or .otf).

	If mapped is False (default), then location is mapped using the
	map element of the axes in designspace file.  If mapped is True,
	it is assumed that location is in designspace's internal space and
	no mapping is performed.
	"""
	if hasattr(designspace, "sources"):  # Assume a DesignspaceDocument
		pass
	else:  # Assume a file path
		from fontTools.designspaceLib import DesignSpaceDocument
		designspace = DesignSpaceDocument.fromfile(designspace)

	ds = load_designspace(designspace)
	log.info("Building interpolated font")

	log.info("Loading master fonts")
	master_fonts = load_masters(designspace, master_finder)
	font = deepcopy(master_fonts[ds.base_idx])

	log.info("Location: %s", pformat(loc))
	if not mapped:
		loc = {name: ds.axes[name].map_forward(v) for name,v in loc.items()}
	log.info("Internal location: %s", pformat(loc))
	loc = models.normalizeLocation(loc, ds.internal_axis_supports)
	log.info("Normalized location: %s", pformat(loc))

	# Assume single-model for now.
	model = models.VariationModel(ds.normalized_master_locs)
	assert 0 == model.mapping[ds.base_idx]

	merger = InstancerMerger(font, model, loc)

	log.info("Building interpolated tables")
	# TODO GSUB/GDEF
	merger.mergeTables(font, master_fonts, ['GPOS'])
	return font


def main(args=None):
	"""Interpolate GDEF/GPOS/GSUB tables for a point on a designspace"""
	from fontTools import configLogger
	import argparse
	import sys

	parser = argparse.ArgumentParser(
		"fonttools varLib.interpolate_layout",
		description=main.__doc__,
	)
	parser.add_argument('designspace_filename', metavar='DESIGNSPACE',
		help="Input TTF files")
	parser.add_argument('locations', metavar='LOCATION', type=str, nargs='+',
		help="Axis locations (e.g. wdth=120")
	parser.add_argument('-o', '--output', metavar='OUTPUT',
		help="Output font file (defaults to <designspacename>-instance.ttf)")
	parser.add_argument('-l', '--loglevel', metavar='LEVEL', default="INFO",
		help="Logging level (defaults to INFO)")


	args = parser.parse_args(args)

	if not args.output:
		args.output = os.path.splitext(args.designspace_filename)[0] + '-instance.ttf'

	configLogger(level=args.loglevel)

	finder = lambda s: s.replace('master_ufo', 'master_ttf_interpolatable').replace('.ufo', '.ttf')

	loc = {}
	for arg in args.locations:
		tag,val = arg.split('=')
		loc[tag] = float(val)

	font = interpolate_layout(args.designspace_filename, loc, finder)
	log.info("Saving font %s", args.output)
	font.save(args.output)


if __name__ == "__main__":
	import sys
	if len(sys.argv) > 1:
		sys.exit(main())
	import doctest
	sys.exit(doctest.testmod().failed)

	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):
	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



<MSG> [varLib.interpolate_layout] Implement flattening of multiple PairPosFormat1 subtables

Also fixes issues with merging of PairPos.

Trying on Noto Sans Thai still fails, now because of issues in class differences in
PairPosFormat2. :(  Investigating.

This is part of fixing https://github.com/fonttools/fonttools/issues/719
though, the changes are currently in interpolate_layout, and need to be ported /
merged with varLib.__init__ variation-font-builder.

<DFF> @@ -188,20 +188,23 @@ def merge(merger, self, lst):
 
 	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 values[j] is not None:
+				vpair = values[j]
+			else:
+				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
+				v1, v2 = None, None
 			else:
-				v.Value1, v.Value2 = vpair.Value1, vpair.Value2
+				v1, v2 = vpair.Value1, vpair.Value2
+			v.Value1 = otBase.ValueRecord(merger.valueFormat1, src=v1) if merger.valueFormat1 else None
+			v.Value2 = otBase.ValueRecord(merger.valueFormat2, src=v2) if merger.valueFormat2 else None
 			values[j] = v
 	del self._firstGlyph
 
-	merger.mergeThings(self.PairValueRecord, padded)
+	merger.mergeLists(self.PairValueRecord, padded)
 
 @InstancerMerger.merger(ot.PairPos)
 def merge(merger, self, lst):
@@ -237,14 +240,84 @@ def merge(merger, self, lst):
 	for glyph, ps in zip(glyphs, self.PairSet):
 		ps._firstGlyph = glyph
 
-	merger.mergeThings(self.PairSet, padded)
+	merger.mergeLists(self.PairSet, padded)
 
 	del merger.valueFormat1, merger.valueFormat2
 
+
+def _PairSet_merge_overlay(lst, font):
+	self = ot.PairSet()
+	self.Coverage = ot.Coverage()
+
+	# Align them
+	glyphs, padded = _merge_GlyphOrders(font,
+				[[v.SecondGlyph for v in vs.PairValueRecord] for vs in lst],
+				[vs.PairValueRecord for vs in lst])
+
+	self.Coverage.glyphs = glyphs
+	self.PairValueRecord = pvrs = []
+	for values in zip(*padded):
+		for v in values:
+			if v is not None:
+				pvrs.append(v)
+				break
+		else:
+			assert False
+	self.PairValueCount = len(self.PairValueRecord)
+
+	return self
+
+def _Lookup_PairPosFormat1_subtables_merge_overlay(lst, font):
+	self = ot.PairPos()
+	self.Format = 1
+	self.Coverage = ot.Coverage()
+	self.ValueFormat1 = reduce(int.__or__, [l.ValueFormat1 for l in lst])
+	self.ValueFormat2 = reduce(int.__or__, [l.ValueFormat2 for l in lst])
+
+	# Align them
+	glyphs, padded = _merge_GlyphOrders(font,
+					    [v.Coverage.glyphs for v in lst],
+					    [v.PairSet for v in lst])
+
+	self.Coverage.glyphs = glyphs
+	self.PairSet = [_PairSet_merge_overlay([v for v in values if v is not None], font)
+		        for values in zip(*padded)]
+	self.PairSetCount = len(self.PairSet)
+	return self
+
+def _Lookup_PairPos_subtables_canonicalize(lst, font):
+	"""Merges multiple Format1 subtables at the beginning of lst.  Returns new list."""
+	head = []
+	tail = []
+	it = iter(lst)
+	for subtable in it:
+		if subtable.Format == 1:
+			head.append(subtable)
+			continue
+		tail.append(subtable)
+		break
+	tail.extend(it)
+	# TODO Only do this if at least one font has a Format1.
+	tail.insert(0, _Lookup_PairPosFormat1_subtables_merge_overlay(head, font))
+	return tail
+
 @InstancerMerger.merger(ot.Lookup)
 def merge(merger, self, lst):
 	merger.lookups = lst
-	merger.mergeObjects(self, lst)
+
+	exclude = []
+	if self.SubTable and isinstance(self.SubTable[0], ot.PairPos):
+		# AFDKO and feaLib sometimes generate two Format1 subtables instead of one.
+		# Merge those before continuing.
+		# https://github.com/fonttools/fonttools/issues/719
+		self.SubTable = _Lookup_PairPos_subtables_canonicalize(self.SubTable, merger.font)
+		subtables = [_Lookup_PairPos_subtables_canonicalize(l.SubTable, merger.font) for l in lst]
+		merger.mergeLists(self.SubTable, subtables)
+		self.SubTableCount = len(self.SubTable)
+		exclude.extend(['SubTable', 'SubTableCount'])
+
+	merger.mergeObjects(self, lst, exclude=exclude)
+
 	del merger.lookups
 
 

{'addition_count': 81, 'commit_subject': '[varLib.interpolate_layout] Implement flattening of multiple PairPosFormat1 subtables', 'deletion_count': 8, 'file_extension': '.py', 'lang': 'py', 'license': 'mit', 'repo_name': 'fonttools/fonttools'}