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