merge.py revision 0bf4f561da7972ab90f6bee87fa30923e201adf5
1# Copyright 2013 Google, Inc. All Rights Reserved. 2# 3# Google Author(s): Behdad Esfahbod 4 5"""Font merger. 6""" 7 8import sys 9import time 10 11import fontTools 12from fontTools import misc, ttLib, cffLib 13 14def _add_method(*clazzes): 15 """Returns a decorator function that adds a new method to one or 16 more classes.""" 17 def wrapper(method): 18 for clazz in clazzes: 19 assert clazz.__name__ != 'DefaultTable', 'Oops, table class not found.' 20 assert not hasattr(clazz, method.func_name), \ 21 "Oops, class '%s' has method '%s'." % (clazz.__name__, 22 method.func_name) 23 setattr(clazz, method.func_name, method) 24 return None 25 return wrapper 26 27 28@_add_method(fontTools.ttLib.getTableClass('maxp')) 29def merge(self, m): 30 # TODO When we correctly merge hinting data, update these values: 31 # maxFunctionDefs, maxInstructionDefs, maxSizeOfInstructions 32 # TODO Assumes that all tables have format 1.0; safe assumption. 33 for key in set(sum((vars(table).keys() for table in m.tables), [])): 34 setattr(self, key, max(getattr(table, key) for table in m.tables)) 35 return True 36 37@_add_method(fontTools.ttLib.getTableClass('head')) 38def merge(self, m): 39 # TODO Check that unitsPerEm are the same. 40 # TODO Use bitwise ops for flags, macStyle, fontDirectionHint 41 minMembers = ['xMin', 'yMin'] 42 # Negate some members 43 for key in minMembers: 44 for table in m.tables: 45 setattr(table, key, -getattr(table, key)) 46 # Get max over members 47 for key in set(sum((vars(table).keys() for table in m.tables), [])): 48 setattr(self, key, max(getattr(table, key) for table in m.tables)) 49 # Negate them back 50 for key in minMembers: 51 for table in m.tables: 52 setattr(table, key, -getattr(table, key)) 53 setattr(self, key, -getattr(self, key)) 54 return True 55 56@_add_method(fontTools.ttLib.getTableClass('hhea')) 57def merge(self, m): 58 # TODO Check that ascent, descent, slope, etc are the same. 59 minMembers = ['descent', 'minLeftSideBearing', 'minRightSideBearing'] 60 # Negate some members 61 for key in minMembers: 62 for table in m.tables: 63 setattr(table, key, -getattr(table, key)) 64 # Get max over members 65 for key in set(sum((vars(table).keys() for table in m.tables), [])): 66 setattr(self, key, max(getattr(table, key) for table in m.tables)) 67 # Negate them back 68 for key in minMembers: 69 for table in m.tables: 70 setattr(table, key, -getattr(table, key)) 71 setattr(self, key, -getattr(self, key)) 72 return True 73 74@_add_method(fontTools.ttLib.getTableClass('OS/2')) 75def merge(self, m): 76 # TODO Check that weight/width/subscript/superscript/etc are the same. 77 # TODO Bitwise ops for UnicodeRange/CodePageRange. 78 # TODO Pretty much all fields generated here have bogus values. 79 # Get max over members 80 for key in set(sum((vars(table).keys() for table in m.tables), [])): 81 setattr(self, key, max(getattr(table, key) for table in m.tables)) 82 return True 83 84@_add_method(fontTools.ttLib.getTableClass('post')) 85def merge(self, m): 86 # TODO Check that italicAngle, underlinePosition, underlineThickness are the same. 87 minMembers = ['underlinePosition', 'minMemType42', 'minMemType1'] 88 # Negate some members 89 for key in minMembers: 90 for table in m.tables: 91 setattr(table, key, -getattr(table, key)) 92 # Get max over members 93 keys = set(sum((vars(table).keys() for table in m.tables), [])) 94 if 'mapping' in keys: 95 keys.remove('mapping') 96 keys.remove('extraNames') 97 for key in keys: 98 setattr(self, key, max(getattr(table, key) for table in m.tables)) 99 # Negate them back 100 for key in minMembers: 101 for table in m.tables: 102 setattr(table, key, -getattr(table, key)) 103 setattr(self, key, -getattr(self, key)) 104 self.mapping = {} 105 for table in m.tables: 106 if hasattr(table, 'mapping'): 107 self.mapping.update(table.mapping) 108 self.extraNames = [] 109 return True 110 111@_add_method(fontTools.ttLib.getTableClass('vmtx'), 112 fontTools.ttLib.getTableClass('hmtx')) 113def merge(self, m): 114 self.metrics = {} 115 for table in m.tables: 116 self.metrics.update(table.metrics) 117 return True 118 119@_add_method(fontTools.ttLib.getTableClass('loca')) 120def merge(self, m): 121 return True # Will be computed automatically 122 123@_add_method(fontTools.ttLib.getTableClass('glyf')) 124def merge(self, m): 125 self.glyphs = {} 126 for table in m.tables: 127 self.glyphs.update(table.glyphs) 128 # TODO Drop hints? 129 return True 130 131@_add_method(fontTools.ttLib.getTableClass('prep'), 132 fontTools.ttLib.getTableClass('fpgm'), 133 fontTools.ttLib.getTableClass('cvt ')) 134def merge(self, m): 135 return False # Will be computed automatically 136 137@_add_method(fontTools.ttLib.getTableClass('cmap')) 138def merge(self, m): 139 # TODO Handle format=14. 140 cmapTables = [t for table in m.tables for t in table.tables 141 if t.platformID == 3 and t.platEncID in [1, 10]] 142 # TODO Better handle format-4 and format-12 coexisting in same font. 143 # TODO Insert both a format-4 and format-12 if needed. 144 module = fontTools.ttLib.getTableModule('cmap') 145 assert all(t.format in [4, 12] for t in cmapTables) 146 format = max(t.format for t in cmapTables) 147 cmapTable = module.cmap_classes[format](format) 148 cmapTable.cmap = {} 149 cmapTable.platformID = 3 150 cmapTable.platEncID = max(t.platEncID for t in cmapTables) 151 cmapTable.language = 0 152 for table in cmapTables: 153 # TODO handle duplicates. 154 cmapTable.cmap.update(table.cmap) 155 self.tableVersion = 0 156 self.tables = [cmapTable] 157 self.numSubTables = len(self.tables) 158 return True 159 160 161class Options(object): 162 163 class UnknownOptionError(Exception): 164 pass 165 166 _drop_tables_default = ['fpgm', 'prep', 'cvt ', 'gasp'] 167 drop_tables = _drop_tables_default 168 169 def __init__(self, **kwargs): 170 171 self.set(**kwargs) 172 173 def set(self, **kwargs): 174 for k,v in kwargs.iteritems(): 175 if not hasattr(self, k): 176 raise self.UnknownOptionError("Unknown option '%s'" % k) 177 setattr(self, k, v) 178 179 def parse_opts(self, argv, ignore_unknown=False): 180 ret = [] 181 opts = {} 182 for a in argv: 183 orig_a = a 184 if not a.startswith('--'): 185 ret.append(a) 186 continue 187 a = a[2:] 188 i = a.find('=') 189 op = '=' 190 if i == -1: 191 if a.startswith("no-"): 192 k = a[3:] 193 v = False 194 else: 195 k = a 196 v = True 197 else: 198 k = a[:i] 199 if k[-1] in "-+": 200 op = k[-1]+'=' # Ops is '-=' or '+=' now. 201 k = k[:-1] 202 v = a[i+1:] 203 k = k.replace('-', '_') 204 if not hasattr(self, k): 205 if ignore_unknown == True or k in ignore_unknown: 206 ret.append(orig_a) 207 continue 208 else: 209 raise self.UnknownOptionError("Unknown option '%s'" % a) 210 211 ov = getattr(self, k) 212 if isinstance(ov, bool): 213 v = bool(v) 214 elif isinstance(ov, int): 215 v = int(v) 216 elif isinstance(ov, list): 217 vv = v.split(',') 218 if vv == ['']: 219 vv = [] 220 vv = [int(x, 0) if len(x) and x[0] in "0123456789" else x for x in vv] 221 if op == '=': 222 v = vv 223 elif op == '+=': 224 v = ov 225 v.extend(vv) 226 elif op == '-=': 227 v = ov 228 for x in vv: 229 if x in v: 230 v.remove(x) 231 else: 232 assert 0 233 234 opts[k] = v 235 self.set(**opts) 236 237 return ret 238 239 240class Merger: 241 242 def __init__(self, options=None, log=None): 243 244 if not log: 245 log = Logger() 246 if not options: 247 options = Options() 248 249 self.options = options 250 self.log = log 251 252 def merge(self, fontfiles): 253 254 mega = ttLib.TTFont() 255 256 # 257 # Settle on a mega glyph order. 258 # 259 fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles] 260 glyphOrders = [font.getGlyphOrder() for font in fonts] 261 megaGlyphOrder = self._mergeGlyphOrders(glyphOrders) 262 # Reload fonts and set new glyph names on them. 263 # TODO Is it necessary to reload font? I think it is. At least 264 # it's safer, in case tables were loaded to provide glyph names. 265 fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles] 266 map(ttLib.TTFont.setGlyphOrder, fonts, glyphOrders) 267 mega.setGlyphOrder(megaGlyphOrder) 268 269 allTags = set(sum([font.keys() for font in fonts], [])) 270 allTags.remove('GlyphOrder') 271 for tag in allTags: 272 273 if tag in self.options.drop_tables: 274 self.log("Dropping '%s'." % tag) 275 continue 276 277 clazz = ttLib.getTableClass(tag) 278 279 if not hasattr(clazz, 'merge'): 280 self.log("Don't know how to merge '%s', dropped." % tag) 281 continue 282 283 # TODO For now assume all fonts have the same tables. 284 self.tables = [font[tag] for font in fonts] 285 table = clazz(tag) 286 if table.merge (self): 287 mega[tag] = table 288 self.log("Merged '%s'." % tag) 289 else: 290 self.log("Dropped '%s'. No need to merge explicitly." % tag) 291 self.log.lapse("merge '%s'" % tag) 292 del self.tables 293 294 return mega 295 296 def _mergeGlyphOrders(self, glyphOrders): 297 """Modifies passed-in glyphOrders to reflect new glyph names.""" 298 # Simply append font index to the glyph name for now. 299 mega = [] 300 for n,glyphOrder in enumerate(glyphOrders): 301 for i,glyphName in enumerate(glyphOrder): 302 glyphName += "#" + `n` 303 glyphOrder[i] = glyphName 304 mega.append(glyphName) 305 return mega 306 307 308class Logger(object): 309 310 def __init__(self, verbose=False, xml=False, timing=False): 311 self.verbose = verbose 312 self.xml = xml 313 self.timing = timing 314 self.last_time = self.start_time = time.time() 315 316 def parse_opts(self, argv): 317 argv = argv[:] 318 for v in ['verbose', 'xml', 'timing']: 319 if "--"+v in argv: 320 setattr(self, v, True) 321 argv.remove("--"+v) 322 return argv 323 324 def __call__(self, *things): 325 if not self.verbose: 326 return 327 print ' '.join(str(x) for x in things) 328 329 def lapse(self, *things): 330 if not self.timing: 331 return 332 new_time = time.time() 333 print "Took %0.3fs to %s" %(new_time - self.last_time, 334 ' '.join(str(x) for x in things)) 335 self.last_time = new_time 336 337 def font(self, font, file=sys.stdout): 338 if not self.xml: 339 return 340 from fontTools.misc import xmlWriter 341 writer = xmlWriter.XMLWriter(file) 342 font.disassembleInstructions = False # Work around ttLib bug 343 for tag in font.keys(): 344 writer.begintag(tag) 345 writer.newline() 346 font[tag].toXML(writer, font) 347 writer.endtag(tag) 348 writer.newline() 349 350 351__all__ = [ 352 'Options', 353 'Merger', 354 'Logger', 355 'main' 356] 357 358def main(args): 359 360 log = Logger() 361 args = log.parse_opts(args) 362 363 options = Options() 364 args = options.parse_opts(args) 365 366 if len(args) < 1: 367 print >>sys.stderr, "usage: pyftmerge font..." 368 sys.exit(1) 369 370 merger = Merger(options=options, log=log) 371 font = merger.merge(args) 372 outfile = 'merged.ttf' 373 font.save(outfile) 374 log.lapse("compile and save font") 375 376 log.last_time = log.start_time 377 log.lapse("make one with everything(TOTAL TIME)") 378 379if __name__ == "__main__": 380 main(sys.argv[1:]) 381