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