merge.py revision 65f19d8440f5fa5d94d1f04593f16cd9f21f176b
145d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod# Copyright 2013 Google, Inc. All Rights Reserved.
245d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod#
345d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod# Google Author(s): Behdad Esfahbod
445d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod
545d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod"""Font merger.
645d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod"""
745d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod
845d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbodimport sys
945d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod
1045d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbodimport fontTools
1145d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbodfrom fontTools import misc, ttLib, cffLib
1245d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod
1345d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahboddef _add_method(*clazzes):
1445d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod  """Returns a decorator function that adds a new method to one or
1545d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod  more classes."""
1645d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod  def wrapper(method):
1745d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod    for clazz in clazzes:
1845d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod      assert clazz.__name__ != 'DefaultTable', 'Oops, table class not found.'
1945d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod      assert not hasattr(clazz, method.func_name), \
2045d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod          "Oops, class '%s' has method '%s'." % (clazz.__name__,
2145d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod                                                 method.func_name)
2245d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod      setattr(clazz, method.func_name, method)
2345d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod    return None
2445d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod  return wrapper
2545d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod
2645d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod
2745d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod@_add_method(fontTools.ttLib.getTableClass('maxp'))
2845d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahboddef merge(self, tables, fonts):
2945d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod	# TODO When we correctly merge hinting data, update these values:
3045d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod	# maxFunctionDefs, maxInstructionDefs, maxSizeOfInstructions
3145d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod	# TODO Assumes that all tables have format 1.0; safe assumption.
3245d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod	for key in set(sum((vars(table).keys() for table in tables), [])):
3345d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod			setattr(self, key, max(getattr(table, key) for table in tables))
3465f19d8440f5fa5d94d1f04593f16cd9f21f176bBehdad Esfahbod	return True
3565f19d8440f5fa5d94d1f04593f16cd9f21f176bBehdad Esfahbod
3665f19d8440f5fa5d94d1f04593f16cd9f21f176bBehdad Esfahbod@_add_method(fontTools.ttLib.getTableClass('hhea'))
3765f19d8440f5fa5d94d1f04593f16cd9f21f176bBehdad Esfahboddef merge(self, tables, fonts):
3865f19d8440f5fa5d94d1f04593f16cd9f21f176bBehdad Esfahbod	# TODO Check that ascent, descent, slope, etc are the same.
3965f19d8440f5fa5d94d1f04593f16cd9f21f176bBehdad Esfahbod	minMembers = ['descent', 'minLeftSideBearing', 'minRightSideBearing']
4065f19d8440f5fa5d94d1f04593f16cd9f21f176bBehdad Esfahbod	# Negate some members
4165f19d8440f5fa5d94d1f04593f16cd9f21f176bBehdad Esfahbod	for key in minMembers:
4265f19d8440f5fa5d94d1f04593f16cd9f21f176bBehdad Esfahbod		for table in tables:
4365f19d8440f5fa5d94d1f04593f16cd9f21f176bBehdad Esfahbod			setattr(table, key, -getattr(table, key))
4465f19d8440f5fa5d94d1f04593f16cd9f21f176bBehdad Esfahbod	# Get max over members
4565f19d8440f5fa5d94d1f04593f16cd9f21f176bBehdad Esfahbod	for key in set(sum((vars(table).keys() for table in tables), [])):
4665f19d8440f5fa5d94d1f04593f16cd9f21f176bBehdad Esfahbod		setattr(self, key, max(getattr(table, key) for table in tables))
4765f19d8440f5fa5d94d1f04593f16cd9f21f176bBehdad Esfahbod	# Negate them back
4865f19d8440f5fa5d94d1f04593f16cd9f21f176bBehdad Esfahbod	for key in minMembers:
4965f19d8440f5fa5d94d1f04593f16cd9f21f176bBehdad Esfahbod		for table in tables:
5065f19d8440f5fa5d94d1f04593f16cd9f21f176bBehdad Esfahbod			setattr(table, key, -getattr(table, key))
5165f19d8440f5fa5d94d1f04593f16cd9f21f176bBehdad Esfahbod		setattr(self, key, -getattr(self, key))
5265f19d8440f5fa5d94d1f04593f16cd9f21f176bBehdad Esfahbod	return True
5365f19d8440f5fa5d94d1f04593f16cd9f21f176bBehdad Esfahbod
5465f19d8440f5fa5d94d1f04593f16cd9f21f176bBehdad Esfahbod@_add_method(fontTools.ttLib.getTableClass('vmtx'),
5565f19d8440f5fa5d94d1f04593f16cd9f21f176bBehdad Esfahbod             fontTools.ttLib.getTableClass('hmtx'))
5665f19d8440f5fa5d94d1f04593f16cd9f21f176bBehdad Esfahboddef merge(self, tables, fonts):
5765f19d8440f5fa5d94d1f04593f16cd9f21f176bBehdad Esfahbod	self.metrics = {}
5865f19d8440f5fa5d94d1f04593f16cd9f21f176bBehdad Esfahbod	for table in tables:
5965f19d8440f5fa5d94d1f04593f16cd9f21f176bBehdad Esfahbod		self.metrics.update(table.metrics)
6065f19d8440f5fa5d94d1f04593f16cd9f21f176bBehdad Esfahbod	return True
6165f19d8440f5fa5d94d1f04593f16cd9f21f176bBehdad Esfahbod
6265f19d8440f5fa5d94d1f04593f16cd9f21f176bBehdad Esfahbod@_add_method(fontTools.ttLib.getTableClass('loca'))
6365f19d8440f5fa5d94d1f04593f16cd9f21f176bBehdad Esfahboddef merge(self, tables, fonts):
6465f19d8440f5fa5d94d1f04593f16cd9f21f176bBehdad Esfahbod	return False # Will be computed automatically
6545d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod
6645d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbodclass Merger:
6745d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod
6845d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod	def __init__(self, fontfiles):
6945d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod		self.fontfiles = fontfiles
7045d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod
7145d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod	def merge(self):
7245d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod
7345d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod		mega = ttLib.TTFont()
7445d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod
7545d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod		#
7645d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod		# Settle on a mega glyph order.
7745d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod		#
7845d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod		fonts = [ttLib.TTFont(fontfile) for fontfile in self.fontfiles]
7945d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod		glyphOrders = [font.getGlyphOrder() for font in fonts]
8045d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod		megaGlyphOrder = self._mergeGlyphOrders(glyphOrders)
8145d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod		# Reload fonts and set new glyph names on them.
8245d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod		# TODO Is it necessary to reload font?  I think it is.  At least
8345d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod		# it's safer, in case tables were loaded to provide glyph names.
8445d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod		fonts = [ttLib.TTFont(fontfile) for fontfile in self.fontfiles]
8545d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod		map(ttLib.TTFont.setGlyphOrder, fonts, glyphOrders)
8645d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod		mega.setGlyphOrder(megaGlyphOrder)
8745d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod
8845d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod		cmaps = [self._get_cmap(font) for font in fonts]
8945d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod
9045d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod		allTags = set(sum([font.keys() for font in fonts], []))
9145d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod		allTags.remove('GlyphOrder')
9245d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod		for tag in allTags:
9345d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod			clazz = ttLib.getTableClass(tag)
9445d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod
9545d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod			if not hasattr(clazz, 'merge'):
9645d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod				print "Don't know how to merge '%s', dropped." % tag
9745d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod				continue
9845d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod
9945d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod			# TODO For now assume all fonts have the same tables.
10045d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod			tables = [font[tag] for font in fonts]
10145d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod			table = clazz(tag)
10265f19d8440f5fa5d94d1f04593f16cd9f21f176bBehdad Esfahbod			if table.merge (tables, fonts):
10365f19d8440f5fa5d94d1f04593f16cd9f21f176bBehdad Esfahbod				mega[tag] = table
10465f19d8440f5fa5d94d1f04593f16cd9f21f176bBehdad Esfahbod				print "Merged '%s'." % tag
10565f19d8440f5fa5d94d1f04593f16cd9f21f176bBehdad Esfahbod			else:
10665f19d8440f5fa5d94d1f04593f16cd9f21f176bBehdad Esfahbod				print "Dropped '%s'.  No need to merge explicitly." % tag
10745d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod
10845d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod		return mega
10945d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod
11045d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod	def _get_cmap(self, font):
11145d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod		cmap = font['cmap']
11245d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod		tables = [t for t in cmap.tables
11345d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod			    if t.platformID == 3 and t.platEncID in [1, 10]]
11445d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod		# XXX Handle format=14
11545d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod		assert len(tables)
11645d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod		# Pick table that has largest coverage
11745d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod		table = max(tables, key=lambda t: len(t.cmap))
11845d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod		return table
11945d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod
12045d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod	def _mergeGlyphOrders(self, glyphOrders):
12145d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod		"""Modifies passed-in glyphOrders to reflect new glyph names."""
12245d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod		# Simply append font index to the glyph name for now.
12345d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod		mega = []
12445d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod		for n,glyphOrder in enumerate(glyphOrders):
12545d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod			for i,glyphName in enumerate(glyphOrder):
12645d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod				glyphName += "#" + `n`
12745d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod				glyphOrder[i] = glyphName
12845d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod				mega.append(glyphName)
12945d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod		return mega
13045d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod
13145d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahboddef main(args):
13245d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod	if len(args) < 1:
13345d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod		print >>sys.stderr, "usage: pyftmerge font..."
13445d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod		sys.exit(1)
13545d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod	merger = Merger(args)
13645d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod	font = merger.merge()
13745d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod	outfile = 'merged.ttf'
13845d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod	font.save(outfile)
13945d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod
14045d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbodif __name__ == "__main__":
14145d2f38aa51169f400062f2153a74277b9d4d04eBehdad Esfahbod	main(sys.argv[1:])
142