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