1# Copyright 2013 Google, Inc. All Rights Reserved. 2# 3# Google Author(s): Behdad Esfahbod, Roozbeh Pournader 4 5from fontTools import ttLib 6from fontTools.ttLib.tables.DefaultTable import DefaultTable 7from fontTools.ttLib.tables import otTables 8from fontTools.merge.base import add_method, mergeObjects 9from fontTools.merge.util import * 10import logging 11 12 13log = logging.getLogger("fontTools.merge") 14 15 16def mergeLookupLists(lst): 17 # TODO Do smarter merge. 18 return sumLists(lst) 19 20 21def mergeFeatures(lst): 22 assert lst 23 self = otTables.Feature() 24 self.FeatureParams = None 25 self.LookupListIndex = mergeLookupLists( 26 [l.LookupListIndex for l in lst if l.LookupListIndex] 27 ) 28 self.LookupCount = len(self.LookupListIndex) 29 return self 30 31 32def mergeFeatureLists(lst): 33 d = {} 34 for l in lst: 35 for f in l: 36 tag = f.FeatureTag 37 if tag not in d: 38 d[tag] = [] 39 d[tag].append(f.Feature) 40 ret = [] 41 for tag in sorted(d.keys()): 42 rec = otTables.FeatureRecord() 43 rec.FeatureTag = tag 44 rec.Feature = mergeFeatures(d[tag]) 45 ret.append(rec) 46 return ret 47 48 49def mergeLangSyses(lst): 50 assert lst 51 52 # TODO Support merging ReqFeatureIndex 53 assert all(l.ReqFeatureIndex == 0xFFFF for l in lst) 54 55 self = otTables.LangSys() 56 self.LookupOrder = None 57 self.ReqFeatureIndex = 0xFFFF 58 self.FeatureIndex = mergeFeatureLists( 59 [l.FeatureIndex for l in lst if l.FeatureIndex] 60 ) 61 self.FeatureCount = len(self.FeatureIndex) 62 return self 63 64 65def mergeScripts(lst): 66 assert lst 67 68 if len(lst) == 1: 69 return lst[0] 70 langSyses = {} 71 for sr in lst: 72 for lsr in sr.LangSysRecord: 73 if lsr.LangSysTag not in langSyses: 74 langSyses[lsr.LangSysTag] = [] 75 langSyses[lsr.LangSysTag].append(lsr.LangSys) 76 lsrecords = [] 77 for tag, langSys_list in sorted(langSyses.items()): 78 lsr = otTables.LangSysRecord() 79 lsr.LangSys = mergeLangSyses(langSys_list) 80 lsr.LangSysTag = tag 81 lsrecords.append(lsr) 82 83 self = otTables.Script() 84 self.LangSysRecord = lsrecords 85 self.LangSysCount = len(lsrecords) 86 dfltLangSyses = [s.DefaultLangSys for s in lst if s.DefaultLangSys] 87 if dfltLangSyses: 88 self.DefaultLangSys = mergeLangSyses(dfltLangSyses) 89 else: 90 self.DefaultLangSys = None 91 return self 92 93 94def mergeScriptRecords(lst): 95 d = {} 96 for l in lst: 97 for s in l: 98 tag = s.ScriptTag 99 if tag not in d: 100 d[tag] = [] 101 d[tag].append(s.Script) 102 ret = [] 103 for tag in sorted(d.keys()): 104 rec = otTables.ScriptRecord() 105 rec.ScriptTag = tag 106 rec.Script = mergeScripts(d[tag]) 107 ret.append(rec) 108 return ret 109 110 111otTables.ScriptList.mergeMap = { 112 "ScriptCount": lambda lst: None, # TODO 113 "ScriptRecord": mergeScriptRecords, 114} 115otTables.BaseScriptList.mergeMap = { 116 "BaseScriptCount": lambda lst: None, # TODO 117 # TODO: Merge duplicate entries 118 "BaseScriptRecord": lambda lst: sorted( 119 sumLists(lst), key=lambda s: s.BaseScriptTag 120 ), 121} 122 123otTables.FeatureList.mergeMap = { 124 "FeatureCount": sum, 125 "FeatureRecord": lambda lst: sorted(sumLists(lst), key=lambda s: s.FeatureTag), 126} 127 128otTables.LookupList.mergeMap = { 129 "LookupCount": sum, 130 "Lookup": sumLists, 131} 132 133otTables.Coverage.mergeMap = { 134 "Format": min, 135 "glyphs": sumLists, 136} 137 138otTables.ClassDef.mergeMap = { 139 "Format": min, 140 "classDefs": sumDicts, 141} 142 143otTables.LigCaretList.mergeMap = { 144 "Coverage": mergeObjects, 145 "LigGlyphCount": sum, 146 "LigGlyph": sumLists, 147} 148 149otTables.AttachList.mergeMap = { 150 "Coverage": mergeObjects, 151 "GlyphCount": sum, 152 "AttachPoint": sumLists, 153} 154 155# XXX Renumber MarkFilterSets of lookups 156otTables.MarkGlyphSetsDef.mergeMap = { 157 "MarkSetTableFormat": equal, 158 "MarkSetCount": sum, 159 "Coverage": sumLists, 160} 161 162otTables.Axis.mergeMap = { 163 "*": mergeObjects, 164} 165 166# XXX Fix BASE table merging 167otTables.BaseTagList.mergeMap = { 168 "BaseTagCount": sum, 169 "BaselineTag": sumLists, 170} 171 172otTables.GDEF.mergeMap = otTables.GSUB.mergeMap = otTables.GPOS.mergeMap = ( 173 otTables.BASE.mergeMap 174) = otTables.JSTF.mergeMap = otTables.MATH.mergeMap = { 175 "*": mergeObjects, 176 "Version": max, 177} 178 179ttLib.getTableClass("GDEF").mergeMap = ttLib.getTableClass("GSUB").mergeMap = ( 180 ttLib.getTableClass("GPOS").mergeMap 181) = ttLib.getTableClass("BASE").mergeMap = ttLib.getTableClass( 182 "JSTF" 183).mergeMap = ttLib.getTableClass( 184 "MATH" 185).mergeMap = { 186 "tableTag": onlyExisting(equal), # XXX clean me up 187 "table": mergeObjects, 188} 189 190 191@add_method(ttLib.getTableClass("GSUB")) 192def merge(self, m, tables): 193 assert len(tables) == len(m.duplicateGlyphsPerFont) 194 for i, (table, dups) in enumerate(zip(tables, m.duplicateGlyphsPerFont)): 195 if not dups: 196 continue 197 if table is None or table is NotImplemented: 198 log.warning( 199 "Have non-identical duplicates to resolve for '%s' but no GSUB. Are duplicates intended?: %s", 200 m.fonts[i]._merger__name, 201 dups, 202 ) 203 continue 204 205 synthFeature = None 206 synthLookup = None 207 for script in table.table.ScriptList.ScriptRecord: 208 if script.ScriptTag == "DFLT": 209 continue # XXX 210 for langsys in [script.Script.DefaultLangSys] + [ 211 l.LangSys for l in script.Script.LangSysRecord 212 ]: 213 if langsys is None: 214 continue # XXX Create! 215 feature = [v for v in langsys.FeatureIndex if v.FeatureTag == "locl"] 216 assert len(feature) <= 1 217 if feature: 218 feature = feature[0] 219 else: 220 if not synthFeature: 221 synthFeature = otTables.FeatureRecord() 222 synthFeature.FeatureTag = "locl" 223 f = synthFeature.Feature = otTables.Feature() 224 f.FeatureParams = None 225 f.LookupCount = 0 226 f.LookupListIndex = [] 227 table.table.FeatureList.FeatureRecord.append(synthFeature) 228 table.table.FeatureList.FeatureCount += 1 229 feature = synthFeature 230 langsys.FeatureIndex.append(feature) 231 langsys.FeatureIndex.sort(key=lambda v: v.FeatureTag) 232 233 if not synthLookup: 234 subtable = otTables.SingleSubst() 235 subtable.mapping = dups 236 synthLookup = otTables.Lookup() 237 synthLookup.LookupFlag = 0 238 synthLookup.LookupType = 1 239 synthLookup.SubTableCount = 1 240 synthLookup.SubTable = [subtable] 241 if table.table.LookupList is None: 242 # mtiLib uses None as default value for LookupList, 243 # while feaLib points to an empty array with count 0 244 # TODO: make them do the same 245 table.table.LookupList = otTables.LookupList() 246 table.table.LookupList.Lookup = [] 247 table.table.LookupList.LookupCount = 0 248 table.table.LookupList.Lookup.append(synthLookup) 249 table.table.LookupList.LookupCount += 1 250 251 if feature.Feature.LookupListIndex[:1] != [synthLookup]: 252 feature.Feature.LookupListIndex[:0] = [synthLookup] 253 feature.Feature.LookupCount += 1 254 255 DefaultTable.merge(self, m, tables) 256 return self 257 258 259@add_method( 260 otTables.SingleSubst, 261 otTables.MultipleSubst, 262 otTables.AlternateSubst, 263 otTables.LigatureSubst, 264 otTables.ReverseChainSingleSubst, 265 otTables.SinglePos, 266 otTables.PairPos, 267 otTables.CursivePos, 268 otTables.MarkBasePos, 269 otTables.MarkLigPos, 270 otTables.MarkMarkPos, 271) 272def mapLookups(self, lookupMap): 273 pass 274 275 276# Copied and trimmed down from subset.py 277@add_method( 278 otTables.ContextSubst, 279 otTables.ChainContextSubst, 280 otTables.ContextPos, 281 otTables.ChainContextPos, 282) 283def __merge_classify_context(self): 284 class ContextHelper(object): 285 def __init__(self, klass, Format): 286 if klass.__name__.endswith("Subst"): 287 Typ = "Sub" 288 Type = "Subst" 289 else: 290 Typ = "Pos" 291 Type = "Pos" 292 if klass.__name__.startswith("Chain"): 293 Chain = "Chain" 294 else: 295 Chain = "" 296 ChainTyp = Chain + Typ 297 298 self.Typ = Typ 299 self.Type = Type 300 self.Chain = Chain 301 self.ChainTyp = ChainTyp 302 303 self.LookupRecord = Type + "LookupRecord" 304 305 if Format == 1: 306 self.Rule = ChainTyp + "Rule" 307 self.RuleSet = ChainTyp + "RuleSet" 308 elif Format == 2: 309 self.Rule = ChainTyp + "ClassRule" 310 self.RuleSet = ChainTyp + "ClassSet" 311 312 if self.Format not in [1, 2, 3]: 313 return None # Don't shoot the messenger; let it go 314 if not hasattr(self.__class__, "_merge__ContextHelpers"): 315 self.__class__._merge__ContextHelpers = {} 316 if self.Format not in self.__class__._merge__ContextHelpers: 317 helper = ContextHelper(self.__class__, self.Format) 318 self.__class__._merge__ContextHelpers[self.Format] = helper 319 return self.__class__._merge__ContextHelpers[self.Format] 320 321 322@add_method( 323 otTables.ContextSubst, 324 otTables.ChainContextSubst, 325 otTables.ContextPos, 326 otTables.ChainContextPos, 327) 328def mapLookups(self, lookupMap): 329 c = self.__merge_classify_context() 330 331 if self.Format in [1, 2]: 332 for rs in getattr(self, c.RuleSet): 333 if not rs: 334 continue 335 for r in getattr(rs, c.Rule): 336 if not r: 337 continue 338 for ll in getattr(r, c.LookupRecord): 339 if not ll: 340 continue 341 ll.LookupListIndex = lookupMap[ll.LookupListIndex] 342 elif self.Format == 3: 343 for ll in getattr(self, c.LookupRecord): 344 if not ll: 345 continue 346 ll.LookupListIndex = lookupMap[ll.LookupListIndex] 347 else: 348 assert 0, "unknown format: %s" % self.Format 349 350 351@add_method(otTables.ExtensionSubst, otTables.ExtensionPos) 352def mapLookups(self, lookupMap): 353 if self.Format == 1: 354 self.ExtSubTable.mapLookups(lookupMap) 355 else: 356 assert 0, "unknown format: %s" % self.Format 357 358 359@add_method(otTables.Lookup) 360def mapLookups(self, lookupMap): 361 for st in self.SubTable: 362 if not st: 363 continue 364 st.mapLookups(lookupMap) 365 366 367@add_method(otTables.LookupList) 368def mapLookups(self, lookupMap): 369 for l in self.Lookup: 370 if not l: 371 continue 372 l.mapLookups(lookupMap) 373 374 375@add_method(otTables.Lookup) 376def mapMarkFilteringSets(self, markFilteringSetMap): 377 if self.LookupFlag & 0x0010: 378 self.MarkFilteringSet = markFilteringSetMap[self.MarkFilteringSet] 379 380 381@add_method(otTables.LookupList) 382def mapMarkFilteringSets(self, markFilteringSetMap): 383 for l in self.Lookup: 384 if not l: 385 continue 386 l.mapMarkFilteringSets(markFilteringSetMap) 387 388 389@add_method(otTables.Feature) 390def mapLookups(self, lookupMap): 391 self.LookupListIndex = [lookupMap[i] for i in self.LookupListIndex] 392 393 394@add_method(otTables.FeatureList) 395def mapLookups(self, lookupMap): 396 for f in self.FeatureRecord: 397 if not f or not f.Feature: 398 continue 399 f.Feature.mapLookups(lookupMap) 400 401 402@add_method(otTables.DefaultLangSys, otTables.LangSys) 403def mapFeatures(self, featureMap): 404 self.FeatureIndex = [featureMap[i] for i in self.FeatureIndex] 405 if self.ReqFeatureIndex != 65535: 406 self.ReqFeatureIndex = featureMap[self.ReqFeatureIndex] 407 408 409@add_method(otTables.Script) 410def mapFeatures(self, featureMap): 411 if self.DefaultLangSys: 412 self.DefaultLangSys.mapFeatures(featureMap) 413 for l in self.LangSysRecord: 414 if not l or not l.LangSys: 415 continue 416 l.LangSys.mapFeatures(featureMap) 417 418 419@add_method(otTables.ScriptList) 420def mapFeatures(self, featureMap): 421 for s in self.ScriptRecord: 422 if not s or not s.Script: 423 continue 424 s.Script.mapFeatures(featureMap) 425 426 427def layoutPreMerge(font): 428 # Map indices to references 429 430 GDEF = font.get("GDEF") 431 GSUB = font.get("GSUB") 432 GPOS = font.get("GPOS") 433 434 for t in [GSUB, GPOS]: 435 if not t: 436 continue 437 438 if t.table.LookupList: 439 lookupMap = {i: v for i, v in enumerate(t.table.LookupList.Lookup)} 440 t.table.LookupList.mapLookups(lookupMap) 441 t.table.FeatureList.mapLookups(lookupMap) 442 443 if ( 444 GDEF 445 and GDEF.table.Version >= 0x00010002 446 and GDEF.table.MarkGlyphSetsDef 447 ): 448 markFilteringSetMap = { 449 i: v for i, v in enumerate(GDEF.table.MarkGlyphSetsDef.Coverage) 450 } 451 t.table.LookupList.mapMarkFilteringSets(markFilteringSetMap) 452 453 if t.table.FeatureList and t.table.ScriptList: 454 featureMap = {i: v for i, v in enumerate(t.table.FeatureList.FeatureRecord)} 455 t.table.ScriptList.mapFeatures(featureMap) 456 457 # TODO FeatureParams nameIDs 458 459 460def layoutPostMerge(font): 461 # Map references back to indices 462 463 GDEF = font.get("GDEF") 464 GSUB = font.get("GSUB") 465 GPOS = font.get("GPOS") 466 467 for t in [GSUB, GPOS]: 468 if not t: 469 continue 470 471 if t.table.FeatureList and t.table.ScriptList: 472 # Collect unregistered (new) features. 473 featureMap = GregariousIdentityDict(t.table.FeatureList.FeatureRecord) 474 t.table.ScriptList.mapFeatures(featureMap) 475 476 # Record used features. 477 featureMap = AttendanceRecordingIdentityDict( 478 t.table.FeatureList.FeatureRecord 479 ) 480 t.table.ScriptList.mapFeatures(featureMap) 481 usedIndices = featureMap.s 482 483 # Remove unused features 484 t.table.FeatureList.FeatureRecord = [ 485 f 486 for i, f in enumerate(t.table.FeatureList.FeatureRecord) 487 if i in usedIndices 488 ] 489 490 # Map back to indices. 491 featureMap = NonhashableDict(t.table.FeatureList.FeatureRecord) 492 t.table.ScriptList.mapFeatures(featureMap) 493 494 t.table.FeatureList.FeatureCount = len(t.table.FeatureList.FeatureRecord) 495 496 if t.table.LookupList: 497 # Collect unregistered (new) lookups. 498 lookupMap = GregariousIdentityDict(t.table.LookupList.Lookup) 499 t.table.FeatureList.mapLookups(lookupMap) 500 t.table.LookupList.mapLookups(lookupMap) 501 502 # Record used lookups. 503 lookupMap = AttendanceRecordingIdentityDict(t.table.LookupList.Lookup) 504 t.table.FeatureList.mapLookups(lookupMap) 505 t.table.LookupList.mapLookups(lookupMap) 506 usedIndices = lookupMap.s 507 508 # Remove unused lookups 509 t.table.LookupList.Lookup = [ 510 l for i, l in enumerate(t.table.LookupList.Lookup) if i in usedIndices 511 ] 512 513 # Map back to indices. 514 lookupMap = NonhashableDict(t.table.LookupList.Lookup) 515 t.table.FeatureList.mapLookups(lookupMap) 516 t.table.LookupList.mapLookups(lookupMap) 517 518 t.table.LookupList.LookupCount = len(t.table.LookupList.Lookup) 519 520 if GDEF and GDEF.table.Version >= 0x00010002: 521 markFilteringSetMap = NonhashableDict( 522 GDEF.table.MarkGlyphSetsDef.Coverage 523 ) 524 t.table.LookupList.mapMarkFilteringSets(markFilteringSetMap) 525 526 # TODO FeatureParams nameIDs 527