otTables.py revision 7ed91eca1eaa96b79eae780778e89bb9ec44c1ee
1"""fontTools.ttLib.tables.otTables -- A collection of classes representing the various 2OpenType subtables. 3 4Most are constructed upon import from data in otData.py, all are populated with 5converter objects from otConverters.py. 6""" 7import operator 8from .otBase import BaseTable, FormatSwitchingBaseTable 9import warnings 10from fontTools.misc.py23 import * 11 12 13class LookupOrder(BaseTable): 14 """Dummy class; this table isn't defined, but is used, and is always NULL.""" 15 16class FeatureParams(BaseTable): 17 18 def compile(self, writer, font): 19 assert featureParamTypes.get(writer['FeatureTag'], None) == self.__class__, "Wrong FeatureParams type for feature '%s': %s" % (writer['FeatureTag'], self.__class__.__name__) 20 BaseTable.compile(self, writer, font) 21 22class FeatureParamsSize(FeatureParams): 23 pass 24 25class FeatureParamsStylisticSet(FeatureParams): 26 pass 27 28class FeatureParamsCharacterVariants(FeatureParams): 29 pass 30 31class Coverage(FormatSwitchingBaseTable): 32 33 # manual implementation to get rid of glyphID dependencies 34 35 def postRead(self, rawTable, font): 36 if self.Format == 1: 37 self.glyphs = rawTable["GlyphArray"] 38 elif self.Format == 2: 39 glyphs = self.glyphs = [] 40 ranges = rawTable["RangeRecord"] 41 glyphOrder = font.getGlyphOrder() 42 # Some SIL fonts have coverage entries that don't have sorted 43 # StartCoverageIndex. If it is so, fixup and warn. We undo 44 # this when writing font out. 45 sorted_ranges = sorted(ranges, cmp=lambda a,b: cmp(a.StartCoverageIndex,b.StartCoverageIndex)) 46 if ranges != sorted_ranges: 47 warnings.warn("GSUB/GPOS Coverage is not sorted by glyph ids.") 48 ranges = sorted_ranges 49 del sorted_ranges 50 for r in ranges: 51 assert r.StartCoverageIndex == len(glyphs), \ 52 (r.StartCoverageIndex, len(glyphs)) 53 start = r.Start 54 end = r.End 55 try: 56 startID = font.getGlyphID(start, requireReal=1) 57 except KeyError: 58 warnings.warn("Coverage table has start glyph ID out of range: %s." % start) 59 continue 60 try: 61 endID = font.getGlyphID(end, requireReal=1) 62 except KeyError: 63 warnings.warn("Coverage table has end glyph ID out of range: %s." % end) 64 endID = len(glyphOrder) 65 glyphs.append(start) 66 rangeList = [glyphOrder[glyphID] for glyphID in range(startID + 1, endID) ] 67 glyphs += rangeList 68 if start != end and endID < len(glyphOrder): 69 glyphs.append(end) 70 else: 71 assert 0, "unknown format: %s" % self.Format 72 73 def preWrite(self, font): 74 glyphs = getattr(self, "glyphs", None) 75 if glyphs is None: 76 glyphs = self.glyphs = [] 77 format = 1 78 rawTable = {"GlyphArray": glyphs} 79 getGlyphID = font.getGlyphID 80 if glyphs: 81 # find out whether Format 2 is more compact or not 82 glyphIDs = [getGlyphID(glyphName) for glyphName in glyphs ] 83 brokenOrder = sorted(glyphIDs) != glyphIDs 84 85 last = glyphIDs[0] 86 ranges = [[last]] 87 for glyphID in glyphIDs[1:]: 88 if glyphID != last + 1: 89 ranges[-1].append(last) 90 ranges.append([glyphID]) 91 last = glyphID 92 ranges[-1].append(last) 93 94 if brokenOrder or len(ranges) * 3 < len(glyphs): # 3 words vs. 1 word 95 # Format 2 is more compact 96 index = 0 97 for i in range(len(ranges)): 98 start, end = ranges[i] 99 r = RangeRecord() 100 r.StartID = start 101 r.Start = font.getGlyphName(start) 102 r.End = font.getGlyphName(end) 103 r.StartCoverageIndex = index 104 ranges[i] = r 105 index = index + end - start + 1 106 if brokenOrder: 107 warnings.warn("GSUB/GPOS Coverage is not sorted by glyph ids.") 108 ranges.sort(cmp=lambda a,b: cmp(a.StartID,b.StartID)) 109 for r in ranges: 110 del r.StartID 111 format = 2 112 rawTable = {"RangeRecord": ranges} 113 #else: 114 # fallthrough; Format 1 is more compact 115 self.Format = format 116 return rawTable 117 118 def toXML2(self, xmlWriter, font): 119 for glyphName in getattr(self, "glyphs", []): 120 xmlWriter.simpletag("Glyph", value=glyphName) 121 xmlWriter.newline() 122 123 def fromXML(self, name, attrs, content, font): 124 glyphs = getattr(self, "glyphs", None) 125 if glyphs is None: 126 glyphs = [] 127 self.glyphs = glyphs 128 glyphs.append(attrs["value"]) 129 130 131def doModulo(value): 132 if value < 0: 133 return value + 65536 134 return value 135 136class SingleSubst(FormatSwitchingBaseTable): 137 138 def postRead(self, rawTable, font): 139 mapping = {} 140 input = _getGlyphsFromCoverageTable(rawTable["Coverage"]) 141 lenMapping = len(input) 142 if self.Format == 1: 143 delta = rawTable["DeltaGlyphID"] 144 inputGIDS = [ font.getGlyphID(name) for name in input ] 145 inputGIDS = map(doModulo, inputGIDS) 146 outGIDS = [ glyphID + delta for glyphID in inputGIDS ] 147 outGIDS = map(doModulo, outGIDS) 148 outNames = [ font.getGlyphName(glyphID) for glyphID in outGIDS ] 149 list(map(operator.setitem, [mapping]*lenMapping, input, outNames)) 150 elif self.Format == 2: 151 assert len(input) == rawTable["GlyphCount"], \ 152 "invalid SingleSubstFormat2 table" 153 subst = rawTable["Substitute"] 154 list(map(operator.setitem, [mapping]*lenMapping, input, subst)) 155 else: 156 assert 0, "unknown format: %s" % self.Format 157 self.mapping = mapping 158 159 def preWrite(self, font): 160 mapping = getattr(self, "mapping", None) 161 if mapping is None: 162 mapping = self.mapping = {} 163 items = list(mapping.items()) 164 getGlyphID = font.getGlyphID 165 gidItems = [(getGlyphID(item[0]), getGlyphID(item[1])) for item in items] 166 sortableItems = sorted(zip(gidItems, items)) 167 168 # figure out format 169 format = 2 170 delta = None 171 for inID, outID in gidItems: 172 if delta is None: 173 delta = outID - inID 174 else: 175 if delta != outID - inID: 176 break 177 else: 178 format = 1 179 180 rawTable = {} 181 self.Format = format 182 cov = Coverage() 183 input = [ item [1][0] for item in sortableItems] 184 subst = [ item [1][1] for item in sortableItems] 185 cov.glyphs = input 186 rawTable["Coverage"] = cov 187 if format == 1: 188 assert delta is not None 189 rawTable["DeltaGlyphID"] = delta 190 else: 191 rawTable["Substitute"] = subst 192 return rawTable 193 194 def toXML2(self, xmlWriter, font): 195 items = sorted(self.mapping.items()) 196 for inGlyph, outGlyph in items: 197 xmlWriter.simpletag("Substitution", 198 [("in", inGlyph), ("out", outGlyph)]) 199 xmlWriter.newline() 200 201 def fromXML(self, name, attrs, content, font): 202 mapping = getattr(self, "mapping", None) 203 if mapping is None: 204 mapping = {} 205 self.mapping = mapping 206 mapping[attrs["in"]] = attrs["out"] 207 208 209class ClassDef(FormatSwitchingBaseTable): 210 211 def postRead(self, rawTable, font): 212 classDefs = {} 213 getGlyphName = font.getGlyphName 214 215 if self.Format == 1: 216 start = rawTable["StartGlyph"] 217 classList = rawTable["ClassValueArray"] 218 lenList = len(classList) 219 glyphID = font.getGlyphID(start) 220 gidList = list(range(glyphID, glyphID + len(classList))) 221 keyList = [getGlyphName(glyphID) for glyphID in gidList] 222 223 list(map(operator.setitem, [classDefs]*lenList, keyList, classList)) 224 225 elif self.Format == 2: 226 records = rawTable["ClassRangeRecord"] 227 for rec in records: 228 start = rec.Start 229 end = rec.End 230 cls = rec.Class 231 classDefs[start] = cls 232 glyphIDs = list(range(font.getGlyphID(start) + 1, font.getGlyphID(end))) 233 lenList = len(glyphIDs) 234 keyList = [getGlyphName(glyphID) for glyphID in glyphIDs] 235 list(map(operator.setitem, [classDefs]*lenList, keyList, [cls]*lenList)) 236 classDefs[end] = cls 237 else: 238 assert 0, "unknown format: %s" % self.Format 239 self.classDefs = classDefs 240 241 def preWrite(self, font): 242 classDefs = getattr(self, "classDefs", None) 243 if classDefs is None: 244 classDefs = self.classDefs = {} 245 items = list(classDefs.items()) 246 getGlyphID = font.getGlyphID 247 for i in range(len(items)): 248 glyphName, cls = items[i] 249 items[i] = getGlyphID(glyphName), glyphName, cls 250 items.sort() 251 if items: 252 last, lastName, lastCls = items[0] 253 rec = ClassRangeRecord() 254 rec.Start = lastName 255 rec.Class = lastCls 256 ranges = [rec] 257 for glyphID, glyphName, cls in items[1:]: 258 if glyphID != last + 1 or cls != lastCls: 259 rec.End = lastName 260 rec = ClassRangeRecord() 261 rec.Start = glyphName 262 rec.Class = cls 263 ranges.append(rec) 264 last = glyphID 265 lastName = glyphName 266 lastCls = cls 267 rec.End = lastName 268 else: 269 ranges = [] 270 self.Format = 2 # currently no support for Format 1 271 return {"ClassRangeRecord": ranges} 272 273 def toXML2(self, xmlWriter, font): 274 items = sorted(self.classDefs.items()) 275 for glyphName, cls in items: 276 xmlWriter.simpletag("ClassDef", [("glyph", glyphName), ("class", cls)]) 277 xmlWriter.newline() 278 279 def fromXML(self, name, attrs, content, font): 280 classDefs = getattr(self, "classDefs", None) 281 if classDefs is None: 282 classDefs = {} 283 self.classDefs = classDefs 284 classDefs[attrs["glyph"]] = int(attrs["class"]) 285 286 287class AlternateSubst(FormatSwitchingBaseTable): 288 289 def postRead(self, rawTable, font): 290 alternates = {} 291 if self.Format == 1: 292 input = _getGlyphsFromCoverageTable(rawTable["Coverage"]) 293 alts = rawTable["AlternateSet"] 294 if len(input) != len(alts): 295 assert len(input) == len(alts) 296 for i in range(len(input)): 297 alternates[input[i]] = alts[i].Alternate 298 else: 299 assert 0, "unknown format: %s" % self.Format 300 self.alternates = alternates 301 302 def preWrite(self, font): 303 self.Format = 1 304 alternates = getattr(self, "alternates", None) 305 if alternates is None: 306 alternates = self.alternates = {} 307 items = list(alternates.items()) 308 for i in range(len(items)): 309 glyphName, set = items[i] 310 items[i] = font.getGlyphID(glyphName), glyphName, set 311 items.sort() 312 cov = Coverage() 313 cov.glyphs = [ item[1] for item in items] 314 alternates = [] 315 setList = [ item[-1] for item in items] 316 for set in setList: 317 alts = AlternateSet() 318 alts.Alternate = set 319 alternates.append(alts) 320 # a special case to deal with the fact that several hundred Adobe Japan1-5 321 # CJK fonts will overflow an offset if the coverage table isn't pushed to the end. 322 # Also useful in that when splitting a sub-table because of an offset overflow 323 # I don't need to calculate the change in the subtable offset due to the change in the coverage table size. 324 # Allows packing more rules in subtable. 325 self.sortCoverageLast = 1 326 return {"Coverage": cov, "AlternateSet": alternates} 327 328 def toXML2(self, xmlWriter, font): 329 items = sorted(self.alternates.items()) 330 for glyphName, alternates in items: 331 xmlWriter.begintag("AlternateSet", glyph=glyphName) 332 xmlWriter.newline() 333 for alt in alternates: 334 xmlWriter.simpletag("Alternate", glyph=alt) 335 xmlWriter.newline() 336 xmlWriter.endtag("AlternateSet") 337 xmlWriter.newline() 338 339 def fromXML(self, name, attrs, content, font): 340 alternates = getattr(self, "alternates", None) 341 if alternates is None: 342 alternates = {} 343 self.alternates = alternates 344 glyphName = attrs["glyph"] 345 set = [] 346 alternates[glyphName] = set 347 for element in content: 348 if not isinstance(element, tuple): 349 continue 350 name, attrs, content = element 351 set.append(attrs["glyph"]) 352 353 354class LigatureSubst(FormatSwitchingBaseTable): 355 356 def postRead(self, rawTable, font): 357 ligatures = {} 358 if self.Format == 1: 359 input = rawTable["Coverage"].glyphs 360 ligSets = rawTable["LigatureSet"] 361 assert len(input) == len(ligSets) 362 for i in range(len(input)): 363 ligatures[input[i]] = ligSets[i].Ligature 364 else: 365 assert 0, "unknown format: %s" % self.Format 366 self.ligatures = ligatures 367 368 def preWrite(self, font): 369 ligatures = getattr(self, "ligatures", None) 370 if ligatures is None: 371 ligatures = self.ligatures = {} 372 items = list(ligatures.items()) 373 for i in range(len(items)): 374 glyphName, set = items[i] 375 items[i] = font.getGlyphID(glyphName), glyphName, set 376 items.sort() 377 cov = Coverage() 378 cov.glyphs = [ item[1] for item in items] 379 380 ligSets = [] 381 setList = [ item[-1] for item in items ] 382 for set in setList: 383 ligSet = LigatureSet() 384 ligs = ligSet.Ligature = [] 385 for lig in set: 386 ligs.append(lig) 387 ligSets.append(ligSet) 388 # Useful in that when splitting a sub-table because of an offset overflow 389 # I don't need to calculate the change in subtabl offset due to the coverage table size. 390 # Allows packing more rules in subtable. 391 self.sortCoverageLast = 1 392 return {"Coverage": cov, "LigatureSet": ligSets} 393 394 def toXML2(self, xmlWriter, font): 395 items = sorted(self.ligatures.items()) 396 for glyphName, ligSets in items: 397 xmlWriter.begintag("LigatureSet", glyph=glyphName) 398 xmlWriter.newline() 399 for lig in ligSets: 400 xmlWriter.simpletag("Ligature", glyph=lig.LigGlyph, 401 components=",".join(lig.Component)) 402 xmlWriter.newline() 403 xmlWriter.endtag("LigatureSet") 404 xmlWriter.newline() 405 406 def fromXML(self, name, attrs, content, font): 407 ligatures = getattr(self, "ligatures", None) 408 if ligatures is None: 409 ligatures = {} 410 self.ligatures = ligatures 411 glyphName = attrs["glyph"] 412 ligs = [] 413 ligatures[glyphName] = ligs 414 for element in content: 415 if not isinstance(element, tuple): 416 continue 417 name, attrs, content = element 418 lig = Ligature() 419 lig.LigGlyph = attrs["glyph"] 420 lig.Component = attrs["components"].split(",") 421 ligs.append(lig) 422 423 424# 425# For each subtable format there is a class. However, we don't really distinguish 426# between "field name" and "format name": often these are the same. Yet there's 427# a whole bunch of fields with different names. The following dict is a mapping 428# from "format name" to "field name". _buildClasses() uses this to create a 429# subclass for each alternate field name. 430# 431_equivalents = { 432 'MarkArray': ("Mark1Array",), 433 'LangSys': ('DefaultLangSys',), 434 'Coverage': ('MarkCoverage', 'BaseCoverage', 'LigatureCoverage', 'Mark1Coverage', 435 'Mark2Coverage', 'BacktrackCoverage', 'InputCoverage', 436 'LookAheadCoverage'), 437 'ClassDef': ('ClassDef1', 'ClassDef2', 'BacktrackClassDef', 'InputClassDef', 438 'LookAheadClassDef', 'GlyphClassDef', 'MarkAttachClassDef'), 439 'Anchor': ('EntryAnchor', 'ExitAnchor', 'BaseAnchor', 'LigatureAnchor', 440 'Mark2Anchor', 'MarkAnchor'), 441 'Device': ('XPlaDevice', 'YPlaDevice', 'XAdvDevice', 'YAdvDevice', 442 'XDeviceTable', 'YDeviceTable', 'DeviceTable'), 443 'Axis': ('HorizAxis', 'VertAxis',), 444 'MinMax': ('DefaultMinMax',), 445 'BaseCoord': ('MinCoord', 'MaxCoord',), 446 'JstfLangSys': ('DefJstfLangSys',), 447 'JstfGSUBModList': ('ShrinkageEnableGSUB', 'ShrinkageDisableGSUB', 'ExtensionEnableGSUB', 448 'ExtensionDisableGSUB',), 449 'JstfGPOSModList': ('ShrinkageEnableGPOS', 'ShrinkageDisableGPOS', 'ExtensionEnableGPOS', 450 'ExtensionDisableGPOS',), 451 'JstfMax': ('ShrinkageJstfMax', 'ExtensionJstfMax',), 452} 453 454# 455# OverFlow logic, to automatically create ExtensionLookups 456# XXX This should probably move to otBase.py 457# 458 459def fixLookupOverFlows(ttf, overflowRecord): 460 """ Either the offset from the LookupList to a lookup overflowed, or 461 an offset from a lookup to a subtable overflowed. 462 The table layout is: 463 GPSO/GUSB 464 Script List 465 Feature List 466 LookUpList 467 Lookup[0] and contents 468 SubTable offset list 469 SubTable[0] and contents 470 ... 471 SubTable[n] and contents 472 ... 473 Lookup[n] and contents 474 SubTable offset list 475 SubTable[0] and contents 476 ... 477 SubTable[n] and contents 478 If the offset to a lookup overflowed (SubTableIndex == None) 479 we must promote the *previous* lookup to an Extension type. 480 If the offset from a lookup to subtable overflowed, then we must promote it 481 to an Extension Lookup type. 482 """ 483 ok = 0 484 lookupIndex = overflowRecord.LookupListIndex 485 if (overflowRecord.SubTableIndex == None): 486 lookupIndex = lookupIndex - 1 487 if lookupIndex < 0: 488 return ok 489 if overflowRecord.tableType == 'GSUB': 490 extType = 7 491 elif overflowRecord.tableType == 'GPOS': 492 extType = 9 493 494 lookups = ttf[overflowRecord.tableType].table.LookupList.Lookup 495 lookup = lookups[lookupIndex] 496 # If the previous lookup is an extType, look further back. Very unlikely, but possible. 497 while lookup.LookupType == extType: 498 lookupIndex = lookupIndex -1 499 if lookupIndex < 0: 500 return ok 501 lookup = lookups[lookupIndex] 502 503 for si in range(len(lookup.SubTable)): 504 subTable = lookup.SubTable[si] 505 extSubTableClass = lookupTypes[overflowRecord.tableType][extType] 506 extSubTable = extSubTableClass() 507 extSubTable.Format = 1 508 extSubTable.ExtensionLookupType = lookup.LookupType 509 extSubTable.ExtSubTable = subTable 510 lookup.SubTable[si] = extSubTable 511 lookup.LookupType = extType 512 ok = 1 513 return ok 514 515def splitAlternateSubst(oldSubTable, newSubTable, overflowRecord): 516 ok = 1 517 newSubTable.Format = oldSubTable.Format 518 if hasattr(oldSubTable, 'sortCoverageLast'): 519 newSubTable.sortCoverageLast = oldSubTable.sortCoverageLast 520 521 oldAlts = sorted(oldSubTable.alternates.items()) 522 oldLen = len(oldAlts) 523 524 if overflowRecord.itemName in [ 'Coverage', 'RangeRecord']: 525 # Coverage table is written last. overflow is to or within the 526 # the coverage table. We will just cut the subtable in half. 527 newLen = int(oldLen/2) 528 529 elif overflowRecord.itemName == 'AlternateSet': 530 # We just need to back up by two items 531 # from the overflowed AlternateSet index to make sure the offset 532 # to the Coverage table doesn't overflow. 533 newLen = overflowRecord.itemIndex - 1 534 535 newSubTable.alternates = {} 536 for i in range(newLen, oldLen): 537 item = oldAlts[i] 538 key = item[0] 539 newSubTable.alternates[key] = item[1] 540 del oldSubTable.alternates[key] 541 542 543 return ok 544 545 546def splitLigatureSubst(oldSubTable, newSubTable, overflowRecord): 547 ok = 1 548 newSubTable.Format = oldSubTable.Format 549 oldLigs = sorted(oldSubTable.ligatures.items()) 550 oldLen = len(oldLigs) 551 552 if overflowRecord.itemName in [ 'Coverage', 'RangeRecord']: 553 # Coverage table is written last. overflow is to or within the 554 # the coverage table. We will just cut the subtable in half. 555 newLen = int(oldLen/2) 556 557 elif overflowRecord.itemName == 'LigatureSet': 558 # We just need to back up by two items 559 # from the overflowed AlternateSet index to make sure the offset 560 # to the Coverage table doesn't overflow. 561 newLen = overflowRecord.itemIndex - 1 562 563 newSubTable.ligatures = {} 564 for i in range(newLen, oldLen): 565 item = oldLigs[i] 566 key = item[0] 567 newSubTable.ligatures[key] = item[1] 568 del oldSubTable.ligatures[key] 569 570 return ok 571 572 573splitTable = { 'GSUB': { 574# 1: splitSingleSubst, 575# 2: splitMultipleSubst, 576 3: splitAlternateSubst, 577 4: splitLigatureSubst, 578# 5: splitContextSubst, 579# 6: splitChainContextSubst, 580# 7: splitExtensionSubst, 581# 8: splitReverseChainSingleSubst, 582 }, 583 'GPOS': { 584# 1: splitSinglePos, 585# 2: splitPairPos, 586# 3: splitCursivePos, 587# 4: splitMarkBasePos, 588# 5: splitMarkLigPos, 589# 6: splitMarkMarkPos, 590# 7: splitContextPos, 591# 8: splitChainContextPos, 592# 9: splitExtensionPos, 593 } 594 595 } 596 597def fixSubTableOverFlows(ttf, overflowRecord): 598 """ 599 An offset has overflowed within a sub-table. We need to divide this subtable into smaller parts. 600 """ 601 ok = 0 602 table = ttf[overflowRecord.tableType].table 603 lookup = table.LookupList.Lookup[overflowRecord.LookupListIndex] 604 subIndex = overflowRecord.SubTableIndex 605 subtable = lookup.SubTable[subIndex] 606 607 if hasattr(subtable, 'ExtSubTable'): 608 # We split the subtable of the Extension table, and add a new Extension table 609 # to contain the new subtable. 610 611 subTableType = subtable.ExtensionLookupType 612 extSubTable = subtable 613 subtable = extSubTable.ExtSubTable 614 newExtSubTableClass = lookupTypes[overflowRecord.tableType][lookup.LookupType] 615 newExtSubTable = newExtSubTableClass() 616 newExtSubTable.Format = extSubTable.Format 617 newExtSubTable.ExtensionLookupType = extSubTable.ExtensionLookupType 618 lookup.SubTable.insert(subIndex + 1, newExtSubTable) 619 620 newSubTableClass = lookupTypes[overflowRecord.tableType][subTableType] 621 newSubTable = newSubTableClass() 622 newExtSubTable.ExtSubTable = newSubTable 623 else: 624 subTableType = lookup.LookupType 625 newSubTableClass = lookupTypes[overflowRecord.tableType][subTableType] 626 newSubTable = newSubTableClass() 627 lookup.SubTable.insert(subIndex + 1, newSubTable) 628 629 if hasattr(lookup, 'SubTableCount'): # may not be defined yet. 630 lookup.SubTableCount = lookup.SubTableCount + 1 631 632 try: 633 splitFunc = splitTable[overflowRecord.tableType][subTableType] 634 except KeyError: 635 return ok 636 637 ok = splitFunc(subtable, newSubTable, overflowRecord) 638 return ok 639 640# End of OverFlow logic 641 642 643def _buildClasses(): 644 import re 645 from .otData import otData 646 647 formatPat = re.compile("([A-Za-z0-9]+)Format(\d+)$") 648 namespace = globals() 649 650 # populate module with classes 651 for name, table in otData: 652 baseClass = BaseTable 653 m = formatPat.match(name) 654 if m: 655 # XxxFormatN subtable, we only add the "base" table 656 name = m.group(1) 657 baseClass = FormatSwitchingBaseTable 658 if name not in namespace: 659 # the class doesn't exist yet, so the base implementation is used. 660 cls = type(name, (baseClass,), {}) 661 namespace[name] = cls 662 663 for base, alts in _equivalents.items(): 664 base = namespace[base] 665 for alt in alts: 666 namespace[alt] = type(alt, (base,), {}) 667 668 global lookupTypes 669 lookupTypes = { 670 'GSUB': { 671 1: SingleSubst, 672 2: MultipleSubst, 673 3: AlternateSubst, 674 4: LigatureSubst, 675 5: ContextSubst, 676 6: ChainContextSubst, 677 7: ExtensionSubst, 678 8: ReverseChainSingleSubst, 679 }, 680 'GPOS': { 681 1: SinglePos, 682 2: PairPos, 683 3: CursivePos, 684 4: MarkBasePos, 685 5: MarkLigPos, 686 6: MarkMarkPos, 687 7: ContextPos, 688 8: ChainContextPos, 689 9: ExtensionPos, 690 }, 691 } 692 lookupTypes['JSTF'] = lookupTypes['GPOS'] # JSTF contains GPOS 693 for lookupEnum in lookupTypes.values(): 694 for enum, cls in lookupEnum.items(): 695 cls.LookupType = enum 696 697 global featureParamTypes 698 featureParamTypes = { 699 'size': FeatureParamsSize, 700 } 701 for i in range(1, 20+1): 702 featureParamTypes['ss%02d' % i] = FeatureParamsStylisticSet 703 for i in range(1, 99+1): 704 featureParamTypes['cv%02d' % i] = FeatureParamsCharacterVariants 705 706 # add converters to classes 707 from .otConverters import buildConverters 708 for name, table in otData: 709 m = formatPat.match(name) 710 if m: 711 # XxxFormatN subtable, add converter to "base" table 712 name, format = m.groups() 713 format = int(format) 714 cls = namespace[name] 715 if not hasattr(cls, "converters"): 716 cls.converters = {} 717 cls.convertersByName = {} 718 converters, convertersByName = buildConverters(table[1:], namespace) 719 cls.converters[format] = converters 720 cls.convertersByName[format] = convertersByName 721 else: 722 cls = namespace[name] 723 cls.converters, cls.convertersByName = buildConverters(table, namespace) 724 725 726_buildClasses() 727 728 729def _getGlyphsFromCoverageTable(coverage): 730 if coverage is None: 731 # empty coverage table 732 return [] 733 else: 734 return coverage.glyphs 735