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