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