merge.py revision 65f19d8440f5fa5d94d1f04593f16cd9f21f176b
1# Copyright 2013 Google, Inc. All Rights Reserved.
2#
3# Google Author(s): Behdad Esfahbod
4
5"""Font merger.
6"""
7
8import sys
9
10import fontTools
11from fontTools import misc, ttLib, cffLib
12
13def _add_method(*clazzes):
14  """Returns a decorator function that adds a new method to one or
15  more classes."""
16  def wrapper(method):
17    for clazz in clazzes:
18      assert clazz.__name__ != 'DefaultTable', 'Oops, table class not found.'
19      assert not hasattr(clazz, method.func_name), \
20          "Oops, class '%s' has method '%s'." % (clazz.__name__,
21                                                 method.func_name)
22      setattr(clazz, method.func_name, method)
23    return None
24  return wrapper
25
26
27@_add_method(fontTools.ttLib.getTableClass('maxp'))
28def merge(self, tables, fonts):
29	# TODO When we correctly merge hinting data, update these values:
30	# maxFunctionDefs, maxInstructionDefs, maxSizeOfInstructions
31	# TODO Assumes that all tables have format 1.0; safe assumption.
32	for key in set(sum((vars(table).keys() for table in tables), [])):
33			setattr(self, key, max(getattr(table, key) for table in tables))
34	return True
35
36@_add_method(fontTools.ttLib.getTableClass('hhea'))
37def merge(self, tables, fonts):
38	# TODO Check that ascent, descent, slope, etc are the same.
39	minMembers = ['descent', 'minLeftSideBearing', 'minRightSideBearing']
40	# Negate some members
41	for key in minMembers:
42		for table in tables:
43			setattr(table, key, -getattr(table, key))
44	# Get max over members
45	for key in set(sum((vars(table).keys() for table in tables), [])):
46		setattr(self, key, max(getattr(table, key) for table in tables))
47	# Negate them back
48	for key in minMembers:
49		for table in tables:
50			setattr(table, key, -getattr(table, key))
51		setattr(self, key, -getattr(self, key))
52	return True
53
54@_add_method(fontTools.ttLib.getTableClass('vmtx'),
55             fontTools.ttLib.getTableClass('hmtx'))
56def merge(self, tables, fonts):
57	self.metrics = {}
58	for table in tables:
59		self.metrics.update(table.metrics)
60	return True
61
62@_add_method(fontTools.ttLib.getTableClass('loca'))
63def merge(self, tables, fonts):
64	return False # Will be computed automatically
65
66class Merger:
67
68	def __init__(self, fontfiles):
69		self.fontfiles = fontfiles
70
71	def merge(self):
72
73		mega = ttLib.TTFont()
74
75		#
76		# Settle on a mega glyph order.
77		#
78		fonts = [ttLib.TTFont(fontfile) for fontfile in self.fontfiles]
79		glyphOrders = [font.getGlyphOrder() for font in fonts]
80		megaGlyphOrder = self._mergeGlyphOrders(glyphOrders)
81		# Reload fonts and set new glyph names on them.
82		# TODO Is it necessary to reload font?  I think it is.  At least
83		# it's safer, in case tables were loaded to provide glyph names.
84		fonts = [ttLib.TTFont(fontfile) for fontfile in self.fontfiles]
85		map(ttLib.TTFont.setGlyphOrder, fonts, glyphOrders)
86		mega.setGlyphOrder(megaGlyphOrder)
87
88		cmaps = [self._get_cmap(font) for font in fonts]
89
90		allTags = set(sum([font.keys() for font in fonts], []))
91		allTags.remove('GlyphOrder')
92		for tag in allTags:
93			clazz = ttLib.getTableClass(tag)
94
95			if not hasattr(clazz, 'merge'):
96				print "Don't know how to merge '%s', dropped." % tag
97				continue
98
99			# TODO For now assume all fonts have the same tables.
100			tables = [font[tag] for font in fonts]
101			table = clazz(tag)
102			if table.merge (tables, fonts):
103				mega[tag] = table
104				print "Merged '%s'." % tag
105			else:
106				print "Dropped '%s'.  No need to merge explicitly." % tag
107
108		return mega
109
110	def _get_cmap(self, font):
111		cmap = font['cmap']
112		tables = [t for t in cmap.tables
113			    if t.platformID == 3 and t.platEncID in [1, 10]]
114		# XXX Handle format=14
115		assert len(tables)
116		# Pick table that has largest coverage
117		table = max(tables, key=lambda t: len(t.cmap))
118		return table
119
120	def _mergeGlyphOrders(self, glyphOrders):
121		"""Modifies passed-in glyphOrders to reflect new glyph names."""
122		# Simply append font index to the glyph name for now.
123		mega = []
124		for n,glyphOrder in enumerate(glyphOrders):
125			for i,glyphName in enumerate(glyphOrder):
126				glyphName += "#" + `n`
127				glyphOrder[i] = glyphName
128				mega.append(glyphName)
129		return mega
130
131def main(args):
132	if len(args) < 1:
133		print >>sys.stderr, "usage: pyftmerge font..."
134		sys.exit(1)
135	merger = Merger(args)
136	font = merger.merge()
137	outfile = 'merged.ttf'
138	font.save(outfile)
139
140if __name__ == "__main__":
141	main(sys.argv[1:])
142