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.misc.timeTools import timestampNow 11from fontTools import ttLib, cffLib 12from fontTools.ttLib.tables import otTables, _h_e_a_d 13from fontTools.ttLib.tables.DefaultTable import DefaultTable 14from fontTools.misc.loggingTools import Timer 15from fontTools.pens.recordingPen import DecomposingRecordingPen 16from functools import reduce 17import sys 18import time 19import operator 20import logging 21 22 23log = logging.getLogger("fontTools.merge") 24timer = Timer(logger=logging.getLogger(__name__+".timer"), level=logging.INFO) 25 26 27def _add_method(*clazzes, **kwargs): 28 """Returns a decorator function that adds a new method to one or 29 more classes.""" 30 allowDefault = kwargs.get('allowDefaultTable', False) 31 def wrapper(method): 32 done = [] 33 for clazz in clazzes: 34 if clazz in done: continue # Support multiple names of a clazz 35 done.append(clazz) 36 assert allowDefault or clazz != DefaultTable, 'Oops, table class not found.' 37 assert method.__name__ not in clazz.__dict__, \ 38 "Oops, class '%s' has method '%s'." % (clazz.__name__, method.__name__) 39 setattr(clazz, method.__name__, method) 40 return None 41 return wrapper 42 43# General utility functions for merging values from different fonts 44 45def equal(lst): 46 lst = list(lst) 47 t = iter(lst) 48 first = next(t) 49 assert all(item == first for item in t), "Expected all items to be equal: %s" % lst 50 return first 51 52def first(lst): 53 return next(iter(lst)) 54 55def recalculate(lst): 56 return NotImplemented 57 58def current_time(lst): 59 return timestampNow() 60 61def bitwise_and(lst): 62 return reduce(operator.and_, lst) 63 64def bitwise_or(lst): 65 return reduce(operator.or_, lst) 66 67def avg_int(lst): 68 lst = list(lst) 69 return sum(lst) // len(lst) 70 71def onlyExisting(func): 72 """Returns a filter func that when called with a list, 73 only calls func on the non-NotImplemented items of the list, 74 and only so if there's at least one item remaining. 75 Otherwise returns NotImplemented.""" 76 77 def wrapper(lst): 78 items = [item for item in lst if item is not NotImplemented] 79 return func(items) if items else NotImplemented 80 81 return wrapper 82 83def sumLists(lst): 84 l = [] 85 for item in lst: 86 l.extend(item) 87 return l 88 89def sumDicts(lst): 90 d = {} 91 for item in lst: 92 d.update(item) 93 return d 94 95def mergeObjects(lst): 96 lst = [item for item in lst if item is not NotImplemented] 97 if not lst: 98 return NotImplemented 99 lst = [item for item in lst if item is not None] 100 if not lst: 101 return None 102 103 clazz = lst[0].__class__ 104 assert all(type(item) == clazz for item in lst), lst 105 106 logic = clazz.mergeMap 107 returnTable = clazz() 108 returnDict = {} 109 110 allKeys = set.union(set(), *(vars(table).keys() for table in lst)) 111 for key in allKeys: 112 try: 113 mergeLogic = logic[key] 114 except KeyError: 115 try: 116 mergeLogic = logic['*'] 117 except KeyError: 118 raise Exception("Don't know how to merge key %s of class %s" % 119 (key, clazz.__name__)) 120 if mergeLogic is NotImplemented: 121 continue 122 value = mergeLogic(getattr(table, key, NotImplemented) for table in lst) 123 if value is not NotImplemented: 124 returnDict[key] = value 125 126 returnTable.__dict__ = returnDict 127 128 return returnTable 129 130def mergeBits(bitmap): 131 132 def wrapper(lst): 133 lst = list(lst) 134 returnValue = 0 135 for bitNumber in range(bitmap['size']): 136 try: 137 mergeLogic = bitmap[bitNumber] 138 except KeyError: 139 try: 140 mergeLogic = bitmap['*'] 141 except KeyError: 142 raise Exception("Don't know how to merge bit %s" % bitNumber) 143 shiftedBit = 1 << bitNumber 144 mergedValue = mergeLogic(bool(item & shiftedBit) for item in lst) 145 returnValue |= mergedValue << bitNumber 146 return returnValue 147 148 return wrapper 149 150 151@_add_method(DefaultTable, allowDefaultTable=True) 152def merge(self, m, tables): 153 if not hasattr(self, 'mergeMap'): 154 log.info("Don't know how to merge '%s'.", self.tableTag) 155 return NotImplemented 156 157 logic = self.mergeMap 158 159 if isinstance(logic, dict): 160 return m.mergeObjects(self, self.mergeMap, tables) 161 else: 162 return logic(tables) 163 164 165ttLib.getTableClass('maxp').mergeMap = { 166 '*': max, 167 'tableTag': equal, 168 'tableVersion': equal, 169 'numGlyphs': sum, 170 'maxStorage': first, 171 'maxFunctionDefs': first, 172 'maxInstructionDefs': first, 173 # TODO When we correctly merge hinting data, update these values: 174 # maxFunctionDefs, maxInstructionDefs, maxSizeOfInstructions 175} 176 177headFlagsMergeBitMap = { 178 'size': 16, 179 '*': bitwise_or, 180 1: bitwise_and, # Baseline at y = 0 181 2: bitwise_and, # lsb at x = 0 182 3: bitwise_and, # Force ppem to integer values. FIXME? 183 5: bitwise_and, # Font is vertical 184 6: lambda bit: 0, # Always set to zero 185 11: bitwise_and, # Font data is 'lossless' 186 13: bitwise_and, # Optimized for ClearType 187 14: bitwise_and, # Last resort font. FIXME? equal or first may be better 188 15: lambda bit: 0, # Always set to zero 189} 190 191ttLib.getTableClass('head').mergeMap = { 192 'tableTag': equal, 193 'tableVersion': max, 194 'fontRevision': max, 195 'checkSumAdjustment': lambda lst: 0, # We need *something* here 196 'magicNumber': equal, 197 'flags': mergeBits(headFlagsMergeBitMap), 198 'unitsPerEm': equal, 199 'created': current_time, 200 'modified': current_time, 201 'xMin': min, 202 'yMin': min, 203 'xMax': max, 204 'yMax': max, 205 'macStyle': first, 206 'lowestRecPPEM': max, 207 'fontDirectionHint': lambda lst: 2, 208 'indexToLocFormat': recalculate, 209 'glyphDataFormat': equal, 210} 211 212ttLib.getTableClass('hhea').mergeMap = { 213 '*': equal, 214 'tableTag': equal, 215 'tableVersion': max, 216 'ascent': max, 217 'descent': min, 218 'lineGap': max, 219 'advanceWidthMax': max, 220 'minLeftSideBearing': min, 221 'minRightSideBearing': min, 222 'xMaxExtent': max, 223 'caretSlopeRise': first, 224 'caretSlopeRun': first, 225 'caretOffset': first, 226 'numberOfHMetrics': recalculate, 227} 228 229ttLib.getTableClass('vhea').mergeMap = { 230 '*': equal, 231 'tableTag': equal, 232 'tableVersion': max, 233 'ascent': max, 234 'descent': min, 235 'lineGap': max, 236 'advanceHeightMax': max, 237 'minTopSideBearing': min, 238 'minBottomSideBearing': min, 239 'yMaxExtent': max, 240 'caretSlopeRise': first, 241 'caretSlopeRun': first, 242 'caretOffset': first, 243 'numberOfVMetrics': recalculate, 244} 245 246os2FsTypeMergeBitMap = { 247 'size': 16, 248 '*': lambda bit: 0, 249 1: bitwise_or, # no embedding permitted 250 2: bitwise_and, # allow previewing and printing documents 251 3: bitwise_and, # allow editing documents 252 8: bitwise_or, # no subsetting permitted 253 9: bitwise_or, # no embedding of outlines permitted 254} 255 256def mergeOs2FsType(lst): 257 lst = list(lst) 258 if all(item == 0 for item in lst): 259 return 0 260 261 # Compute least restrictive logic for each fsType value 262 for i in range(len(lst)): 263 # unset bit 1 (no embedding permitted) if either bit 2 or 3 is set 264 if lst[i] & 0x000C: 265 lst[i] &= ~0x0002 266 # set bit 2 (allow previewing) if bit 3 is set (allow editing) 267 elif lst[i] & 0x0008: 268 lst[i] |= 0x0004 269 # set bits 2 and 3 if everything is allowed 270 elif lst[i] == 0: 271 lst[i] = 0x000C 272 273 fsType = mergeBits(os2FsTypeMergeBitMap)(lst) 274 # unset bits 2 and 3 if bit 1 is set (some font is "no embedding") 275 if fsType & 0x0002: 276 fsType &= ~0x000C 277 return fsType 278 279 280ttLib.getTableClass('OS/2').mergeMap = { 281 '*': first, 282 'tableTag': equal, 283 'version': max, 284 'xAvgCharWidth': avg_int, # Apparently fontTools doesn't recalc this 285 'fsType': mergeOs2FsType, # Will be overwritten 286 'panose': first, # FIXME: should really be the first Latin font 287 'ulUnicodeRange1': bitwise_or, 288 'ulUnicodeRange2': bitwise_or, 289 'ulUnicodeRange3': bitwise_or, 290 'ulUnicodeRange4': bitwise_or, 291 'fsFirstCharIndex': min, 292 'fsLastCharIndex': max, 293 'sTypoAscender': max, 294 'sTypoDescender': min, 295 'sTypoLineGap': max, 296 'usWinAscent': max, 297 'usWinDescent': max, 298 # Version 2,3,4 299 'ulCodePageRange1': onlyExisting(bitwise_or), 300 'ulCodePageRange2': onlyExisting(bitwise_or), 301 'usMaxContex': onlyExisting(max), 302 # TODO version 5 303} 304 305@_add_method(ttLib.getTableClass('OS/2')) 306def merge(self, m, tables): 307 DefaultTable.merge(self, m, tables) 308 if self.version < 2: 309 # bits 8 and 9 are reserved and should be set to zero 310 self.fsType &= ~0x0300 311 if self.version >= 3: 312 # Only one of bits 1, 2, and 3 may be set. We already take 313 # care of bit 1 implications in mergeOs2FsType. So unset 314 # bit 2 if bit 3 is already set. 315 if self.fsType & 0x0008: 316 self.fsType &= ~0x0004 317 return self 318 319ttLib.getTableClass('post').mergeMap = { 320 '*': first, 321 'tableTag': equal, 322 'formatType': max, 323 'isFixedPitch': min, 324 'minMemType42': max, 325 'maxMemType42': lambda lst: 0, 326 'minMemType1': max, 327 'maxMemType1': lambda lst: 0, 328 'mapping': onlyExisting(sumDicts), 329 'extraNames': lambda lst: [], 330} 331 332ttLib.getTableClass('vmtx').mergeMap = ttLib.getTableClass('hmtx').mergeMap = { 333 'tableTag': equal, 334 'metrics': sumDicts, 335} 336 337ttLib.getTableClass('name').mergeMap = { 338 'tableTag': equal, 339 'names': first, # FIXME? Does mixing name records make sense? 340} 341 342ttLib.getTableClass('loca').mergeMap = { 343 '*': recalculate, 344 'tableTag': equal, 345} 346 347ttLib.getTableClass('glyf').mergeMap = { 348 'tableTag': equal, 349 'glyphs': sumDicts, 350 'glyphOrder': sumLists, 351} 352 353@_add_method(ttLib.getTableClass('glyf')) 354def merge(self, m, tables): 355 for i,table in enumerate(tables): 356 for g in table.glyphs.values(): 357 if i: 358 # Drop hints for all but first font, since 359 # we don't map functions / CVT values. 360 g.removeHinting() 361 # Expand composite glyphs to load their 362 # composite glyph names. 363 if g.isComposite(): 364 g.expand(table) 365 return DefaultTable.merge(self, m, tables) 366 367ttLib.getTableClass('prep').mergeMap = lambda self, lst: first(lst) 368ttLib.getTableClass('fpgm').mergeMap = lambda self, lst: first(lst) 369ttLib.getTableClass('cvt ').mergeMap = lambda self, lst: first(lst) 370ttLib.getTableClass('gasp').mergeMap = lambda self, lst: first(lst) # FIXME? Appears irreconcilable 371 372def _glyphsAreSame(glyphSet1, glyphSet2, glyph1, glyph2): 373 pen1 = DecomposingRecordingPen(glyphSet1) 374 pen2 = DecomposingRecordingPen(glyphSet2) 375 g1 = glyphSet1[glyph1] 376 g2 = glyphSet2[glyph2] 377 g1.draw(pen1) 378 g2.draw(pen2) 379 return (pen1.value == pen2.value and 380 g1.width == g2.width and 381 (not hasattr(g1, 'height') or g1.height == g2.height)) 382 383# Valid (format, platformID, platEncID) triplets for cmap subtables containing 384# Unicode BMP-only and Unicode Full Repertoire semantics. 385# Cf. OpenType spec for "Platform specific encodings": 386# https://docs.microsoft.com/en-us/typography/opentype/spec/name 387class CmapUnicodePlatEncodings: 388 BMP = {(4, 3, 1), (4, 0, 3), (4, 0, 4), (4, 0, 6)} 389 FullRepertoire = {(12, 3, 10), (12, 0, 4), (12, 0, 6)} 390 391@_add_method(ttLib.getTableClass('cmap')) 392def merge(self, m, tables): 393 # TODO Handle format=14. 394 # Only merge format 4 and 12 Unicode subtables, ignores all other subtables 395 # If there is a format 12 table for the same font, ignore the format 4 table 396 cmapTables = [] 397 for fontIdx,table in enumerate(tables): 398 format4 = None 399 format12 = None 400 for subtable in table.tables: 401 properties = (subtable.format, subtable.platformID, subtable.platEncID) 402 if properties in CmapUnicodePlatEncodings.BMP: 403 format4 = subtable 404 elif properties in CmapUnicodePlatEncodings.FullRepertoire: 405 format12 = subtable 406 else: 407 log.warning( 408 "Dropped cmap subtable from font [%s]:\t" 409 "format %2s, platformID %2s, platEncID %2s", 410 fontIdx, subtable.format, subtable.platformID, subtable.platEncID 411 ) 412 if format12 is not None: 413 cmapTables.append((format12, fontIdx)) 414 elif format4 is not None: 415 cmapTables.append((format4, fontIdx)) 416 417 # Build a unicode mapping, then decide which format is needed to store it. 418 cmap = {} 419 fontIndexForGlyph = {} 420 glyphSets = [None for f in m.fonts] if hasattr(m, 'fonts') else None 421 for table,fontIdx in cmapTables: 422 # handle duplicates 423 for uni,gid in table.cmap.items(): 424 oldgid = cmap.get(uni, None) 425 if oldgid is None: 426 cmap[uni] = gid 427 fontIndexForGlyph[gid] = fontIdx 428 elif oldgid != gid: 429 # Char previously mapped to oldgid, now to gid. 430 # Record, to fix up in GSUB 'locl' later. 431 if m.duplicateGlyphsPerFont[fontIdx].get(oldgid) is None: 432 if glyphSets is not None: 433 oldFontIdx = fontIndexForGlyph[oldgid] 434 for idx in (fontIdx, oldFontIdx): 435 if glyphSets[idx] is None: 436 glyphSets[idx] = m.fonts[idx].getGlyphSet() 437 if _glyphsAreSame(glyphSets[oldFontIdx], glyphSets[fontIdx], oldgid, gid): 438 continue 439 m.duplicateGlyphsPerFont[fontIdx][oldgid] = gid 440 elif m.duplicateGlyphsPerFont[fontIdx][oldgid] != gid: 441 # Char previously mapped to oldgid but oldgid is already remapped to a different 442 # gid, because of another Unicode character. 443 # TODO: Try harder to do something about these. 444 log.warning("Dropped mapping from codepoint %#06X to glyphId '%s'", uni, gid) 445 446 cmapBmpOnly = {uni: gid for uni,gid in cmap.items() if uni <= 0xFFFF} 447 self.tables = [] 448 module = ttLib.getTableModule('cmap') 449 if len(cmapBmpOnly) != len(cmap): 450 # format-12 required. 451 cmapTable = module.cmap_classes[12](12) 452 cmapTable.platformID = 3 453 cmapTable.platEncID = 10 454 cmapTable.language = 0 455 cmapTable.cmap = cmap 456 self.tables.append(cmapTable) 457 # always create format-4 458 cmapTable = module.cmap_classes[4](4) 459 cmapTable.platformID = 3 460 cmapTable.platEncID = 1 461 cmapTable.language = 0 462 cmapTable.cmap = cmapBmpOnly 463 # ordered by platform then encoding 464 self.tables.insert(0, cmapTable) 465 self.tableVersion = 0 466 self.numSubTables = len(self.tables) 467 return self 468 469 470def mergeLookupLists(lst): 471 # TODO Do smarter merge. 472 return sumLists(lst) 473 474def mergeFeatures(lst): 475 assert lst 476 self = otTables.Feature() 477 self.FeatureParams = None 478 self.LookupListIndex = mergeLookupLists([l.LookupListIndex for l in lst if l.LookupListIndex]) 479 self.LookupCount = len(self.LookupListIndex) 480 return self 481 482def mergeFeatureLists(lst): 483 d = {} 484 for l in lst: 485 for f in l: 486 tag = f.FeatureTag 487 if tag not in d: 488 d[tag] = [] 489 d[tag].append(f.Feature) 490 ret = [] 491 for tag in sorted(d.keys()): 492 rec = otTables.FeatureRecord() 493 rec.FeatureTag = tag 494 rec.Feature = mergeFeatures(d[tag]) 495 ret.append(rec) 496 return ret 497 498def mergeLangSyses(lst): 499 assert lst 500 501 # TODO Support merging ReqFeatureIndex 502 assert all(l.ReqFeatureIndex == 0xFFFF for l in lst) 503 504 self = otTables.LangSys() 505 self.LookupOrder = None 506 self.ReqFeatureIndex = 0xFFFF 507 self.FeatureIndex = mergeFeatureLists([l.FeatureIndex for l in lst if l.FeatureIndex]) 508 self.FeatureCount = len(self.FeatureIndex) 509 return self 510 511def mergeScripts(lst): 512 assert lst 513 514 if len(lst) == 1: 515 return lst[0] 516 langSyses = {} 517 for sr in lst: 518 for lsr in sr.LangSysRecord: 519 if lsr.LangSysTag not in langSyses: 520 langSyses[lsr.LangSysTag] = [] 521 langSyses[lsr.LangSysTag].append(lsr.LangSys) 522 lsrecords = [] 523 for tag, langSys_list in sorted(langSyses.items()): 524 lsr = otTables.LangSysRecord() 525 lsr.LangSys = mergeLangSyses(langSys_list) 526 lsr.LangSysTag = tag 527 lsrecords.append(lsr) 528 529 self = otTables.Script() 530 self.LangSysRecord = lsrecords 531 self.LangSysCount = len(lsrecords) 532 dfltLangSyses = [s.DefaultLangSys for s in lst if s.DefaultLangSys] 533 if dfltLangSyses: 534 self.DefaultLangSys = mergeLangSyses(dfltLangSyses) 535 else: 536 self.DefaultLangSys = None 537 return self 538 539def mergeScriptRecords(lst): 540 d = {} 541 for l in lst: 542 for s in l: 543 tag = s.ScriptTag 544 if tag not in d: 545 d[tag] = [] 546 d[tag].append(s.Script) 547 ret = [] 548 for tag in sorted(d.keys()): 549 rec = otTables.ScriptRecord() 550 rec.ScriptTag = tag 551 rec.Script = mergeScripts(d[tag]) 552 ret.append(rec) 553 return ret 554 555otTables.ScriptList.mergeMap = { 556 'ScriptCount': lambda lst: None, # TODO 557 'ScriptRecord': mergeScriptRecords, 558} 559otTables.BaseScriptList.mergeMap = { 560 'BaseScriptCount': lambda lst: None, # TODO 561 # TODO: Merge duplicate entries 562 'BaseScriptRecord': lambda lst: sorted(sumLists(lst), key=lambda s: s.BaseScriptTag), 563} 564 565otTables.FeatureList.mergeMap = { 566 'FeatureCount': sum, 567 'FeatureRecord': lambda lst: sorted(sumLists(lst), key=lambda s: s.FeatureTag), 568} 569 570otTables.LookupList.mergeMap = { 571 'LookupCount': sum, 572 'Lookup': sumLists, 573} 574 575otTables.Coverage.mergeMap = { 576 'Format': min, 577 'glyphs': sumLists, 578} 579 580otTables.ClassDef.mergeMap = { 581 'Format': min, 582 'classDefs': sumDicts, 583} 584 585otTables.LigCaretList.mergeMap = { 586 'Coverage': mergeObjects, 587 'LigGlyphCount': sum, 588 'LigGlyph': sumLists, 589} 590 591otTables.AttachList.mergeMap = { 592 'Coverage': mergeObjects, 593 'GlyphCount': sum, 594 'AttachPoint': sumLists, 595} 596 597# XXX Renumber MarkFilterSets of lookups 598otTables.MarkGlyphSetsDef.mergeMap = { 599 'MarkSetTableFormat': equal, 600 'MarkSetCount': sum, 601 'Coverage': sumLists, 602} 603 604otTables.Axis.mergeMap = { 605 '*': mergeObjects, 606} 607 608# XXX Fix BASE table merging 609otTables.BaseTagList.mergeMap = { 610 'BaseTagCount': sum, 611 'BaselineTag': sumLists, 612} 613 614otTables.GDEF.mergeMap = \ 615otTables.GSUB.mergeMap = \ 616otTables.GPOS.mergeMap = \ 617otTables.BASE.mergeMap = \ 618otTables.JSTF.mergeMap = \ 619otTables.MATH.mergeMap = \ 620{ 621 '*': mergeObjects, 622 'Version': max, 623} 624 625ttLib.getTableClass('GDEF').mergeMap = \ 626ttLib.getTableClass('GSUB').mergeMap = \ 627ttLib.getTableClass('GPOS').mergeMap = \ 628ttLib.getTableClass('BASE').mergeMap = \ 629ttLib.getTableClass('JSTF').mergeMap = \ 630ttLib.getTableClass('MATH').mergeMap = \ 631{ 632 'tableTag': onlyExisting(equal), # XXX clean me up 633 'table': mergeObjects, 634} 635 636@_add_method(ttLib.getTableClass('GSUB')) 637def merge(self, m, tables): 638 639 assert len(tables) == len(m.duplicateGlyphsPerFont) 640 for i,(table,dups) in enumerate(zip(tables, m.duplicateGlyphsPerFont)): 641 if not dups: continue 642 assert (table is not None and table is not NotImplemented), "Have duplicates to resolve for font %d but no GSUB: %s" % (i + 1, dups) 643 synthFeature = None 644 synthLookup = None 645 for script in table.table.ScriptList.ScriptRecord: 646 if script.ScriptTag == 'DFLT': continue # XXX 647 for langsys in [script.Script.DefaultLangSys] + [l.LangSys for l in script.Script.LangSysRecord]: 648 if langsys is None: continue # XXX Create! 649 feature = [v for v in langsys.FeatureIndex if v.FeatureTag == 'locl'] 650 assert len(feature) <= 1 651 if feature: 652 feature = feature[0] 653 else: 654 if not synthFeature: 655 synthFeature = otTables.FeatureRecord() 656 synthFeature.FeatureTag = 'locl' 657 f = synthFeature.Feature = otTables.Feature() 658 f.FeatureParams = None 659 f.LookupCount = 0 660 f.LookupListIndex = [] 661 langsys.FeatureIndex.append(synthFeature) 662 langsys.FeatureIndex.sort(key=lambda v: v.FeatureTag) 663 table.table.FeatureList.FeatureRecord.append(synthFeature) 664 table.table.FeatureList.FeatureCount += 1 665 feature = synthFeature 666 667 if not synthLookup: 668 subtable = otTables.SingleSubst() 669 subtable.mapping = dups 670 synthLookup = otTables.Lookup() 671 synthLookup.LookupFlag = 0 672 synthLookup.LookupType = 1 673 synthLookup.SubTableCount = 1 674 synthLookup.SubTable = [subtable] 675 if table.table.LookupList is None: 676 # mtiLib uses None as default value for LookupList, 677 # while feaLib points to an empty array with count 0 678 # TODO: make them do the same 679 table.table.LookupList = otTables.LookupList() 680 table.table.LookupList.Lookup = [] 681 table.table.LookupList.LookupCount = 0 682 table.table.LookupList.Lookup.append(synthLookup) 683 table.table.LookupList.LookupCount += 1 684 685 feature.Feature.LookupListIndex[:0] = [synthLookup] 686 feature.Feature.LookupCount += 1 687 688 DefaultTable.merge(self, m, tables) 689 return self 690 691@_add_method(otTables.SingleSubst, 692 otTables.MultipleSubst, 693 otTables.AlternateSubst, 694 otTables.LigatureSubst, 695 otTables.ReverseChainSingleSubst, 696 otTables.SinglePos, 697 otTables.PairPos, 698 otTables.CursivePos, 699 otTables.MarkBasePos, 700 otTables.MarkLigPos, 701 otTables.MarkMarkPos) 702def mapLookups(self, lookupMap): 703 pass 704 705# Copied and trimmed down from subset.py 706@_add_method(otTables.ContextSubst, 707 otTables.ChainContextSubst, 708 otTables.ContextPos, 709 otTables.ChainContextPos) 710def __merge_classify_context(self): 711 712 class ContextHelper(object): 713 def __init__(self, klass, Format): 714 if klass.__name__.endswith('Subst'): 715 Typ = 'Sub' 716 Type = 'Subst' 717 else: 718 Typ = 'Pos' 719 Type = 'Pos' 720 if klass.__name__.startswith('Chain'): 721 Chain = 'Chain' 722 else: 723 Chain = '' 724 ChainTyp = Chain+Typ 725 726 self.Typ = Typ 727 self.Type = Type 728 self.Chain = Chain 729 self.ChainTyp = ChainTyp 730 731 self.LookupRecord = Type+'LookupRecord' 732 733 if Format == 1: 734 self.Rule = ChainTyp+'Rule' 735 self.RuleSet = ChainTyp+'RuleSet' 736 elif Format == 2: 737 self.Rule = ChainTyp+'ClassRule' 738 self.RuleSet = ChainTyp+'ClassSet' 739 740 if self.Format not in [1, 2, 3]: 741 return None # Don't shoot the messenger; let it go 742 if not hasattr(self.__class__, "__ContextHelpers"): 743 self.__class__.__ContextHelpers = {} 744 if self.Format not in self.__class__.__ContextHelpers: 745 helper = ContextHelper(self.__class__, self.Format) 746 self.__class__.__ContextHelpers[self.Format] = helper 747 return self.__class__.__ContextHelpers[self.Format] 748 749 750@_add_method(otTables.ContextSubst, 751 otTables.ChainContextSubst, 752 otTables.ContextPos, 753 otTables.ChainContextPos) 754def mapLookups(self, lookupMap): 755 c = self.__merge_classify_context() 756 757 if self.Format in [1, 2]: 758 for rs in getattr(self, c.RuleSet): 759 if not rs: continue 760 for r in getattr(rs, c.Rule): 761 if not r: continue 762 for ll in getattr(r, c.LookupRecord): 763 if not ll: continue 764 ll.LookupListIndex = lookupMap[ll.LookupListIndex] 765 elif self.Format == 3: 766 for ll in getattr(self, c.LookupRecord): 767 if not ll: continue 768 ll.LookupListIndex = lookupMap[ll.LookupListIndex] 769 else: 770 assert 0, "unknown format: %s" % self.Format 771 772@_add_method(otTables.ExtensionSubst, 773 otTables.ExtensionPos) 774def mapLookups(self, lookupMap): 775 if self.Format == 1: 776 self.ExtSubTable.mapLookups(lookupMap) 777 else: 778 assert 0, "unknown format: %s" % self.Format 779 780@_add_method(otTables.Lookup) 781def mapLookups(self, lookupMap): 782 for st in self.SubTable: 783 if not st: continue 784 st.mapLookups(lookupMap) 785 786@_add_method(otTables.LookupList) 787def mapLookups(self, lookupMap): 788 for l in self.Lookup: 789 if not l: continue 790 l.mapLookups(lookupMap) 791 792@_add_method(otTables.Feature) 793def mapLookups(self, lookupMap): 794 self.LookupListIndex = [lookupMap[i] for i in self.LookupListIndex] 795 796@_add_method(otTables.FeatureList) 797def mapLookups(self, lookupMap): 798 for f in self.FeatureRecord: 799 if not f or not f.Feature: continue 800 f.Feature.mapLookups(lookupMap) 801 802@_add_method(otTables.DefaultLangSys, 803 otTables.LangSys) 804def mapFeatures(self, featureMap): 805 self.FeatureIndex = [featureMap[i] for i in self.FeatureIndex] 806 if self.ReqFeatureIndex != 65535: 807 self.ReqFeatureIndex = featureMap[self.ReqFeatureIndex] 808 809@_add_method(otTables.Script) 810def mapFeatures(self, featureMap): 811 if self.DefaultLangSys: 812 self.DefaultLangSys.mapFeatures(featureMap) 813 for l in self.LangSysRecord: 814 if not l or not l.LangSys: continue 815 l.LangSys.mapFeatures(featureMap) 816 817@_add_method(otTables.ScriptList) 818def mapFeatures(self, featureMap): 819 for s in self.ScriptRecord: 820 if not s or not s.Script: continue 821 s.Script.mapFeatures(featureMap) 822 823 824class Options(object): 825 826 class UnknownOptionError(Exception): 827 pass 828 829 def __init__(self, **kwargs): 830 831 self.verbose = False 832 self.timing = False 833 834 self.set(**kwargs) 835 836 def set(self, **kwargs): 837 for k,v in kwargs.items(): 838 if not hasattr(self, k): 839 raise self.UnknownOptionError("Unknown option '%s'" % k) 840 setattr(self, k, v) 841 842 def parse_opts(self, argv, ignore_unknown=[]): 843 ret = [] 844 opts = {} 845 for a in argv: 846 orig_a = a 847 if not a.startswith('--'): 848 ret.append(a) 849 continue 850 a = a[2:] 851 i = a.find('=') 852 op = '=' 853 if i == -1: 854 if a.startswith("no-"): 855 k = a[3:] 856 v = False 857 else: 858 k = a 859 v = True 860 else: 861 k = a[:i] 862 if k[-1] in "-+": 863 op = k[-1]+'=' # Ops is '-=' or '+=' now. 864 k = k[:-1] 865 v = a[i+1:] 866 k = k.replace('-', '_') 867 if not hasattr(self, k): 868 if ignore_unknown is True or k in ignore_unknown: 869 ret.append(orig_a) 870 continue 871 else: 872 raise self.UnknownOptionError("Unknown option '%s'" % a) 873 874 ov = getattr(self, k) 875 if isinstance(ov, bool): 876 v = bool(v) 877 elif isinstance(ov, int): 878 v = int(v) 879 elif isinstance(ov, list): 880 vv = v.split(',') 881 if vv == ['']: 882 vv = [] 883 vv = [int(x, 0) if len(x) and x[0] in "0123456789" else x for x in vv] 884 if op == '=': 885 v = vv 886 elif op == '+=': 887 v = ov 888 v.extend(vv) 889 elif op == '-=': 890 v = ov 891 for x in vv: 892 if x in v: 893 v.remove(x) 894 else: 895 assert 0 896 897 opts[k] = v 898 self.set(**opts) 899 900 return ret 901 902class _AttendanceRecordingIdentityDict(object): 903 """A dictionary-like object that records indices of items actually accessed 904 from a list.""" 905 906 def __init__(self, lst): 907 self.l = lst 908 self.d = {id(v):i for i,v in enumerate(lst)} 909 self.s = set() 910 911 def __getitem__(self, v): 912 self.s.add(self.d[id(v)]) 913 return v 914 915class _GregariousIdentityDict(object): 916 """A dictionary-like object that welcomes guests without reservations and 917 adds them to the end of the guest list.""" 918 919 def __init__(self, lst): 920 self.l = lst 921 self.s = set(id(v) for v in lst) 922 923 def __getitem__(self, v): 924 if id(v) not in self.s: 925 self.s.add(id(v)) 926 self.l.append(v) 927 return v 928 929class _NonhashableDict(object): 930 """A dictionary-like object mapping objects to values.""" 931 932 def __init__(self, keys, values=None): 933 if values is None: 934 self.d = {id(v):i for i,v in enumerate(keys)} 935 else: 936 self.d = {id(k):v for k,v in zip(keys, values)} 937 938 def __getitem__(self, k): 939 return self.d[id(k)] 940 941 def __setitem__(self, k, v): 942 self.d[id(k)] = v 943 944 def __delitem__(self, k): 945 del self.d[id(k)] 946 947class Merger(object): 948 949 def __init__(self, options=None): 950 951 if not options: 952 options = Options() 953 954 self.options = options 955 956 def merge(self, fontfiles): 957 958 mega = ttLib.TTFont() 959 960 # 961 # Settle on a mega glyph order. 962 # 963 fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles] 964 glyphOrders = [font.getGlyphOrder() for font in fonts] 965 megaGlyphOrder = self._mergeGlyphOrders(glyphOrders) 966 # Reload fonts and set new glyph names on them. 967 # TODO Is it necessary to reload font? I think it is. At least 968 # it's safer, in case tables were loaded to provide glyph names. 969 fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles] 970 for font,glyphOrder in zip(fonts, glyphOrders): 971 font.setGlyphOrder(glyphOrder) 972 mega.setGlyphOrder(megaGlyphOrder) 973 974 for font in fonts: 975 self._preMerge(font) 976 977 self.fonts = fonts 978 self.duplicateGlyphsPerFont = [{} for f in fonts] 979 980 allTags = reduce(set.union, (list(font.keys()) for font in fonts), set()) 981 allTags.remove('GlyphOrder') 982 983 # Make sure we process cmap before GSUB as we have a dependency there. 984 if 'GSUB' in allTags: 985 allTags.remove('GSUB') 986 allTags = ['GSUB'] + list(allTags) 987 if 'cmap' in allTags: 988 allTags.remove('cmap') 989 allTags = ['cmap'] + list(allTags) 990 991 for tag in allTags: 992 with timer("merge '%s'" % tag): 993 tables = [font.get(tag, NotImplemented) for font in fonts] 994 995 log.info("Merging '%s'.", tag) 996 clazz = ttLib.getTableClass(tag) 997 table = clazz(tag).merge(self, tables) 998 # XXX Clean this up and use: table = mergeObjects(tables) 999 1000 if table is not NotImplemented and table is not False: 1001 mega[tag] = table 1002 log.info("Merged '%s'.", tag) 1003 else: 1004 log.info("Dropped '%s'.", tag) 1005 1006 del self.duplicateGlyphsPerFont 1007 del self.fonts 1008 1009 self._postMerge(mega) 1010 1011 return mega 1012 1013 def _mergeGlyphOrders(self, glyphOrders): 1014 """Modifies passed-in glyphOrders to reflect new glyph names. 1015 Returns glyphOrder for the merged font.""" 1016 # Simply append font index to the glyph name for now. 1017 # TODO Even this simplistic numbering can result in conflicts. 1018 # But then again, we have to improve this soon anyway. 1019 mega = [] 1020 for n,glyphOrder in enumerate(glyphOrders): 1021 for i,glyphName in enumerate(glyphOrder): 1022 glyphName += "#" + repr(n) 1023 glyphOrder[i] = glyphName 1024 mega.append(glyphName) 1025 return mega 1026 1027 def mergeObjects(self, returnTable, logic, tables): 1028 # Right now we don't use self at all. Will use in the future 1029 # for options and logging. 1030 1031 allKeys = set.union(set(), *(vars(table).keys() for table in tables if table is not NotImplemented)) 1032 for key in allKeys: 1033 try: 1034 mergeLogic = logic[key] 1035 except KeyError: 1036 try: 1037 mergeLogic = logic['*'] 1038 except KeyError: 1039 raise Exception("Don't know how to merge key %s of class %s" % 1040 (key, returnTable.__class__.__name__)) 1041 if mergeLogic is NotImplemented: 1042 continue 1043 value = mergeLogic(getattr(table, key, NotImplemented) for table in tables) 1044 if value is not NotImplemented: 1045 setattr(returnTable, key, value) 1046 1047 return returnTable 1048 1049 def _preMerge(self, font): 1050 1051 # Map indices to references 1052 1053 GDEF = font.get('GDEF') 1054 GSUB = font.get('GSUB') 1055 GPOS = font.get('GPOS') 1056 1057 for t in [GSUB, GPOS]: 1058 if not t: continue 1059 1060 if t.table.LookupList: 1061 lookupMap = {i:v for i,v in enumerate(t.table.LookupList.Lookup)} 1062 t.table.LookupList.mapLookups(lookupMap) 1063 t.table.FeatureList.mapLookups(lookupMap) 1064 1065 if t.table.FeatureList and t.table.ScriptList: 1066 featureMap = {i:v for i,v in enumerate(t.table.FeatureList.FeatureRecord)} 1067 t.table.ScriptList.mapFeatures(featureMap) 1068 1069 # TODO GDEF/Lookup MarkFilteringSets 1070 # TODO FeatureParams nameIDs 1071 1072 def _postMerge(self, font): 1073 1074 # Map references back to indices 1075 1076 GDEF = font.get('GDEF') 1077 GSUB = font.get('GSUB') 1078 GPOS = font.get('GPOS') 1079 1080 for t in [GSUB, GPOS]: 1081 if not t: continue 1082 1083 if t.table.FeatureList and t.table.ScriptList: 1084 1085 # Collect unregistered (new) features. 1086 featureMap = _GregariousIdentityDict(t.table.FeatureList.FeatureRecord) 1087 t.table.ScriptList.mapFeatures(featureMap) 1088 1089 # Record used features. 1090 featureMap = _AttendanceRecordingIdentityDict(t.table.FeatureList.FeatureRecord) 1091 t.table.ScriptList.mapFeatures(featureMap) 1092 usedIndices = featureMap.s 1093 1094 # Remove unused features 1095 t.table.FeatureList.FeatureRecord = [f for i,f in enumerate(t.table.FeatureList.FeatureRecord) if i in usedIndices] 1096 1097 # Map back to indices. 1098 featureMap = _NonhashableDict(t.table.FeatureList.FeatureRecord) 1099 t.table.ScriptList.mapFeatures(featureMap) 1100 1101 t.table.FeatureList.FeatureCount = len(t.table.FeatureList.FeatureRecord) 1102 1103 if t.table.LookupList: 1104 1105 # Collect unregistered (new) lookups. 1106 lookupMap = _GregariousIdentityDict(t.table.LookupList.Lookup) 1107 t.table.FeatureList.mapLookups(lookupMap) 1108 t.table.LookupList.mapLookups(lookupMap) 1109 1110 # Record used lookups. 1111 lookupMap = _AttendanceRecordingIdentityDict(t.table.LookupList.Lookup) 1112 t.table.FeatureList.mapLookups(lookupMap) 1113 t.table.LookupList.mapLookups(lookupMap) 1114 usedIndices = lookupMap.s 1115 1116 # Remove unused lookups 1117 t.table.LookupList.Lookup = [l for i,l in enumerate(t.table.LookupList.Lookup) if i in usedIndices] 1118 1119 # Map back to indices. 1120 lookupMap = _NonhashableDict(t.table.LookupList.Lookup) 1121 t.table.FeatureList.mapLookups(lookupMap) 1122 t.table.LookupList.mapLookups(lookupMap) 1123 1124 t.table.LookupList.LookupCount = len(t.table.LookupList.Lookup) 1125 1126 # TODO GDEF/Lookup MarkFilteringSets 1127 # TODO FeatureParams nameIDs 1128 1129 1130__all__ = [ 1131 'Options', 1132 'Merger', 1133 'main' 1134] 1135 1136@timer("make one with everything (TOTAL TIME)") 1137def main(args=None): 1138 from fontTools import configLogger 1139 1140 if args is None: 1141 args = sys.argv[1:] 1142 1143 options = Options() 1144 args = options.parse_opts(args) 1145 1146 if len(args) < 1: 1147 print("usage: pyftmerge font...", file=sys.stderr) 1148 return 1 1149 1150 configLogger(level=logging.INFO if options.verbose else logging.WARNING) 1151 if options.timing: 1152 timer.logger.setLevel(logging.DEBUG) 1153 else: 1154 timer.logger.disabled = True 1155 1156 merger = Merger(options=options) 1157 font = merger.merge(args) 1158 outfile = 'merged.ttf' 1159 with timer("compile and save font"): 1160 font.save(outfile) 1161 1162 1163if __name__ == "__main__": 1164 sys.exit(main()) 1165