merge.py revision f480c7cf21c51ef67570f1a5fa1c1653fa817bfc
1# Copyright 2013 Google, Inc. All Rights Reserved. 2# 3# Google Author(s): Behdad Esfahbod, Roozbeh Pournader 4 5"""Font merger. 6""" 7 8from __future__ import print_function, division, absolute_import 9from fontTools.misc.py23 import * 10from fontTools import ttLib, cffLib 11from fontTools.ttLib.tables import otTables, _h_e_a_d 12from fontTools.ttLib.tables.DefaultTable import DefaultTable 13from functools import reduce 14import sys 15import time 16import operator 17 18 19def _add_method(*clazzes, **kwargs): 20 """Returns a decorator function that adds a new method to one or 21 more classes.""" 22 allowDefault = kwargs.get('allowDefaultTable', False) 23 def wrapper(method): 24 for clazz in clazzes: 25 assert allowDefault or clazz != DefaultTable, 'Oops, table class not found.' 26 assert method.__name__ not in clazz.__dict__, \ 27 "Oops, class '%s' has method '%s'." % (clazz.__name__, 28 method.__name__) 29 setattr(clazz, method.__name__, method) 30 return None 31 return wrapper 32 33# General utility functions for merging values from different fonts 34 35def equal(lst): 36 t = iter(lst) 37 first = next(t) 38 assert all(item == first for item in t) 39 return first 40 41def first(lst): 42 return next(iter(lst)) 43 44def recalculate(lst): 45 return NotImplemented 46 47def current_time(lst): 48 return int(time.time() - _h_e_a_d.mac_epoch_diff) 49 50def bitwise_and(lst): 51 return reduce(operator.and_, lst) 52 53def bitwise_or(lst): 54 return reduce(operator.or_, lst) 55 56def avg_int(lst): 57 lst = list(lst) 58 return sum(lst) // len(lst) 59 60def implementedFilter(func): 61 """Returns a filter func that when called with a list, 62 only calls func on the non-NotImplemented items of the list, 63 and only so if there's at least one item remaining. 64 Otherwise returns NotImplemented.""" 65 66 def wrapper(lst): 67 items = [item for item in lst if item is not NotImplemented] 68 return func(items) if items else NotImplemented 69 70 return wrapper 71 72def sumLists(lst): 73 l = [] 74 for item in lst: 75 l.extend(item) 76 return l 77 78def sumDicts(lst): 79 d = {} 80 for item in lst: 81 d.update(item) 82 return d 83 84def mergeObjects(lst): 85 lst = [item for item in lst if item is not None and item is not NotImplemented] 86 if not lst: 87 return None # Not all can be NotImplemented 88 89 clazz = lst[0].__class__ 90 assert all(type(item) == clazz for item in lst), lst 91 logic = clazz.mergeMap 92 returnTable = clazz() 93 94 allKeys = set.union(set(), *(vars(table).keys() for table in lst)) 95 for key in allKeys: 96 try: 97 mergeLogic = logic[key] 98 except KeyError: 99 try: 100 mergeLogic = logic['*'] 101 except KeyError: 102 raise Exception("Don't know how to merge key %s of class %s" % 103 (key, clazz.__name__)) 104 if mergeLogic is NotImplemented: 105 continue 106 value = mergeLogic(getattr(table, key, NotImplemented) for table in lst) 107 if value is not NotImplemented: 108 setattr(returnTable, key, value) 109 110 return returnTable 111 112def mergeBits(logic, lst): 113 lst = list(lst) 114 returnValue = 0 115 for bitNumber in range(logic['size']): 116 try: 117 mergeLogic = logic[bitNumber] 118 except KeyError: 119 try: 120 mergeLogic = logic['*'] 121 except KeyError: 122 raise Exception("Don't know how to merge bit %s" % bitNumber) 123 shiftedBit = 1 << bitNumber 124 mergedValue = mergeLogic(bool(item & shiftedBit) for item in lst) 125 returnValue |= mergedValue << bitNumber 126 return returnValue 127 128 129@_add_method(DefaultTable, allowDefaultTable=True) 130def merge(self, m, tables): 131 if not hasattr(self, 'mergeMap'): 132 m.log("Don't know how to merge '%s'." % self.tableTag) 133 return NotImplemented 134 135 logic = self.mergeMap 136 137 if isinstance(logic, dict): 138 return m.mergeObjects(self, self.mergeMap, tables) 139 else: 140 return logic(tables) 141 142 143ttLib.getTableClass('maxp').mergeMap = { 144 '*': max, 145 'tableTag': equal, 146 'tableVersion': equal, 147 'numGlyphs': sum, 148 'maxStorage': first, 149 'maxFunctionDefs': first, 150 'maxInstructionDefs': first, 151 # TODO When we correctly merge hinting data, update these values: 152 # maxFunctionDefs, maxInstructionDefs, maxSizeOfInstructions 153} 154 155headFlagsMergeMap = { 156 'size': 16, 157 '*': bitwise_or, 158 1: bitwise_and, # Baseline at y = 0 159 2: bitwise_and, # lsb at x = 0 160 3: bitwise_and, # Force ppem to integer values. FIXME? 161 5: bitwise_and, # Font is vertical 162 6: lambda bit: 0, # Always set to zero 163 11: bitwise_and, # Font data is 'lossless' 164 13: bitwise_and, # Optimized for ClearType 165 14: bitwise_and, # Last resort font. FIXME? equal or first may be better 166 15: lambda bit: 0, # Always set to zero 167} 168 169ttLib.getTableClass('head').mergeMap = { 170 'tableTag': equal, 171 'tableVersion': max, 172 'fontRevision': max, 173 'checkSumAdjustment': lambda lst: 0, # We need *something* here 174 'magicNumber': equal, 175 'flags': lambda lst: mergeBits(headFlagsMergeMap, lst), 176 'unitsPerEm': equal, 177 'created': current_time, 178 'modified': current_time, 179 'xMin': min, 180 'yMin': min, 181 'xMax': max, 182 'yMax': max, 183 'macStyle': first, 184 'lowestRecPPEM': max, 185 'fontDirectionHint': lambda lst: 2, 186 'indexToLocFormat': recalculate, 187 'glyphDataFormat': equal, 188} 189 190ttLib.getTableClass('hhea').mergeMap = { 191 '*': equal, 192 'tableTag': equal, 193 'tableVersion': max, 194 'ascent': max, 195 'descent': min, 196 'lineGap': max, 197 'advanceWidthMax': max, 198 'minLeftSideBearing': min, 199 'minRightSideBearing': min, 200 'xMaxExtent': max, 201 'caretSlopeRise': first, 202 'caretSlopeRun': first, 203 'caretOffset': first, 204 'numberOfHMetrics': recalculate, 205} 206 207os2FsTypeMergeMap = { 208 'size': 16, 209 '*': lambda bit: 0, 210 1: bitwise_or, # no embedding permitted 211 2: bitwise_and, # allow previewing and printing documents 212 3: bitwise_and, # allow editing documents 213 8: bitwise_or, # no subsetting permitted 214 9: bitwise_or, # no embedding of outlines permitted 215} 216 217def mergeOs2FsType(lst): 218 lst = list(lst) 219 if all(item == 0 for item in lst): 220 return 0 221 222 # Compute least restrictive logic for each fsType value 223 for i in range(len(lst)): 224 # unset bit 1 (no embedding permitted) if either bit 2 or 3 is set 225 if lst[i] & 0x000C: 226 lst[i] &= ~0x0002 227 # set bit 2 (allow previewing) if bit 3 is set (allow editing) 228 elif lst[i] & 0x0008: 229 lst[i] |= 0x0004 230 # set bits 2 and 3 if everything is allowed 231 elif lst[i] == 0: 232 lst[i] = 0x000C 233 234 fsType = mergeBits(os2FsTypeMergeMap, lst) 235 # unset bits 2 and 3 if bit 1 is set (some font is "no embedding") 236 if fsType & 0x0002: 237 fsType &= ~0x000C 238 return fsType 239 240 241ttLib.getTableClass('OS/2').mergeMap = { 242 '*': first, 243 'tableTag': equal, 244 'version': max, 245 'xAvgCharWidth': avg_int, # Apparently fontTools doesn't recalc this 246 'fsType': mergeOs2FsType, # Will be overwritten 247 'panose': first, # FIXME: should really be the first Latin font 248 'ulUnicodeRange1': bitwise_or, 249 'ulUnicodeRange2': bitwise_or, 250 'ulUnicodeRange3': bitwise_or, 251 'ulUnicodeRange4': bitwise_or, 252 'fsFirstCharIndex': min, 253 'fsLastCharIndex': max, 254 'sTypoAscender': max, 255 'sTypoDescender': min, 256 'sTypoLineGap': max, 257 'usWinAscent': max, 258 'usWinDescent': max, 259 'ulCodePageRange1': bitwise_or, 260 'ulCodePageRange2': bitwise_or, 261 'usMaxContex': max, 262 # TODO version 5 263} 264 265@_add_method(ttLib.getTableClass('OS/2')) 266def merge(self, m, tables): 267 DefaultTable.merge(self, m, tables) 268 if self.version < 2: 269 # bits 8 and 9 are reserved and should be set to zero 270 self.fsType &= ~0x0300 271 if self.version >= 3: 272 # Only one of bits 1, 2, and 3 may be set. We already take 273 # care of bit 1 implications in mergeOs2FsType. So unset 274 # bit 2 if bit 3 is already set. 275 if self.fsType & 0x0008: 276 self.fsType &= ~0x0004 277 return self 278 279ttLib.getTableClass('post').mergeMap = { 280 '*': first, 281 'tableTag': equal, 282 'formatType': max, 283 'isFixedPitch': min, 284 'minMemType42': max, 285 'maxMemType42': lambda lst: 0, 286 'minMemType1': max, 287 'maxMemType1': lambda lst: 0, 288 'mapping': implementedFilter(sumDicts), 289 'extraNames': lambda lst: [], 290} 291 292ttLib.getTableClass('vmtx').mergeMap = ttLib.getTableClass('hmtx').mergeMap = { 293 'tableTag': equal, 294 'metrics': sumDicts, 295} 296 297ttLib.getTableClass('gasp').mergeMap = { 298 'tableTag': equal, 299 'version': max, 300 'gaspRange': first, # FIXME? Appears irreconcilable 301} 302 303ttLib.getTableClass('name').mergeMap = { 304 'tableTag': equal, 305 'names': first, # FIXME? Does mixing name records make sense? 306} 307 308ttLib.getTableClass('loca').mergeMap = { 309 '*': recalculate, 310 'tableTag': equal, 311} 312 313ttLib.getTableClass('glyf').mergeMap = { 314 'tableTag': equal, 315 'glyphs': sumDicts, 316 'glyphOrder': sumLists, 317} 318 319@_add_method(ttLib.getTableClass('glyf')) 320def merge(self, m, tables): 321 for i,table in enumerate(tables): 322 for g in table.glyphs.values(): 323 if i: 324 # Drop hints for all but first font, since 325 # we don't map functions / CVT values. 326 g.removeHinting() 327 # Expand composite glyphs to load their 328 # composite glyph names. 329 if g.isComposite(): 330 g.expand(table) 331 return DefaultTable.merge(self, m, tables) 332 333ttLib.getTableClass('prep').mergeMap = lambda self, lst: first(lst) 334ttLib.getTableClass('fpgm').mergeMap = lambda self, lst: first(lst) 335ttLib.getTableClass('cvt ').mergeMap = lambda self, lst: first(lst) 336 337@_add_method(ttLib.getTableClass('cmap')) 338def merge(self, m, tables): 339 # TODO Handle format=14. 340 cmapTables = [t for table in tables for t in table.tables 341 if t.isUnicode()] 342 # TODO Better handle format-4 and format-12 coexisting in same font. 343 # TODO Insert both a format-4 and format-12 if needed. 344 module = ttLib.getTableModule('cmap') 345 assert all(t.format in [4, 12] for t in cmapTables) 346 format = max(t.format for t in cmapTables) 347 cmapTable = module.cmap_classes[format](format) 348 cmapTable.cmap = {} 349 cmapTable.platformID = 3 350 cmapTable.platEncID = max(t.platEncID for t in cmapTables) 351 cmapTable.language = 0 352 for table in cmapTables: 353 # TODO handle duplicates. 354 cmapTable.cmap.update(table.cmap) 355 self.tableVersion = 0 356 self.tables = [cmapTable] 357 self.numSubTables = len(self.tables) 358 return self 359 360 361otTables.ScriptList.mergeMap = { 362 'ScriptCount': sum, 363 'ScriptRecord': lambda lst: sorted(sumLists(lst), key=lambda s: s.ScriptTag), 364} 365 366otTables.FeatureList.mergeMap = { 367 'FeatureCount': sum, 368 'FeatureRecord': sumLists, 369} 370 371otTables.LookupList.mergeMap = { 372 'LookupCount': sum, 373 'Lookup': sumLists, 374} 375 376otTables.Coverage.mergeMap = { 377 'glyphs': sumLists, 378} 379 380otTables.ClassDef.mergeMap = { 381 'classDefs': sumDicts, 382} 383 384otTables.LigCaretList.mergeMap = { 385 'Coverage': mergeObjects, 386 'LigGlyphCount': sum, 387 'LigGlyph': sumLists, 388} 389 390otTables.AttachList.mergeMap = { 391 'Coverage': mergeObjects, 392 'GlyphCount': sum, 393 'AttachPoint': sumLists, 394} 395 396# XXX Renumber MarkFilterSets of lookups 397otTables.MarkGlyphSetsDef.mergeMap = { 398 'MarkSetTableFormat': equal, 399 'MarkSetCount': sum, 400 'Coverage': sumLists, 401} 402 403otTables.GDEF.mergeMap = { 404 '*': mergeObjects, 405 'Version': max, 406} 407 408otTables.GSUB.mergeMap = otTables.GPOS.mergeMap = { 409 '*': mergeObjects, 410 'Version': max, 411} 412 413ttLib.getTableClass('GDEF').mergeMap = \ 414ttLib.getTableClass('GSUB').mergeMap = \ 415ttLib.getTableClass('GPOS').mergeMap = \ 416ttLib.getTableClass('BASE').mergeMap = \ 417ttLib.getTableClass('JSTF').mergeMap = \ 418ttLib.getTableClass('MATH').mergeMap = \ 419{ 420 'tableTag': equal, 421 'table': mergeObjects, 422} 423 424 425@_add_method(otTables.SingleSubst, 426 otTables.MultipleSubst, 427 otTables.AlternateSubst, 428 otTables.LigatureSubst, 429 otTables.ReverseChainSingleSubst, 430 otTables.SinglePos, 431 otTables.PairPos, 432 otTables.CursivePos, 433 otTables.MarkBasePos, 434 otTables.MarkLigPos, 435 otTables.MarkMarkPos) 436def mapLookups(self, lookupMap): 437 pass 438 439# Copied and trimmed down from subset.py 440@_add_method(otTables.ContextSubst, 441 otTables.ChainContextSubst, 442 otTables.ContextPos, 443 otTables.ChainContextPos) 444def __classify_context(self): 445 446 class ContextHelper(object): 447 def __init__(self, klass, Format): 448 if klass.__name__.endswith('Subst'): 449 Typ = 'Sub' 450 Type = 'Subst' 451 else: 452 Typ = 'Pos' 453 Type = 'Pos' 454 if klass.__name__.startswith('Chain'): 455 Chain = 'Chain' 456 else: 457 Chain = '' 458 ChainTyp = Chain+Typ 459 460 self.Typ = Typ 461 self.Type = Type 462 self.Chain = Chain 463 self.ChainTyp = ChainTyp 464 465 self.LookupRecord = Type+'LookupRecord' 466 467 if Format == 1: 468 self.Rule = ChainTyp+'Rule' 469 self.RuleSet = ChainTyp+'RuleSet' 470 elif Format == 2: 471 self.Rule = ChainTyp+'ClassRule' 472 self.RuleSet = ChainTyp+'ClassSet' 473 474 if self.Format not in [1, 2, 3]: 475 return None # Don't shoot the messenger; let it go 476 if not hasattr(self.__class__, "__ContextHelpers"): 477 self.__class__.__ContextHelpers = {} 478 if self.Format not in self.__class__.__ContextHelpers: 479 helper = ContextHelper(self.__class__, self.Format) 480 self.__class__.__ContextHelpers[self.Format] = helper 481 return self.__class__.__ContextHelpers[self.Format] 482 483 484@_add_method(otTables.ContextSubst, 485 otTables.ChainContextSubst, 486 otTables.ContextPos, 487 otTables.ChainContextPos) 488def mapLookups(self, lookupMap): 489 c = self.__classify_context() 490 491 if self.Format in [1, 2]: 492 for rs in getattr(self, c.RuleSet): 493 if not rs: continue 494 for r in getattr(rs, c.Rule): 495 if not r: continue 496 for ll in getattr(r, c.LookupRecord): 497 if not ll: continue 498 ll.LookupListIndex = lookupMap[ll.LookupListIndex] 499 elif self.Format == 3: 500 for ll in getattr(self, c.LookupRecord): 501 if not ll: continue 502 ll.LookupListIndex = lookupMap[ll.LookupListIndex] 503 else: 504 assert 0, "unknown format: %s" % self.Format 505 506@_add_method(otTables.Lookup) 507def mapLookups(self, lookupMap): 508 for st in self.SubTable: 509 if not st: continue 510 st.mapLookups(lookupMap) 511 512@_add_method(otTables.LookupList) 513def mapLookups(self, lookupMap): 514 for l in self.Lookup: 515 if not l: continue 516 l.mapLookups(lookupMap) 517 518@_add_method(otTables.Feature) 519def mapLookups(self, lookupMap): 520 self.LookupListIndex = [lookupMap[i] for i in self.LookupListIndex] 521 522@_add_method(otTables.FeatureList) 523def mapLookups(self, lookupMap): 524 for f in self.FeatureRecord: 525 if not f or not f.Feature: continue 526 f.Feature.mapLookups(lookupMap) 527 528@_add_method(otTables.DefaultLangSys, 529 otTables.LangSys) 530def mapFeatures(self, featureMap): 531 self.FeatureIndex = [featureMap[i] for i in self.FeatureIndex] 532 if self.ReqFeatureIndex != 65535: 533 self.ReqFeatureIndex = featureMap[self.ReqFeatureIndex] 534 535@_add_method(otTables.Script) 536def mapFeatures(self, featureMap): 537 if self.DefaultLangSys: 538 self.DefaultLangSys.mapFeatures(featureMap) 539 for l in self.LangSysRecord: 540 if not l or not l.LangSys: continue 541 l.LangSys.mapFeatures(featureMap) 542 543@_add_method(otTables.ScriptList) 544def mapFeatures(self, featureMap): 545 for s in self.ScriptRecord: 546 if not s or not s.Script: continue 547 s.Script.mapFeatures(featureMap) 548 549 550class Options(object): 551 552 class UnknownOptionError(Exception): 553 pass 554 555 def __init__(self, **kwargs): 556 557 self.set(**kwargs) 558 559 def set(self, **kwargs): 560 for k,v in kwargs.items(): 561 if not hasattr(self, k): 562 raise self.UnknownOptionError("Unknown option '%s'" % k) 563 setattr(self, k, v) 564 565 def parse_opts(self, argv, ignore_unknown=False): 566 ret = [] 567 opts = {} 568 for a in argv: 569 orig_a = a 570 if not a.startswith('--'): 571 ret.append(a) 572 continue 573 a = a[2:] 574 i = a.find('=') 575 op = '=' 576 if i == -1: 577 if a.startswith("no-"): 578 k = a[3:] 579 v = False 580 else: 581 k = a 582 v = True 583 else: 584 k = a[:i] 585 if k[-1] in "-+": 586 op = k[-1]+'=' # Ops is '-=' or '+=' now. 587 k = k[:-1] 588 v = a[i+1:] 589 k = k.replace('-', '_') 590 if not hasattr(self, k): 591 if ignore_unknown == True or k in ignore_unknown: 592 ret.append(orig_a) 593 continue 594 else: 595 raise self.UnknownOptionError("Unknown option '%s'" % a) 596 597 ov = getattr(self, k) 598 if isinstance(ov, bool): 599 v = bool(v) 600 elif isinstance(ov, int): 601 v = int(v) 602 elif isinstance(ov, list): 603 vv = v.split(',') 604 if vv == ['']: 605 vv = [] 606 vv = [int(x, 0) if len(x) and x[0] in "0123456789" else x for x in vv] 607 if op == '=': 608 v = vv 609 elif op == '+=': 610 v = ov 611 v.extend(vv) 612 elif op == '-=': 613 v = ov 614 for x in vv: 615 if x in v: 616 v.remove(x) 617 else: 618 assert 0 619 620 opts[k] = v 621 self.set(**opts) 622 623 return ret 624 625 626class Merger(object): 627 628 def __init__(self, options=None, log=None): 629 630 if not log: 631 log = Logger() 632 if not options: 633 options = Options() 634 635 self.options = options 636 self.log = log 637 638 def merge(self, fontfiles): 639 640 mega = ttLib.TTFont() 641 642 # 643 # Settle on a mega glyph order. 644 # 645 fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles] 646 glyphOrders = [font.getGlyphOrder() for font in fonts] 647 megaGlyphOrder = self._mergeGlyphOrders(glyphOrders) 648 # Reload fonts and set new glyph names on them. 649 # TODO Is it necessary to reload font? I think it is. At least 650 # it's safer, in case tables were loaded to provide glyph names. 651 fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles] 652 for font,glyphOrder in zip(fonts, glyphOrders): 653 font.setGlyphOrder(glyphOrder) 654 mega.setGlyphOrder(megaGlyphOrder) 655 656 for font in fonts: 657 self._preMerge(font) 658 659 allTags = reduce(set.union, (list(font.keys()) for font in fonts), set()) 660 allTags.remove('GlyphOrder') 661 for tag in allTags: 662 663 clazz = ttLib.getTableClass(tag) 664 665 tables = [font.get(tag, NotImplemented) for font in fonts] 666 table = clazz(tag).merge(self, tables) 667 if table is not NotImplemented and table is not False: 668 mega[tag] = table 669 self.log("Merged '%s'." % tag) 670 else: 671 self.log("Dropped '%s'." % tag) 672 self.log.lapse("merge '%s'" % tag) 673 674 self._postMerge(mega) 675 676 return mega 677 678 def _mergeGlyphOrders(self, glyphOrders): 679 """Modifies passed-in glyphOrders to reflect new glyph names. 680 Returns glyphOrder for the merged font.""" 681 # Simply append font index to the glyph name for now. 682 # TODO Even this simplistic numbering can result in conflicts. 683 # But then again, we have to improve this soon anyway. 684 mega = [] 685 for n,glyphOrder in enumerate(glyphOrders): 686 for i,glyphName in enumerate(glyphOrder): 687 glyphName += "#" + repr(n) 688 glyphOrder[i] = glyphName 689 mega.append(glyphName) 690 return mega 691 692 def mergeObjects(self, returnTable, logic, tables): 693 # Right now we don't use self at all. Will use in the future 694 # for options and logging. 695 696 if logic is NotImplemented: 697 return NotImplemented 698 699 allKeys = set.union(set(), *(vars(table).keys() for table in tables if table is not NotImplemented)) 700 for key in allKeys: 701 try: 702 mergeLogic = logic[key] 703 except KeyError: 704 try: 705 mergeLogic = logic['*'] 706 except KeyError: 707 raise Exception("Don't know how to merge key %s of class %s" % 708 (key, returnTable.__class__.__name__)) 709 if mergeLogic is NotImplemented: 710 continue 711 value = mergeLogic(getattr(table, key, NotImplemented) for table in tables) 712 if value is not NotImplemented: 713 setattr(returnTable, key, value) 714 715 return returnTable 716 717 def _preMerge(self, font): 718 719 GDEF = font.get('GDEF') 720 GSUB = font.get('GSUB') 721 GPOS = font.get('GPOS') 722 723 for t in [GSUB, GPOS]: 724 if not t: continue 725 726 if t.table.LookupList: 727 lookupMap = {i:id(v) for i,v in enumerate(t.table.LookupList.Lookup)} 728 t.table.LookupList.mapLookups(lookupMap) 729 if t.table.FeatureList: 730 # XXX Handle present FeatureList but absent LookupList 731 t.table.FeatureList.mapLookups(lookupMap) 732 733 if t.table.FeatureList and t.table.ScriptList: 734 featureMap = {i:id(v) for i,v in enumerate(t.table.FeatureList.FeatureRecord)} 735 t.table.ScriptList.mapFeatures(featureMap) 736 737 # TODO GDEF/Lookup MarkFilteringSets 738 # TODO FeatureParams nameIDs 739 740 def _postMerge(self, font): 741 742 GDEF = font.get('GDEF') 743 GSUB = font.get('GSUB') 744 GPOS = font.get('GPOS') 745 746 for t in [GSUB, GPOS]: 747 if not t: continue 748 749 if t.table.LookupList: 750 lookupMap = {id(v):i for i,v in enumerate(t.table.LookupList.Lookup)} 751 t.table.LookupList.mapLookups(lookupMap) 752 if t.table.FeatureList: 753 # XXX Handle present FeatureList but absent LookupList 754 t.table.FeatureList.mapLookups(lookupMap) 755 756 if t.table.FeatureList and t.table.ScriptList: 757 # XXX Handle present ScriptList but absent FeatureList 758 featureMap = {id(v):i for i,v in enumerate(t.table.FeatureList.FeatureRecord)} 759 t.table.ScriptList.mapFeatures(featureMap) 760 761 # TODO GDEF/Lookup MarkFilteringSets 762 # TODO FeatureParams nameIDs 763 764 765class Logger(object): 766 767 def __init__(self, verbose=False, xml=False, timing=False): 768 self.verbose = verbose 769 self.xml = xml 770 self.timing = timing 771 self.last_time = self.start_time = time.time() 772 773 def parse_opts(self, argv): 774 argv = argv[:] 775 for v in ['verbose', 'xml', 'timing']: 776 if "--"+v in argv: 777 setattr(self, v, True) 778 argv.remove("--"+v) 779 return argv 780 781 def __call__(self, *things): 782 if not self.verbose: 783 return 784 print(' '.join(str(x) for x in things)) 785 786 def lapse(self, *things): 787 if not self.timing: 788 return 789 new_time = time.time() 790 print("Took %0.3fs to %s" %(new_time - self.last_time, 791 ' '.join(str(x) for x in things))) 792 self.last_time = new_time 793 794 def font(self, font, file=sys.stdout): 795 if not self.xml: 796 return 797 from fontTools.misc import xmlWriter 798 writer = xmlWriter.XMLWriter(file) 799 font.disassembleInstructions = False # Work around ttLib bug 800 for tag in font.keys(): 801 writer.begintag(tag) 802 writer.newline() 803 font[tag].toXML(writer, font) 804 writer.endtag(tag) 805 writer.newline() 806 807 808__all__ = [ 809 'Options', 810 'Merger', 811 'Logger', 812 'main' 813] 814 815def main(args): 816 817 log = Logger() 818 args = log.parse_opts(args) 819 820 options = Options() 821 args = options.parse_opts(args) 822 823 if len(args) < 1: 824 print("usage: pyftmerge font...", file=sys.stderr) 825 sys.exit(1) 826 827 merger = Merger(options=options, log=log) 828 font = merger.merge(args) 829 outfile = 'merged.ttf' 830 font.save(outfile) 831 log.lapse("compile and save font") 832 833 log.last_time = log.start_time 834 log.lapse("make one with everything(TOTAL TIME)") 835 836if __name__ == "__main__": 837 main(sys.argv[1:]) 838