1# coding: utf-8 2"""fontTools.ttLib.tables.otTables -- A collection of classes representing the various 3OpenType subtables. 4 5Most are constructed upon import from data in otData.py, all are populated with 6converter objects from otConverters.py. 7""" 8import copy 9from enum import IntEnum 10import itertools 11from collections import defaultdict, namedtuple 12from fontTools.misc.roundTools import otRound 13from fontTools.misc.textTools import bytesjoin, pad, safeEval 14from .otBase import ( 15 BaseTable, FormatSwitchingBaseTable, ValueRecord, CountReference, 16 getFormatSwitchingBaseTableClass, 17) 18from fontTools.feaLib.lookupDebugInfo import LookupDebugInfo, LOOKUP_DEBUG_INFO_KEY 19import logging 20import struct 21 22 23log = logging.getLogger(__name__) 24 25 26class AATStateTable(object): 27 def __init__(self): 28 self.GlyphClasses = {} # GlyphID --> GlyphClass 29 self.States = [] # List of AATState, indexed by state number 30 self.PerGlyphLookups = [] # [{GlyphID:GlyphID}, ...] 31 32 33class AATState(object): 34 def __init__(self): 35 self.Transitions = {} # GlyphClass --> AATAction 36 37 38class AATAction(object): 39 _FLAGS = None 40 41 @staticmethod 42 def compileActions(font, states): 43 return (None, None) 44 45 def _writeFlagsToXML(self, xmlWriter): 46 flags = [f for f in self._FLAGS if self.__dict__[f]] 47 if flags: 48 xmlWriter.simpletag("Flags", value=",".join(flags)) 49 xmlWriter.newline() 50 if self.ReservedFlags != 0: 51 xmlWriter.simpletag( 52 "ReservedFlags", 53 value='0x%04X' % self.ReservedFlags) 54 xmlWriter.newline() 55 56 def _setFlag(self, flag): 57 assert flag in self._FLAGS, "unsupported flag %s" % flag 58 self.__dict__[flag] = True 59 60 61class RearrangementMorphAction(AATAction): 62 staticSize = 4 63 actionHeaderSize = 0 64 _FLAGS = ["MarkFirst", "DontAdvance", "MarkLast"] 65 66 _VERBS = { 67 0: "no change", 68 1: "Ax ⇒ xA", 69 2: "xD ⇒ Dx", 70 3: "AxD ⇒ DxA", 71 4: "ABx ⇒ xAB", 72 5: "ABx ⇒ xBA", 73 6: "xCD ⇒ CDx", 74 7: "xCD ⇒ DCx", 75 8: "AxCD ⇒ CDxA", 76 9: "AxCD ⇒ DCxA", 77 10: "ABxD ⇒ DxAB", 78 11: "ABxD ⇒ DxBA", 79 12: "ABxCD ⇒ CDxAB", 80 13: "ABxCD ⇒ CDxBA", 81 14: "ABxCD ⇒ DCxAB", 82 15: "ABxCD ⇒ DCxBA", 83 } 84 85 def __init__(self): 86 self.NewState = 0 87 self.Verb = 0 88 self.MarkFirst = False 89 self.DontAdvance = False 90 self.MarkLast = False 91 self.ReservedFlags = 0 92 93 def compile(self, writer, font, actionIndex): 94 assert actionIndex is None 95 writer.writeUShort(self.NewState) 96 assert self.Verb >= 0 and self.Verb <= 15, self.Verb 97 flags = self.Verb | self.ReservedFlags 98 if self.MarkFirst: flags |= 0x8000 99 if self.DontAdvance: flags |= 0x4000 100 if self.MarkLast: flags |= 0x2000 101 writer.writeUShort(flags) 102 103 def decompile(self, reader, font, actionReader): 104 assert actionReader is None 105 self.NewState = reader.readUShort() 106 flags = reader.readUShort() 107 self.Verb = flags & 0xF 108 self.MarkFirst = bool(flags & 0x8000) 109 self.DontAdvance = bool(flags & 0x4000) 110 self.MarkLast = bool(flags & 0x2000) 111 self.ReservedFlags = flags & 0x1FF0 112 113 def toXML(self, xmlWriter, font, attrs, name): 114 xmlWriter.begintag(name, **attrs) 115 xmlWriter.newline() 116 xmlWriter.simpletag("NewState", value=self.NewState) 117 xmlWriter.newline() 118 self._writeFlagsToXML(xmlWriter) 119 xmlWriter.simpletag("Verb", value=self.Verb) 120 verbComment = self._VERBS.get(self.Verb) 121 if verbComment is not None: 122 xmlWriter.comment(verbComment) 123 xmlWriter.newline() 124 xmlWriter.endtag(name) 125 xmlWriter.newline() 126 127 def fromXML(self, name, attrs, content, font): 128 self.NewState = self.Verb = self.ReservedFlags = 0 129 self.MarkFirst = self.DontAdvance = self.MarkLast = False 130 content = [t for t in content if isinstance(t, tuple)] 131 for eltName, eltAttrs, eltContent in content: 132 if eltName == "NewState": 133 self.NewState = safeEval(eltAttrs["value"]) 134 elif eltName == "Verb": 135 self.Verb = safeEval(eltAttrs["value"]) 136 elif eltName == "ReservedFlags": 137 self.ReservedFlags = safeEval(eltAttrs["value"]) 138 elif eltName == "Flags": 139 for flag in eltAttrs["value"].split(","): 140 self._setFlag(flag.strip()) 141 142 143class ContextualMorphAction(AATAction): 144 staticSize = 8 145 actionHeaderSize = 0 146 _FLAGS = ["SetMark", "DontAdvance"] 147 148 def __init__(self): 149 self.NewState = 0 150 self.SetMark, self.DontAdvance = False, False 151 self.ReservedFlags = 0 152 self.MarkIndex, self.CurrentIndex = 0xFFFF, 0xFFFF 153 154 def compile(self, writer, font, actionIndex): 155 assert actionIndex is None 156 writer.writeUShort(self.NewState) 157 flags = self.ReservedFlags 158 if self.SetMark: flags |= 0x8000 159 if self.DontAdvance: flags |= 0x4000 160 writer.writeUShort(flags) 161 writer.writeUShort(self.MarkIndex) 162 writer.writeUShort(self.CurrentIndex) 163 164 def decompile(self, reader, font, actionReader): 165 assert actionReader is None 166 self.NewState = reader.readUShort() 167 flags = reader.readUShort() 168 self.SetMark = bool(flags & 0x8000) 169 self.DontAdvance = bool(flags & 0x4000) 170 self.ReservedFlags = flags & 0x3FFF 171 self.MarkIndex = reader.readUShort() 172 self.CurrentIndex = reader.readUShort() 173 174 def toXML(self, xmlWriter, font, attrs, name): 175 xmlWriter.begintag(name, **attrs) 176 xmlWriter.newline() 177 xmlWriter.simpletag("NewState", value=self.NewState) 178 xmlWriter.newline() 179 self._writeFlagsToXML(xmlWriter) 180 xmlWriter.simpletag("MarkIndex", value=self.MarkIndex) 181 xmlWriter.newline() 182 xmlWriter.simpletag("CurrentIndex", 183 value=self.CurrentIndex) 184 xmlWriter.newline() 185 xmlWriter.endtag(name) 186 xmlWriter.newline() 187 188 def fromXML(self, name, attrs, content, font): 189 self.NewState = self.ReservedFlags = 0 190 self.SetMark = self.DontAdvance = False 191 self.MarkIndex, self.CurrentIndex = 0xFFFF, 0xFFFF 192 content = [t for t in content if isinstance(t, tuple)] 193 for eltName, eltAttrs, eltContent in content: 194 if eltName == "NewState": 195 self.NewState = safeEval(eltAttrs["value"]) 196 elif eltName == "Flags": 197 for flag in eltAttrs["value"].split(","): 198 self._setFlag(flag.strip()) 199 elif eltName == "ReservedFlags": 200 self.ReservedFlags = safeEval(eltAttrs["value"]) 201 elif eltName == "MarkIndex": 202 self.MarkIndex = safeEval(eltAttrs["value"]) 203 elif eltName == "CurrentIndex": 204 self.CurrentIndex = safeEval(eltAttrs["value"]) 205 206 207class LigAction(object): 208 def __init__(self): 209 self.Store = False 210 # GlyphIndexDelta is a (possibly negative) delta that gets 211 # added to the glyph ID at the top of the AAT runtime 212 # execution stack. It is *not* a byte offset into the 213 # morx table. The result of the addition, which is performed 214 # at run time by the shaping engine, is an index into 215 # the ligature components table. See 'morx' specification. 216 # In the AAT specification, this field is called Offset; 217 # but its meaning is quite different from other offsets 218 # in either AAT or OpenType, so we use a different name. 219 self.GlyphIndexDelta = 0 220 221 222class LigatureMorphAction(AATAction): 223 staticSize = 6 224 225 # 4 bytes for each of {action,ligComponents,ligatures}Offset 226 actionHeaderSize = 12 227 228 _FLAGS = ["SetComponent", "DontAdvance"] 229 230 def __init__(self): 231 self.NewState = 0 232 self.SetComponent, self.DontAdvance = False, False 233 self.ReservedFlags = 0 234 self.Actions = [] 235 236 def compile(self, writer, font, actionIndex): 237 assert actionIndex is not None 238 writer.writeUShort(self.NewState) 239 flags = self.ReservedFlags 240 if self.SetComponent: flags |= 0x8000 241 if self.DontAdvance: flags |= 0x4000 242 if len(self.Actions) > 0: flags |= 0x2000 243 writer.writeUShort(flags) 244 if len(self.Actions) > 0: 245 actions = self.compileLigActions() 246 writer.writeUShort(actionIndex[actions]) 247 else: 248 writer.writeUShort(0) 249 250 def decompile(self, reader, font, actionReader): 251 assert actionReader is not None 252 self.NewState = reader.readUShort() 253 flags = reader.readUShort() 254 self.SetComponent = bool(flags & 0x8000) 255 self.DontAdvance = bool(flags & 0x4000) 256 performAction = bool(flags & 0x2000) 257 # As of 2017-09-12, the 'morx' specification says that 258 # the reserved bitmask in ligature subtables is 0x3FFF. 259 # However, the specification also defines a flag 0x2000, 260 # so the reserved value should actually be 0x1FFF. 261 # TODO: Report this specification bug to Apple. 262 self.ReservedFlags = flags & 0x1FFF 263 actionIndex = reader.readUShort() 264 if performAction: 265 self.Actions = self._decompileLigActions( 266 actionReader, actionIndex) 267 else: 268 self.Actions = [] 269 270 @staticmethod 271 def compileActions(font, states): 272 result, actions, actionIndex = b"", set(), {} 273 for state in states: 274 for _glyphClass, trans in state.Transitions.items(): 275 actions.add(trans.compileLigActions()) 276 # Sort the compiled actions in decreasing order of 277 # length, so that the longer sequence come before the 278 # shorter ones. For each compiled action ABCD, its 279 # suffixes BCD, CD, and D do not be encoded separately 280 # (in case they occur); instead, we can just store an 281 # index that points into the middle of the longer 282 # sequence. Every compiled AAT ligature sequence is 283 # terminated with an end-of-sequence flag, which can 284 # only be set on the last element of the sequence. 285 # Therefore, it is sufficient to consider just the 286 # suffixes. 287 for a in sorted(actions, key=lambda x:(-len(x), x)): 288 if a not in actionIndex: 289 for i in range(0, len(a), 4): 290 suffix = a[i:] 291 suffixIndex = (len(result) + i) // 4 292 actionIndex.setdefault( 293 suffix, suffixIndex) 294 result += a 295 result = pad(result, 4) 296 return (result, actionIndex) 297 298 def compileLigActions(self): 299 result = [] 300 for i, action in enumerate(self.Actions): 301 last = (i == len(self.Actions) - 1) 302 value = action.GlyphIndexDelta & 0x3FFFFFFF 303 value |= 0x80000000 if last else 0 304 value |= 0x40000000 if action.Store else 0 305 result.append(struct.pack(">L", value)) 306 return bytesjoin(result) 307 308 def _decompileLigActions(self, actionReader, actionIndex): 309 actions = [] 310 last = False 311 reader = actionReader.getSubReader( 312 actionReader.pos + actionIndex * 4) 313 while not last: 314 value = reader.readULong() 315 last = bool(value & 0x80000000) 316 action = LigAction() 317 actions.append(action) 318 action.Store = bool(value & 0x40000000) 319 delta = value & 0x3FFFFFFF 320 if delta >= 0x20000000: # sign-extend 30-bit value 321 delta = -0x40000000 + delta 322 action.GlyphIndexDelta = delta 323 return actions 324 325 def fromXML(self, name, attrs, content, font): 326 self.NewState = self.ReservedFlags = 0 327 self.SetComponent = self.DontAdvance = False 328 self.ReservedFlags = 0 329 self.Actions = [] 330 content = [t for t in content if isinstance(t, tuple)] 331 for eltName, eltAttrs, eltContent in content: 332 if eltName == "NewState": 333 self.NewState = safeEval(eltAttrs["value"]) 334 elif eltName == "Flags": 335 for flag in eltAttrs["value"].split(","): 336 self._setFlag(flag.strip()) 337 elif eltName == "ReservedFlags": 338 self.ReservedFlags = safeEval(eltAttrs["value"]) 339 elif eltName == "Action": 340 action = LigAction() 341 flags = eltAttrs.get("Flags", "").split(",") 342 flags = [f.strip() for f in flags] 343 action.Store = "Store" in flags 344 action.GlyphIndexDelta = safeEval( 345 eltAttrs["GlyphIndexDelta"]) 346 self.Actions.append(action) 347 348 def toXML(self, xmlWriter, font, attrs, name): 349 xmlWriter.begintag(name, **attrs) 350 xmlWriter.newline() 351 xmlWriter.simpletag("NewState", value=self.NewState) 352 xmlWriter.newline() 353 self._writeFlagsToXML(xmlWriter) 354 for action in self.Actions: 355 attribs = [("GlyphIndexDelta", action.GlyphIndexDelta)] 356 if action.Store: 357 attribs.append(("Flags", "Store")) 358 xmlWriter.simpletag("Action", attribs) 359 xmlWriter.newline() 360 xmlWriter.endtag(name) 361 xmlWriter.newline() 362 363 364class InsertionMorphAction(AATAction): 365 staticSize = 8 366 actionHeaderSize = 4 # 4 bytes for actionOffset 367 _FLAGS = ["SetMark", "DontAdvance", 368 "CurrentIsKashidaLike", "MarkedIsKashidaLike", 369 "CurrentInsertBefore", "MarkedInsertBefore"] 370 371 def __init__(self): 372 self.NewState = 0 373 for flag in self._FLAGS: 374 setattr(self, flag, False) 375 self.ReservedFlags = 0 376 self.CurrentInsertionAction, self.MarkedInsertionAction = [], [] 377 378 def compile(self, writer, font, actionIndex): 379 assert actionIndex is not None 380 writer.writeUShort(self.NewState) 381 flags = self.ReservedFlags 382 if self.SetMark: flags |= 0x8000 383 if self.DontAdvance: flags |= 0x4000 384 if self.CurrentIsKashidaLike: flags |= 0x2000 385 if self.MarkedIsKashidaLike: flags |= 0x1000 386 if self.CurrentInsertBefore: flags |= 0x0800 387 if self.MarkedInsertBefore: flags |= 0x0400 388 flags |= len(self.CurrentInsertionAction) << 5 389 flags |= len(self.MarkedInsertionAction) 390 writer.writeUShort(flags) 391 if len(self.CurrentInsertionAction) > 0: 392 currentIndex = actionIndex[ 393 tuple(self.CurrentInsertionAction)] 394 else: 395 currentIndex = 0xFFFF 396 writer.writeUShort(currentIndex) 397 if len(self.MarkedInsertionAction) > 0: 398 markedIndex = actionIndex[ 399 tuple(self.MarkedInsertionAction)] 400 else: 401 markedIndex = 0xFFFF 402 writer.writeUShort(markedIndex) 403 404 def decompile(self, reader, font, actionReader): 405 assert actionReader is not None 406 self.NewState = reader.readUShort() 407 flags = reader.readUShort() 408 self.SetMark = bool(flags & 0x8000) 409 self.DontAdvance = bool(flags & 0x4000) 410 self.CurrentIsKashidaLike = bool(flags & 0x2000) 411 self.MarkedIsKashidaLike = bool(flags & 0x1000) 412 self.CurrentInsertBefore = bool(flags & 0x0800) 413 self.MarkedInsertBefore = bool(flags & 0x0400) 414 self.CurrentInsertionAction = self._decompileInsertionAction( 415 actionReader, font, 416 index=reader.readUShort(), 417 count=((flags & 0x03E0) >> 5)) 418 self.MarkedInsertionAction = self._decompileInsertionAction( 419 actionReader, font, 420 index=reader.readUShort(), 421 count=(flags & 0x001F)) 422 423 def _decompileInsertionAction(self, actionReader, font, index, count): 424 if index == 0xFFFF or count == 0: 425 return [] 426 reader = actionReader.getSubReader( 427 actionReader.pos + index * 2) 428 return font.getGlyphNameMany(reader.readUShortArray(count)) 429 430 def toXML(self, xmlWriter, font, attrs, name): 431 xmlWriter.begintag(name, **attrs) 432 xmlWriter.newline() 433 xmlWriter.simpletag("NewState", value=self.NewState) 434 xmlWriter.newline() 435 self._writeFlagsToXML(xmlWriter) 436 for g in self.CurrentInsertionAction: 437 xmlWriter.simpletag("CurrentInsertionAction", glyph=g) 438 xmlWriter.newline() 439 for g in self.MarkedInsertionAction: 440 xmlWriter.simpletag("MarkedInsertionAction", glyph=g) 441 xmlWriter.newline() 442 xmlWriter.endtag(name) 443 xmlWriter.newline() 444 445 def fromXML(self, name, attrs, content, font): 446 self.__init__() 447 content = [t for t in content if isinstance(t, tuple)] 448 for eltName, eltAttrs, eltContent in content: 449 if eltName == "NewState": 450 self.NewState = safeEval(eltAttrs["value"]) 451 elif eltName == "Flags": 452 for flag in eltAttrs["value"].split(","): 453 self._setFlag(flag.strip()) 454 elif eltName == "CurrentInsertionAction": 455 self.CurrentInsertionAction.append( 456 eltAttrs["glyph"]) 457 elif eltName == "MarkedInsertionAction": 458 self.MarkedInsertionAction.append( 459 eltAttrs["glyph"]) 460 else: 461 assert False, eltName 462 463 @staticmethod 464 def compileActions(font, states): 465 actions, actionIndex, result = set(), {}, b"" 466 for state in states: 467 for _glyphClass, trans in state.Transitions.items(): 468 if trans.CurrentInsertionAction is not None: 469 actions.add(tuple(trans.CurrentInsertionAction)) 470 if trans.MarkedInsertionAction is not None: 471 actions.add(tuple(trans.MarkedInsertionAction)) 472 # Sort the compiled actions in decreasing order of 473 # length, so that the longer sequence come before the 474 # shorter ones. 475 for action in sorted(actions, key=lambda x:(-len(x), x)): 476 # We insert all sub-sequences of the action glyph sequence 477 # into actionIndex. For example, if one action triggers on 478 # glyph sequence [A, B, C, D, E] and another action triggers 479 # on [C, D], we return result=[A, B, C, D, E] (as list of 480 # encoded glyph IDs), and actionIndex={('A','B','C','D','E'): 0, 481 # ('C','D'): 2}. 482 if action in actionIndex: 483 continue 484 for start in range(0, len(action)): 485 startIndex = (len(result) // 2) + start 486 for limit in range(start, len(action)): 487 glyphs = action[start : limit + 1] 488 actionIndex.setdefault(glyphs, startIndex) 489 for glyph in action: 490 glyphID = font.getGlyphID(glyph) 491 result += struct.pack(">H", glyphID) 492 return result, actionIndex 493 494 495class FeatureParams(BaseTable): 496 497 def compile(self, writer, font): 498 assert featureParamTypes.get(writer['FeatureTag']) == self.__class__, "Wrong FeatureParams type for feature '%s': %s" % (writer['FeatureTag'], self.__class__.__name__) 499 BaseTable.compile(self, writer, font) 500 501 def toXML(self, xmlWriter, font, attrs=None, name=None): 502 BaseTable.toXML(self, xmlWriter, font, attrs, name=self.__class__.__name__) 503 504class FeatureParamsSize(FeatureParams): 505 pass 506 507class FeatureParamsStylisticSet(FeatureParams): 508 pass 509 510class FeatureParamsCharacterVariants(FeatureParams): 511 pass 512 513class Coverage(FormatSwitchingBaseTable): 514 515 # manual implementation to get rid of glyphID dependencies 516 517 def populateDefaults(self, propagator=None): 518 if not hasattr(self, 'glyphs'): 519 self.glyphs = [] 520 521 def postRead(self, rawTable, font): 522 if self.Format == 1: 523 self.glyphs = rawTable["GlyphArray"] 524 elif self.Format == 2: 525 glyphs = self.glyphs = [] 526 ranges = rawTable["RangeRecord"] 527 # Some SIL fonts have coverage entries that don't have sorted 528 # StartCoverageIndex. If it is so, fixup and warn. We undo 529 # this when writing font out. 530 sorted_ranges = sorted(ranges, key=lambda a: a.StartCoverageIndex) 531 if ranges != sorted_ranges: 532 log.warning("GSUB/GPOS Coverage is not sorted by glyph ids.") 533 ranges = sorted_ranges 534 del sorted_ranges 535 for r in ranges: 536 start = r.Start 537 end = r.End 538 startID = font.getGlyphID(start) 539 endID = font.getGlyphID(end) + 1 540 glyphs.extend(font.getGlyphNameMany(range(startID, endID))) 541 else: 542 self.glyphs = [] 543 log.warning("Unknown Coverage format: %s", self.Format) 544 del self.Format # Don't need this anymore 545 546 def preWrite(self, font): 547 glyphs = getattr(self, "glyphs", None) 548 if glyphs is None: 549 glyphs = self.glyphs = [] 550 format = 1 551 rawTable = {"GlyphArray": glyphs} 552 if glyphs: 553 # find out whether Format 2 is more compact or not 554 glyphIDs = font.getGlyphIDMany(glyphs) 555 brokenOrder = sorted(glyphIDs) != glyphIDs 556 557 last = glyphIDs[0] 558 ranges = [[last]] 559 for glyphID in glyphIDs[1:]: 560 if glyphID != last + 1: 561 ranges[-1].append(last) 562 ranges.append([glyphID]) 563 last = glyphID 564 ranges[-1].append(last) 565 566 if brokenOrder or len(ranges) * 3 < len(glyphs): # 3 words vs. 1 word 567 # Format 2 is more compact 568 index = 0 569 for i in range(len(ranges)): 570 start, end = ranges[i] 571 r = RangeRecord() 572 r.StartID = start 573 r.Start = font.getGlyphName(start) 574 r.End = font.getGlyphName(end) 575 r.StartCoverageIndex = index 576 ranges[i] = r 577 index = index + end - start + 1 578 if brokenOrder: 579 log.warning("GSUB/GPOS Coverage is not sorted by glyph ids.") 580 ranges.sort(key=lambda a: a.StartID) 581 for r in ranges: 582 del r.StartID 583 format = 2 584 rawTable = {"RangeRecord": ranges} 585 #else: 586 # fallthrough; Format 1 is more compact 587 self.Format = format 588 return rawTable 589 590 def toXML2(self, xmlWriter, font): 591 for glyphName in getattr(self, "glyphs", []): 592 xmlWriter.simpletag("Glyph", value=glyphName) 593 xmlWriter.newline() 594 595 def fromXML(self, name, attrs, content, font): 596 glyphs = getattr(self, "glyphs", None) 597 if glyphs is None: 598 glyphs = [] 599 self.glyphs = glyphs 600 glyphs.append(attrs["value"]) 601 602 603class DeltaSetIndexMap(getFormatSwitchingBaseTableClass("uint8")): 604 605 def populateDefaults(self, propagator=None): 606 if not hasattr(self, 'mapping'): 607 self.mapping = [] 608 609 def postRead(self, rawTable, font): 610 assert (rawTable['EntryFormat'] & 0xFFC0) == 0 611 self.mapping = rawTable['mapping'] 612 613 @staticmethod 614 def getEntryFormat(mapping): 615 ored = 0 616 for idx in mapping: 617 ored |= idx 618 619 inner = ored & 0xFFFF 620 innerBits = 0 621 while inner: 622 innerBits += 1 623 inner >>= 1 624 innerBits = max(innerBits, 1) 625 assert innerBits <= 16 626 627 ored = (ored >> (16-innerBits)) | (ored & ((1<<innerBits)-1)) 628 if ored <= 0x000000FF: 629 entrySize = 1 630 elif ored <= 0x0000FFFF: 631 entrySize = 2 632 elif ored <= 0x00FFFFFF: 633 entrySize = 3 634 else: 635 entrySize = 4 636 637 return ((entrySize - 1) << 4) | (innerBits - 1) 638 639 def preWrite(self, font): 640 mapping = getattr(self, "mapping", None) 641 if mapping is None: 642 mapping = self.mapping = [] 643 self.Format = 1 if len(mapping) > 0xFFFF else 0 644 rawTable = self.__dict__.copy() 645 rawTable['MappingCount'] = len(mapping) 646 rawTable['EntryFormat'] = self.getEntryFormat(mapping) 647 return rawTable 648 649 def toXML2(self, xmlWriter, font): 650 for i, value in enumerate(getattr(self, "mapping", [])): 651 attrs = ( 652 ('index', i), 653 ('outer', value >> 16), 654 ('inner', value & 0xFFFF), 655 ) 656 xmlWriter.simpletag("Map", attrs) 657 xmlWriter.newline() 658 659 def fromXML(self, name, attrs, content, font): 660 mapping = getattr(self, "mapping", None) 661 if mapping is None: 662 self.mapping = mapping = [] 663 index = safeEval(attrs['index']) 664 outer = safeEval(attrs['outer']) 665 inner = safeEval(attrs['inner']) 666 assert inner <= 0xFFFF 667 mapping.insert(index, (outer << 16) | inner) 668 669 670class VarIdxMap(BaseTable): 671 672 def populateDefaults(self, propagator=None): 673 if not hasattr(self, 'mapping'): 674 self.mapping = {} 675 676 def postRead(self, rawTable, font): 677 assert (rawTable['EntryFormat'] & 0xFFC0) == 0 678 glyphOrder = font.getGlyphOrder() 679 mapList = rawTable['mapping'] 680 mapList.extend([mapList[-1]] * (len(glyphOrder) - len(mapList))) 681 self.mapping = dict(zip(glyphOrder, mapList)) 682 683 def preWrite(self, font): 684 mapping = getattr(self, "mapping", None) 685 if mapping is None: 686 mapping = self.mapping = {} 687 688 glyphOrder = font.getGlyphOrder() 689 mapping = [mapping[g] for g in glyphOrder] 690 while len(mapping) > 1 and mapping[-2] == mapping[-1]: 691 del mapping[-1] 692 693 rawTable = {'mapping': mapping} 694 rawTable['MappingCount'] = len(mapping) 695 rawTable['EntryFormat'] = DeltaSetIndexMap.getEntryFormat(mapping) 696 return rawTable 697 698 def toXML2(self, xmlWriter, font): 699 for glyph, value in sorted(getattr(self, "mapping", {}).items()): 700 attrs = ( 701 ('glyph', glyph), 702 ('outer', value >> 16), 703 ('inner', value & 0xFFFF), 704 ) 705 xmlWriter.simpletag("Map", attrs) 706 xmlWriter.newline() 707 708 def fromXML(self, name, attrs, content, font): 709 mapping = getattr(self, "mapping", None) 710 if mapping is None: 711 mapping = {} 712 self.mapping = mapping 713 try: 714 glyph = attrs['glyph'] 715 except: # https://github.com/fonttools/fonttools/commit/21cbab8ce9ded3356fef3745122da64dcaf314e9#commitcomment-27649836 716 glyph = font.getGlyphOrder()[attrs['index']] 717 outer = safeEval(attrs['outer']) 718 inner = safeEval(attrs['inner']) 719 assert inner <= 0xFFFF 720 mapping[glyph] = (outer << 16) | inner 721 722 723class VarRegionList(BaseTable): 724 725 def preWrite(self, font): 726 # The OT spec says VarStore.VarRegionList.RegionAxisCount should always 727 # be equal to the fvar.axisCount, and OTS < v8.0.0 enforces this rule 728 # even when the VarRegionList is empty. We can't treat RegionAxisCount 729 # like a normal propagated count (== len(Region[i].VarRegionAxis)), 730 # otherwise it would default to 0 if VarRegionList is empty. 731 # Thus, we force it to always be equal to fvar.axisCount. 732 # https://github.com/khaledhosny/ots/pull/192 733 fvarTable = font.get("fvar") 734 if fvarTable: 735 self.RegionAxisCount = len(fvarTable.axes) 736 return { 737 **self.__dict__, 738 "RegionAxisCount": CountReference(self.__dict__, "RegionAxisCount") 739 } 740 741 742class SingleSubst(FormatSwitchingBaseTable): 743 744 def populateDefaults(self, propagator=None): 745 if not hasattr(self, 'mapping'): 746 self.mapping = {} 747 748 def postRead(self, rawTable, font): 749 mapping = {} 750 input = _getGlyphsFromCoverageTable(rawTable["Coverage"]) 751 if self.Format == 1: 752 delta = rawTable["DeltaGlyphID"] 753 inputGIDS = font.getGlyphIDMany(input) 754 outGIDS = [ (glyphID + delta) % 65536 for glyphID in inputGIDS ] 755 outNames = font.getGlyphNameMany(outGIDS) 756 for inp, out in zip(input, outNames): 757 mapping[inp] = out 758 elif self.Format == 2: 759 assert len(input) == rawTable["GlyphCount"], \ 760 "invalid SingleSubstFormat2 table" 761 subst = rawTable["Substitute"] 762 for inp, sub in zip(input, subst): 763 mapping[inp] = sub 764 else: 765 assert 0, "unknown format: %s" % self.Format 766 self.mapping = mapping 767 del self.Format # Don't need this anymore 768 769 def preWrite(self, font): 770 mapping = getattr(self, "mapping", None) 771 if mapping is None: 772 mapping = self.mapping = {} 773 items = list(mapping.items()) 774 getGlyphID = font.getGlyphID 775 gidItems = [(getGlyphID(a), getGlyphID(b)) for a,b in items] 776 sortableItems = sorted(zip(gidItems, items)) 777 778 # figure out format 779 format = 2 780 delta = None 781 for inID, outID in gidItems: 782 if delta is None: 783 delta = (outID - inID) % 65536 784 785 if (inID + delta) % 65536 != outID: 786 break 787 else: 788 if delta is None: 789 # the mapping is empty, better use format 2 790 format = 2 791 else: 792 format = 1 793 794 rawTable = {} 795 self.Format = format 796 cov = Coverage() 797 input = [ item [1][0] for item in sortableItems] 798 subst = [ item [1][1] for item in sortableItems] 799 cov.glyphs = input 800 rawTable["Coverage"] = cov 801 if format == 1: 802 assert delta is not None 803 rawTable["DeltaGlyphID"] = delta 804 else: 805 rawTable["Substitute"] = subst 806 return rawTable 807 808 def toXML2(self, xmlWriter, font): 809 items = sorted(self.mapping.items()) 810 for inGlyph, outGlyph in items: 811 xmlWriter.simpletag("Substitution", 812 [("in", inGlyph), ("out", outGlyph)]) 813 xmlWriter.newline() 814 815 def fromXML(self, name, attrs, content, font): 816 mapping = getattr(self, "mapping", None) 817 if mapping is None: 818 mapping = {} 819 self.mapping = mapping 820 mapping[attrs["in"]] = attrs["out"] 821 822 823class MultipleSubst(FormatSwitchingBaseTable): 824 825 def populateDefaults(self, propagator=None): 826 if not hasattr(self, 'mapping'): 827 self.mapping = {} 828 829 def postRead(self, rawTable, font): 830 mapping = {} 831 if self.Format == 1: 832 glyphs = _getGlyphsFromCoverageTable(rawTable["Coverage"]) 833 subst = [s.Substitute for s in rawTable["Sequence"]] 834 mapping = dict(zip(glyphs, subst)) 835 else: 836 assert 0, "unknown format: %s" % self.Format 837 self.mapping = mapping 838 del self.Format # Don't need this anymore 839 840 def preWrite(self, font): 841 mapping = getattr(self, "mapping", None) 842 if mapping is None: 843 mapping = self.mapping = {} 844 cov = Coverage() 845 cov.glyphs = sorted(list(mapping.keys()), key=font.getGlyphID) 846 self.Format = 1 847 rawTable = { 848 "Coverage": cov, 849 "Sequence": [self.makeSequence_(mapping[glyph]) 850 for glyph in cov.glyphs], 851 } 852 return rawTable 853 854 def toXML2(self, xmlWriter, font): 855 items = sorted(self.mapping.items()) 856 for inGlyph, outGlyphs in items: 857 out = ",".join(outGlyphs) 858 xmlWriter.simpletag("Substitution", 859 [("in", inGlyph), ("out", out)]) 860 xmlWriter.newline() 861 862 def fromXML(self, name, attrs, content, font): 863 mapping = getattr(self, "mapping", None) 864 if mapping is None: 865 mapping = {} 866 self.mapping = mapping 867 868 # TTX v3.0 and earlier. 869 if name == "Coverage": 870 self.old_coverage_ = [] 871 for element in content: 872 if not isinstance(element, tuple): 873 continue 874 element_name, element_attrs, _ = element 875 if element_name == "Glyph": 876 self.old_coverage_.append(element_attrs["value"]) 877 return 878 if name == "Sequence": 879 index = int(attrs.get("index", len(mapping))) 880 glyph = self.old_coverage_[index] 881 glyph_mapping = mapping[glyph] = [] 882 for element in content: 883 if not isinstance(element, tuple): 884 continue 885 element_name, element_attrs, _ = element 886 if element_name == "Substitute": 887 glyph_mapping.append(element_attrs["value"]) 888 return 889 890 # TTX v3.1 and later. 891 outGlyphs = attrs["out"].split(",") if attrs["out"] else [] 892 mapping[attrs["in"]] = [g.strip() for g in outGlyphs] 893 894 @staticmethod 895 def makeSequence_(g): 896 seq = Sequence() 897 seq.Substitute = g 898 return seq 899 900 901class ClassDef(FormatSwitchingBaseTable): 902 903 def populateDefaults(self, propagator=None): 904 if not hasattr(self, 'classDefs'): 905 self.classDefs = {} 906 907 def postRead(self, rawTable, font): 908 classDefs = {} 909 910 if self.Format == 1: 911 start = rawTable["StartGlyph"] 912 classList = rawTable["ClassValueArray"] 913 startID = font.getGlyphID(start) 914 endID = startID + len(classList) 915 glyphNames = font.getGlyphNameMany(range(startID, endID)) 916 for glyphName, cls in zip(glyphNames, classList): 917 if cls: 918 classDefs[glyphName] = cls 919 920 elif self.Format == 2: 921 records = rawTable["ClassRangeRecord"] 922 for rec in records: 923 cls = rec.Class 924 if not cls: 925 continue 926 start = rec.Start 927 end = rec.End 928 startID = font.getGlyphID(start) 929 endID = font.getGlyphID(end) + 1 930 glyphNames = font.getGlyphNameMany(range(startID, endID)) 931 for glyphName in glyphNames: 932 classDefs[glyphName] = cls 933 else: 934 log.warning("Unknown ClassDef format: %s", self.Format) 935 self.classDefs = classDefs 936 del self.Format # Don't need this anymore 937 938 def _getClassRanges(self, font): 939 classDefs = getattr(self, "classDefs", None) 940 if classDefs is None: 941 self.classDefs = {} 942 return 943 getGlyphID = font.getGlyphID 944 items = [] 945 for glyphName, cls in classDefs.items(): 946 if not cls: 947 continue 948 items.append((getGlyphID(glyphName), glyphName, cls)) 949 if items: 950 items.sort() 951 last, lastName, lastCls = items[0] 952 ranges = [[lastCls, last, lastName]] 953 for glyphID, glyphName, cls in items[1:]: 954 if glyphID != last + 1 or cls != lastCls: 955 ranges[-1].extend([last, lastName]) 956 ranges.append([cls, glyphID, glyphName]) 957 last = glyphID 958 lastName = glyphName 959 lastCls = cls 960 ranges[-1].extend([last, lastName]) 961 return ranges 962 963 def preWrite(self, font): 964 format = 2 965 rawTable = {"ClassRangeRecord": []} 966 ranges = self._getClassRanges(font) 967 if ranges: 968 startGlyph = ranges[0][1] 969 endGlyph = ranges[-1][3] 970 glyphCount = endGlyph - startGlyph + 1 971 if len(ranges) * 3 < glyphCount + 1: 972 # Format 2 is more compact 973 for i in range(len(ranges)): 974 cls, start, startName, end, endName = ranges[i] 975 rec = ClassRangeRecord() 976 rec.Start = startName 977 rec.End = endName 978 rec.Class = cls 979 ranges[i] = rec 980 format = 2 981 rawTable = {"ClassRangeRecord": ranges} 982 else: 983 # Format 1 is more compact 984 startGlyphName = ranges[0][2] 985 classes = [0] * glyphCount 986 for cls, start, startName, end, endName in ranges: 987 for g in range(start - startGlyph, end - startGlyph + 1): 988 classes[g] = cls 989 format = 1 990 rawTable = {"StartGlyph": startGlyphName, "ClassValueArray": classes} 991 self.Format = format 992 return rawTable 993 994 def toXML2(self, xmlWriter, font): 995 items = sorted(self.classDefs.items()) 996 for glyphName, cls in items: 997 xmlWriter.simpletag("ClassDef", [("glyph", glyphName), ("class", cls)]) 998 xmlWriter.newline() 999 1000 def fromXML(self, name, attrs, content, font): 1001 classDefs = getattr(self, "classDefs", None) 1002 if classDefs is None: 1003 classDefs = {} 1004 self.classDefs = classDefs 1005 classDefs[attrs["glyph"]] = int(attrs["class"]) 1006 1007 1008class AlternateSubst(FormatSwitchingBaseTable): 1009 1010 def populateDefaults(self, propagator=None): 1011 if not hasattr(self, 'alternates'): 1012 self.alternates = {} 1013 1014 def postRead(self, rawTable, font): 1015 alternates = {} 1016 if self.Format == 1: 1017 input = _getGlyphsFromCoverageTable(rawTable["Coverage"]) 1018 alts = rawTable["AlternateSet"] 1019 assert len(input) == len(alts) 1020 for inp,alt in zip(input,alts): 1021 alternates[inp] = alt.Alternate 1022 else: 1023 assert 0, "unknown format: %s" % self.Format 1024 self.alternates = alternates 1025 del self.Format # Don't need this anymore 1026 1027 def preWrite(self, font): 1028 self.Format = 1 1029 alternates = getattr(self, "alternates", None) 1030 if alternates is None: 1031 alternates = self.alternates = {} 1032 items = list(alternates.items()) 1033 for i in range(len(items)): 1034 glyphName, set = items[i] 1035 items[i] = font.getGlyphID(glyphName), glyphName, set 1036 items.sort() 1037 cov = Coverage() 1038 cov.glyphs = [ item[1] for item in items] 1039 alternates = [] 1040 setList = [ item[-1] for item in items] 1041 for set in setList: 1042 alts = AlternateSet() 1043 alts.Alternate = set 1044 alternates.append(alts) 1045 # a special case to deal with the fact that several hundred Adobe Japan1-5 1046 # CJK fonts will overflow an offset if the coverage table isn't pushed to the end. 1047 # Also useful in that when splitting a sub-table because of an offset overflow 1048 # I don't need to calculate the change in the subtable offset due to the change in the coverage table size. 1049 # Allows packing more rules in subtable. 1050 self.sortCoverageLast = 1 1051 return {"Coverage": cov, "AlternateSet": alternates} 1052 1053 def toXML2(self, xmlWriter, font): 1054 items = sorted(self.alternates.items()) 1055 for glyphName, alternates in items: 1056 xmlWriter.begintag("AlternateSet", glyph=glyphName) 1057 xmlWriter.newline() 1058 for alt in alternates: 1059 xmlWriter.simpletag("Alternate", glyph=alt) 1060 xmlWriter.newline() 1061 xmlWriter.endtag("AlternateSet") 1062 xmlWriter.newline() 1063 1064 def fromXML(self, name, attrs, content, font): 1065 alternates = getattr(self, "alternates", None) 1066 if alternates is None: 1067 alternates = {} 1068 self.alternates = alternates 1069 glyphName = attrs["glyph"] 1070 set = [] 1071 alternates[glyphName] = set 1072 for element in content: 1073 if not isinstance(element, tuple): 1074 continue 1075 name, attrs, content = element 1076 set.append(attrs["glyph"]) 1077 1078 1079class LigatureSubst(FormatSwitchingBaseTable): 1080 1081 def populateDefaults(self, propagator=None): 1082 if not hasattr(self, 'ligatures'): 1083 self.ligatures = {} 1084 1085 def postRead(self, rawTable, font): 1086 ligatures = {} 1087 if self.Format == 1: 1088 input = _getGlyphsFromCoverageTable(rawTable["Coverage"]) 1089 ligSets = rawTable["LigatureSet"] 1090 assert len(input) == len(ligSets) 1091 for i in range(len(input)): 1092 ligatures[input[i]] = ligSets[i].Ligature 1093 else: 1094 assert 0, "unknown format: %s" % self.Format 1095 self.ligatures = ligatures 1096 del self.Format # Don't need this anymore 1097 1098 def preWrite(self, font): 1099 self.Format = 1 1100 ligatures = getattr(self, "ligatures", None) 1101 if ligatures is None: 1102 ligatures = self.ligatures = {} 1103 1104 if ligatures and isinstance(next(iter(ligatures)), tuple): 1105 # New high-level API in v3.1 and later. Note that we just support compiling this 1106 # for now. We don't load to this API, and don't do XML with it. 1107 1108 # ligatures is map from components-sequence to lig-glyph 1109 newLigatures = dict() 1110 for comps,lig in sorted(ligatures.items(), key=lambda item: (-len(item[0]), item[0])): 1111 ligature = Ligature() 1112 ligature.Component = comps[1:] 1113 ligature.CompCount = len(comps) 1114 ligature.LigGlyph = lig 1115 newLigatures.setdefault(comps[0], []).append(ligature) 1116 ligatures = newLigatures 1117 1118 items = list(ligatures.items()) 1119 for i in range(len(items)): 1120 glyphName, set = items[i] 1121 items[i] = font.getGlyphID(glyphName), glyphName, set 1122 items.sort() 1123 cov = Coverage() 1124 cov.glyphs = [ item[1] for item in items] 1125 1126 ligSets = [] 1127 setList = [ item[-1] for item in items ] 1128 for set in setList: 1129 ligSet = LigatureSet() 1130 ligs = ligSet.Ligature = [] 1131 for lig in set: 1132 ligs.append(lig) 1133 ligSets.append(ligSet) 1134 # Useful in that when splitting a sub-table because of an offset overflow 1135 # I don't need to calculate the change in subtabl offset due to the coverage table size. 1136 # Allows packing more rules in subtable. 1137 self.sortCoverageLast = 1 1138 return {"Coverage": cov, "LigatureSet": ligSets} 1139 1140 def toXML2(self, xmlWriter, font): 1141 items = sorted(self.ligatures.items()) 1142 for glyphName, ligSets in items: 1143 xmlWriter.begintag("LigatureSet", glyph=glyphName) 1144 xmlWriter.newline() 1145 for lig in ligSets: 1146 xmlWriter.simpletag("Ligature", glyph=lig.LigGlyph, 1147 components=",".join(lig.Component)) 1148 xmlWriter.newline() 1149 xmlWriter.endtag("LigatureSet") 1150 xmlWriter.newline() 1151 1152 def fromXML(self, name, attrs, content, font): 1153 ligatures = getattr(self, "ligatures", None) 1154 if ligatures is None: 1155 ligatures = {} 1156 self.ligatures = ligatures 1157 glyphName = attrs["glyph"] 1158 ligs = [] 1159 ligatures[glyphName] = ligs 1160 for element in content: 1161 if not isinstance(element, tuple): 1162 continue 1163 name, attrs, content = element 1164 lig = Ligature() 1165 lig.LigGlyph = attrs["glyph"] 1166 components = attrs["components"] 1167 lig.Component = components.split(",") if components else [] 1168 lig.CompCount = len(lig.Component) 1169 ligs.append(lig) 1170 1171 1172class COLR(BaseTable): 1173 1174 def decompile(self, reader, font): 1175 # COLRv0 is exceptional in that LayerRecordCount appears *after* the 1176 # LayerRecordArray it counts, but the parser logic expects Count fields 1177 # to always precede the arrays. Here we work around this by parsing the 1178 # LayerRecordCount before the rest of the table, and storing it in 1179 # the reader's local state. 1180 subReader = reader.getSubReader(offset=0) 1181 for conv in self.getConverters(): 1182 if conv.name != "LayerRecordCount": 1183 subReader.advance(conv.staticSize) 1184 continue 1185 reader[conv.name] = conv.read(subReader, font, tableDict={}) 1186 break 1187 else: 1188 raise AssertionError("LayerRecordCount converter not found") 1189 return BaseTable.decompile(self, reader, font) 1190 1191 def preWrite(self, font): 1192 # The writer similarly assumes Count values precede the things counted, 1193 # thus here we pre-initialize a CountReference; the actual count value 1194 # will be set to the lenght of the array by the time this is assembled. 1195 self.LayerRecordCount = None 1196 return { 1197 **self.__dict__, 1198 "LayerRecordCount": CountReference(self.__dict__, "LayerRecordCount") 1199 } 1200 1201 1202class LookupList(BaseTable): 1203 @property 1204 def table(self): 1205 for l in self.Lookup: 1206 for st in l.SubTable: 1207 if type(st).__name__.endswith("Subst"): 1208 return "GSUB" 1209 if type(st).__name__.endswith("Pos"): 1210 return "GPOS" 1211 raise ValueError 1212 1213 def toXML2(self, xmlWriter, font): 1214 if not font or "Debg" not in font or LOOKUP_DEBUG_INFO_KEY not in font["Debg"].data: 1215 return super().toXML2(xmlWriter, font) 1216 debugData = font["Debg"].data[LOOKUP_DEBUG_INFO_KEY][self.table] 1217 for conv in self.getConverters(): 1218 if conv.repeat: 1219 value = getattr(self, conv.name, []) 1220 for lookupIndex, item in enumerate(value): 1221 if str(lookupIndex) in debugData: 1222 info = LookupDebugInfo(*debugData[str(lookupIndex)]) 1223 tag = info.location 1224 if info.name: 1225 tag = f'{info.name}: {tag}' 1226 if info.feature: 1227 script,language,feature = info.feature 1228 tag = f'{tag} in {feature} ({script}/{language})' 1229 xmlWriter.comment(tag) 1230 xmlWriter.newline() 1231 1232 conv.xmlWrite(xmlWriter, font, item, conv.name, 1233 [("index", lookupIndex)]) 1234 else: 1235 if conv.aux and not eval(conv.aux, None, vars(self)): 1236 continue 1237 value = getattr(self, conv.name, None) # TODO Handle defaults instead of defaulting to None! 1238 conv.xmlWrite(xmlWriter, font, value, conv.name, []) 1239 1240class BaseGlyphRecordArray(BaseTable): 1241 1242 def preWrite(self, font): 1243 self.BaseGlyphRecord = sorted( 1244 self.BaseGlyphRecord, 1245 key=lambda rec: font.getGlyphID(rec.BaseGlyph) 1246 ) 1247 return self.__dict__.copy() 1248 1249 1250class BaseGlyphList(BaseTable): 1251 1252 def preWrite(self, font): 1253 self.BaseGlyphPaintRecord = sorted( 1254 self.BaseGlyphPaintRecord, 1255 key=lambda rec: font.getGlyphID(rec.BaseGlyph) 1256 ) 1257 return self.__dict__.copy() 1258 1259 1260class ClipBox(getFormatSwitchingBaseTableClass("uint8")): 1261 1262 def as_tuple(self): 1263 return tuple(getattr(self, conv.name) for conv in self.getConverters()) 1264 1265 def __repr__(self): 1266 return f"{self.__class__.__name__}{self.as_tuple()}" 1267 1268 1269class ClipList(getFormatSwitchingBaseTableClass("uint8")): 1270 1271 def populateDefaults(self, propagator=None): 1272 if not hasattr(self, "clips"): 1273 self.clips = {} 1274 1275 def postRead(self, rawTable, font): 1276 clips = {} 1277 glyphOrder = font.getGlyphOrder() 1278 for i, rec in enumerate(rawTable["ClipRecord"]): 1279 if rec.StartGlyphID > rec.EndGlyphID: 1280 log.warning( 1281 "invalid ClipRecord[%i].StartGlyphID (%i) > " 1282 "EndGlyphID (%i); skipped", 1283 i, 1284 rec.StartGlyphID, 1285 rec.EndGlyphID, 1286 ) 1287 continue 1288 redefinedGlyphs = [] 1289 missingGlyphs = [] 1290 for glyphID in range(rec.StartGlyphID, rec.EndGlyphID + 1): 1291 try: 1292 glyph = glyphOrder[glyphID] 1293 except IndexError: 1294 missingGlyphs.append(glyphID) 1295 continue 1296 if glyph not in clips: 1297 clips[glyph] = copy.copy(rec.ClipBox) 1298 else: 1299 redefinedGlyphs.append(glyphID) 1300 if redefinedGlyphs: 1301 log.warning( 1302 "ClipRecord[%i] overlaps previous records; " 1303 "ignoring redefined clip boxes for the " 1304 "following glyph ID range: [%i-%i]", 1305 i, 1306 min(redefinedGlyphs), 1307 max(redefinedGlyphs), 1308 ) 1309 if missingGlyphs: 1310 log.warning( 1311 "ClipRecord[%i] range references missing " 1312 "glyph IDs: [%i-%i]", 1313 i, 1314 min(missingGlyphs), 1315 max(missingGlyphs), 1316 ) 1317 self.clips = clips 1318 1319 def groups(self): 1320 glyphsByClip = defaultdict(list) 1321 uniqueClips = {} 1322 for glyphName, clipBox in self.clips.items(): 1323 key = clipBox.as_tuple() 1324 glyphsByClip[key].append(glyphName) 1325 if key not in uniqueClips: 1326 uniqueClips[key] = clipBox 1327 return { 1328 frozenset(glyphs): uniqueClips[key] 1329 for key, glyphs in glyphsByClip.items() 1330 } 1331 1332 def preWrite(self, font): 1333 if not hasattr(self, "clips"): 1334 self.clips = {} 1335 clipBoxRanges = {} 1336 glyphMap = font.getReverseGlyphMap() 1337 for glyphs, clipBox in self.groups().items(): 1338 glyphIDs = sorted( 1339 glyphMap[glyphName] for glyphName in glyphs 1340 if glyphName in glyphMap 1341 ) 1342 if not glyphIDs: 1343 continue 1344 last = glyphIDs[0] 1345 ranges = [[last]] 1346 for glyphID in glyphIDs[1:]: 1347 if glyphID != last + 1: 1348 ranges[-1].append(last) 1349 ranges.append([glyphID]) 1350 last = glyphID 1351 ranges[-1].append(last) 1352 for start, end in ranges: 1353 assert (start, end) not in clipBoxRanges 1354 clipBoxRanges[(start, end)] = clipBox 1355 1356 clipRecords = [] 1357 for (start, end), clipBox in sorted(clipBoxRanges.items()): 1358 record = ClipRecord() 1359 record.StartGlyphID = start 1360 record.EndGlyphID = end 1361 record.ClipBox = clipBox 1362 clipRecords.append(record) 1363 rawTable = { 1364 "ClipCount": len(clipRecords), 1365 "ClipRecord": clipRecords, 1366 } 1367 return rawTable 1368 1369 def toXML(self, xmlWriter, font, attrs=None, name=None): 1370 tableName = name if name else self.__class__.__name__ 1371 if attrs is None: 1372 attrs = [] 1373 if hasattr(self, "Format"): 1374 attrs.append(("Format", self.Format)) 1375 xmlWriter.begintag(tableName, attrs) 1376 xmlWriter.newline() 1377 # sort clips alphabetically to ensure deterministic XML dump 1378 for glyphs, clipBox in sorted( 1379 self.groups().items(), key=lambda item: min(item[0]) 1380 ): 1381 xmlWriter.begintag("Clip") 1382 xmlWriter.newline() 1383 for glyphName in sorted(glyphs): 1384 xmlWriter.simpletag("Glyph", value=glyphName) 1385 xmlWriter.newline() 1386 xmlWriter.begintag("ClipBox", [("Format", clipBox.Format)]) 1387 xmlWriter.newline() 1388 clipBox.toXML2(xmlWriter, font) 1389 xmlWriter.endtag("ClipBox") 1390 xmlWriter.newline() 1391 xmlWriter.endtag("Clip") 1392 xmlWriter.newline() 1393 xmlWriter.endtag(tableName) 1394 xmlWriter.newline() 1395 1396 def fromXML(self, name, attrs, content, font): 1397 clips = getattr(self, "clips", None) 1398 if clips is None: 1399 self.clips = clips = {} 1400 assert name == "Clip" 1401 glyphs = [] 1402 clipBox = None 1403 for elem in content: 1404 if not isinstance(elem, tuple): 1405 continue 1406 name, attrs, content = elem 1407 if name == "Glyph": 1408 glyphs.append(attrs["value"]) 1409 elif name == "ClipBox": 1410 clipBox = ClipBox() 1411 clipBox.Format = safeEval(attrs["Format"]) 1412 for elem in content: 1413 if not isinstance(elem, tuple): 1414 continue 1415 name, attrs, content = elem 1416 clipBox.fromXML(name, attrs, content, font) 1417 if clipBox: 1418 for glyphName in glyphs: 1419 clips[glyphName] = clipBox 1420 1421 1422class ExtendMode(IntEnum): 1423 PAD = 0 1424 REPEAT = 1 1425 REFLECT = 2 1426 1427 1428# Porter-Duff modes for COLRv1 PaintComposite: 1429# https://github.com/googlefonts/colr-gradients-spec/tree/off_sub_1#compositemode-enumeration 1430class CompositeMode(IntEnum): 1431 CLEAR = 0 1432 SRC = 1 1433 DEST = 2 1434 SRC_OVER = 3 1435 DEST_OVER = 4 1436 SRC_IN = 5 1437 DEST_IN = 6 1438 SRC_OUT = 7 1439 DEST_OUT = 8 1440 SRC_ATOP = 9 1441 DEST_ATOP = 10 1442 XOR = 11 1443 PLUS = 12 1444 SCREEN = 13 1445 OVERLAY = 14 1446 DARKEN = 15 1447 LIGHTEN = 16 1448 COLOR_DODGE = 17 1449 COLOR_BURN = 18 1450 HARD_LIGHT = 19 1451 SOFT_LIGHT = 20 1452 DIFFERENCE = 21 1453 EXCLUSION = 22 1454 MULTIPLY = 23 1455 HSL_HUE = 24 1456 HSL_SATURATION = 25 1457 HSL_COLOR = 26 1458 HSL_LUMINOSITY = 27 1459 1460 1461class PaintFormat(IntEnum): 1462 PaintColrLayers = 1 1463 PaintSolid = 2 1464 PaintVarSolid = 3, 1465 PaintLinearGradient = 4 1466 PaintVarLinearGradient = 5 1467 PaintRadialGradient = 6 1468 PaintVarRadialGradient = 7 1469 PaintSweepGradient = 8 1470 PaintVarSweepGradient = 9 1471 PaintGlyph = 10 1472 PaintColrGlyph = 11 1473 PaintTransform = 12 1474 PaintVarTransform = 13 1475 PaintTranslate = 14 1476 PaintVarTranslate = 15 1477 PaintScale = 16 1478 PaintVarScale = 17 1479 PaintScaleAroundCenter = 18 1480 PaintVarScaleAroundCenter = 19 1481 PaintScaleUniform = 20 1482 PaintVarScaleUniform = 21 1483 PaintScaleUniformAroundCenter = 22 1484 PaintVarScaleUniformAroundCenter = 23 1485 PaintRotate = 24 1486 PaintVarRotate = 25 1487 PaintRotateAroundCenter = 26 1488 PaintVarRotateAroundCenter = 27 1489 PaintSkew = 28 1490 PaintVarSkew = 29 1491 PaintSkewAroundCenter = 30 1492 PaintVarSkewAroundCenter = 31 1493 PaintComposite = 32 1494 1495 1496class Paint(getFormatSwitchingBaseTableClass("uint8")): 1497 1498 def getFormatName(self): 1499 try: 1500 return PaintFormat(self.Format).name 1501 except ValueError: 1502 raise NotImplementedError(f"Unknown Paint format: {self.Format}") 1503 1504 def toXML(self, xmlWriter, font, attrs=None, name=None): 1505 tableName = name if name else self.__class__.__name__ 1506 if attrs is None: 1507 attrs = [] 1508 attrs.append(("Format", self.Format)) 1509 xmlWriter.begintag(tableName, attrs) 1510 xmlWriter.comment(self.getFormatName()) 1511 xmlWriter.newline() 1512 self.toXML2(xmlWriter, font) 1513 xmlWriter.endtag(tableName) 1514 xmlWriter.newline() 1515 1516 def getChildren(self, colr): 1517 if self.Format == PaintFormat.PaintColrLayers: 1518 # https://github.com/fonttools/fonttools/issues/2438: don't die when no LayerList exists 1519 layers = [] 1520 if colr.LayerList is not None: 1521 layers = colr.LayerList.Paint 1522 return layers[ 1523 self.FirstLayerIndex : self.FirstLayerIndex + self.NumLayers 1524 ] 1525 1526 if self.Format == PaintFormat.PaintColrGlyph: 1527 for record in colr.BaseGlyphList.BaseGlyphPaintRecord: 1528 if record.BaseGlyph == self.Glyph: 1529 return [record.Paint] 1530 else: 1531 raise KeyError(f"{self.Glyph!r} not in colr.BaseGlyphList") 1532 1533 children = [] 1534 for conv in self.getConverters(): 1535 if conv.tableClass is not None and issubclass(conv.tableClass, type(self)): 1536 children.append(getattr(self, conv.name)) 1537 1538 return children 1539 1540 def traverse(self, colr: COLR, callback): 1541 """Depth-first traversal of graph rooted at self, callback on each node.""" 1542 if not callable(callback): 1543 raise TypeError("callback must be callable") 1544 stack = [self] 1545 visited = set() 1546 while stack: 1547 current = stack.pop() 1548 if id(current) in visited: 1549 continue 1550 callback(current) 1551 visited.add(id(current)) 1552 stack.extend(reversed(current.getChildren(colr))) 1553 1554 1555# For each subtable format there is a class. However, we don't really distinguish 1556# between "field name" and "format name": often these are the same. Yet there's 1557# a whole bunch of fields with different names. The following dict is a mapping 1558# from "format name" to "field name". _buildClasses() uses this to create a 1559# subclass for each alternate field name. 1560# 1561_equivalents = { 1562 'MarkArray': ("Mark1Array",), 1563 'LangSys': ('DefaultLangSys',), 1564 'Coverage': ('MarkCoverage', 'BaseCoverage', 'LigatureCoverage', 'Mark1Coverage', 1565 'Mark2Coverage', 'BacktrackCoverage', 'InputCoverage', 1566 'LookAheadCoverage', 'VertGlyphCoverage', 'HorizGlyphCoverage', 1567 'TopAccentCoverage', 'ExtendedShapeCoverage', 'MathKernCoverage'), 1568 'ClassDef': ('ClassDef1', 'ClassDef2', 'BacktrackClassDef', 'InputClassDef', 1569 'LookAheadClassDef', 'GlyphClassDef', 'MarkAttachClassDef'), 1570 'Anchor': ('EntryAnchor', 'ExitAnchor', 'BaseAnchor', 'LigatureAnchor', 1571 'Mark2Anchor', 'MarkAnchor'), 1572 'Device': ('XPlaDevice', 'YPlaDevice', 'XAdvDevice', 'YAdvDevice', 1573 'XDeviceTable', 'YDeviceTable', 'DeviceTable'), 1574 'Axis': ('HorizAxis', 'VertAxis',), 1575 'MinMax': ('DefaultMinMax',), 1576 'BaseCoord': ('MinCoord', 'MaxCoord',), 1577 'JstfLangSys': ('DefJstfLangSys',), 1578 'JstfGSUBModList': ('ShrinkageEnableGSUB', 'ShrinkageDisableGSUB', 'ExtensionEnableGSUB', 1579 'ExtensionDisableGSUB',), 1580 'JstfGPOSModList': ('ShrinkageEnableGPOS', 'ShrinkageDisableGPOS', 'ExtensionEnableGPOS', 1581 'ExtensionDisableGPOS',), 1582 'JstfMax': ('ShrinkageJstfMax', 'ExtensionJstfMax',), 1583 'MathKern': ('TopRightMathKern', 'TopLeftMathKern', 'BottomRightMathKern', 1584 'BottomLeftMathKern'), 1585 'MathGlyphConstruction': ('VertGlyphConstruction', 'HorizGlyphConstruction'), 1586} 1587 1588# 1589# OverFlow logic, to automatically create ExtensionLookups 1590# XXX This should probably move to otBase.py 1591# 1592 1593def fixLookupOverFlows(ttf, overflowRecord): 1594 """ Either the offset from the LookupList to a lookup overflowed, or 1595 an offset from a lookup to a subtable overflowed. 1596 The table layout is: 1597 GPSO/GUSB 1598 Script List 1599 Feature List 1600 LookUpList 1601 Lookup[0] and contents 1602 SubTable offset list 1603 SubTable[0] and contents 1604 ... 1605 SubTable[n] and contents 1606 ... 1607 Lookup[n] and contents 1608 SubTable offset list 1609 SubTable[0] and contents 1610 ... 1611 SubTable[n] and contents 1612 If the offset to a lookup overflowed (SubTableIndex is None) 1613 we must promote the *previous* lookup to an Extension type. 1614 If the offset from a lookup to subtable overflowed, then we must promote it 1615 to an Extension Lookup type. 1616 """ 1617 ok = 0 1618 lookupIndex = overflowRecord.LookupListIndex 1619 if (overflowRecord.SubTableIndex is None): 1620 lookupIndex = lookupIndex - 1 1621 if lookupIndex < 0: 1622 return ok 1623 if overflowRecord.tableType == 'GSUB': 1624 extType = 7 1625 elif overflowRecord.tableType == 'GPOS': 1626 extType = 9 1627 1628 lookups = ttf[overflowRecord.tableType].table.LookupList.Lookup 1629 lookup = lookups[lookupIndex] 1630 # If the previous lookup is an extType, look further back. Very unlikely, but possible. 1631 while lookup.SubTable[0].__class__.LookupType == extType: 1632 lookupIndex = lookupIndex -1 1633 if lookupIndex < 0: 1634 return ok 1635 lookup = lookups[lookupIndex] 1636 1637 for lookupIndex in range(lookupIndex, len(lookups)): 1638 lookup = lookups[lookupIndex] 1639 if lookup.LookupType != extType: 1640 lookup.LookupType = extType 1641 for si in range(len(lookup.SubTable)): 1642 subTable = lookup.SubTable[si] 1643 extSubTableClass = lookupTypes[overflowRecord.tableType][extType] 1644 extSubTable = extSubTableClass() 1645 extSubTable.Format = 1 1646 extSubTable.ExtSubTable = subTable 1647 lookup.SubTable[si] = extSubTable 1648 ok = 1 1649 return ok 1650 1651def splitMultipleSubst(oldSubTable, newSubTable, overflowRecord): 1652 ok = 1 1653 oldMapping = sorted(oldSubTable.mapping.items()) 1654 oldLen = len(oldMapping) 1655 1656 if overflowRecord.itemName in ['Coverage', 'RangeRecord']: 1657 # Coverage table is written last. Overflow is to or within the 1658 # the coverage table. We will just cut the subtable in half. 1659 newLen = oldLen // 2 1660 1661 elif overflowRecord.itemName == 'Sequence': 1662 # We just need to back up by two items from the overflowed 1663 # Sequence index to make sure the offset to the Coverage table 1664 # doesn't overflow. 1665 newLen = overflowRecord.itemIndex - 1 1666 1667 newSubTable.mapping = {} 1668 for i in range(newLen, oldLen): 1669 item = oldMapping[i] 1670 key = item[0] 1671 newSubTable.mapping[key] = item[1] 1672 del oldSubTable.mapping[key] 1673 1674 return ok 1675 1676def splitAlternateSubst(oldSubTable, newSubTable, overflowRecord): 1677 ok = 1 1678 if hasattr(oldSubTable, 'sortCoverageLast'): 1679 newSubTable.sortCoverageLast = oldSubTable.sortCoverageLast 1680 1681 oldAlts = sorted(oldSubTable.alternates.items()) 1682 oldLen = len(oldAlts) 1683 1684 if overflowRecord.itemName in [ 'Coverage', 'RangeRecord']: 1685 # Coverage table is written last. overflow is to or within the 1686 # the coverage table. We will just cut the subtable in half. 1687 newLen = oldLen//2 1688 1689 elif overflowRecord.itemName == 'AlternateSet': 1690 # We just need to back up by two items 1691 # from the overflowed AlternateSet index to make sure the offset 1692 # to the Coverage table doesn't overflow. 1693 newLen = overflowRecord.itemIndex - 1 1694 1695 newSubTable.alternates = {} 1696 for i in range(newLen, oldLen): 1697 item = oldAlts[i] 1698 key = item[0] 1699 newSubTable.alternates[key] = item[1] 1700 del oldSubTable.alternates[key] 1701 1702 return ok 1703 1704 1705def splitLigatureSubst(oldSubTable, newSubTable, overflowRecord): 1706 ok = 1 1707 oldLigs = sorted(oldSubTable.ligatures.items()) 1708 oldLen = len(oldLigs) 1709 1710 if overflowRecord.itemName in [ 'Coverage', 'RangeRecord']: 1711 # Coverage table is written last. overflow is to or within the 1712 # the coverage table. We will just cut the subtable in half. 1713 newLen = oldLen//2 1714 1715 elif overflowRecord.itemName == 'LigatureSet': 1716 # We just need to back up by two items 1717 # from the overflowed AlternateSet index to make sure the offset 1718 # to the Coverage table doesn't overflow. 1719 newLen = overflowRecord.itemIndex - 1 1720 1721 newSubTable.ligatures = {} 1722 for i in range(newLen, oldLen): 1723 item = oldLigs[i] 1724 key = item[0] 1725 newSubTable.ligatures[key] = item[1] 1726 del oldSubTable.ligatures[key] 1727 1728 return ok 1729 1730 1731def splitPairPos(oldSubTable, newSubTable, overflowRecord): 1732 st = oldSubTable 1733 ok = False 1734 newSubTable.Format = oldSubTable.Format 1735 if oldSubTable.Format == 1 and len(oldSubTable.PairSet) > 1: 1736 for name in 'ValueFormat1', 'ValueFormat2': 1737 setattr(newSubTable, name, getattr(oldSubTable, name)) 1738 1739 # Move top half of coverage to new subtable 1740 1741 newSubTable.Coverage = oldSubTable.Coverage.__class__() 1742 1743 coverage = oldSubTable.Coverage.glyphs 1744 records = oldSubTable.PairSet 1745 1746 oldCount = len(oldSubTable.PairSet) // 2 1747 1748 oldSubTable.Coverage.glyphs = coverage[:oldCount] 1749 oldSubTable.PairSet = records[:oldCount] 1750 1751 newSubTable.Coverage.glyphs = coverage[oldCount:] 1752 newSubTable.PairSet = records[oldCount:] 1753 1754 oldSubTable.PairSetCount = len(oldSubTable.PairSet) 1755 newSubTable.PairSetCount = len(newSubTable.PairSet) 1756 1757 ok = True 1758 1759 elif oldSubTable.Format == 2 and len(oldSubTable.Class1Record) > 1: 1760 if not hasattr(oldSubTable, 'Class2Count'): 1761 oldSubTable.Class2Count = len(oldSubTable.Class1Record[0].Class2Record) 1762 for name in 'Class2Count', 'ClassDef2', 'ValueFormat1', 'ValueFormat2': 1763 setattr(newSubTable, name, getattr(oldSubTable, name)) 1764 1765 # The two subtables will still have the same ClassDef2 and the table 1766 # sharing will still cause the sharing to overflow. As such, disable 1767 # sharing on the one that is serialized second (that's oldSubTable). 1768 oldSubTable.DontShare = True 1769 1770 # Move top half of class numbers to new subtable 1771 1772 newSubTable.Coverage = oldSubTable.Coverage.__class__() 1773 newSubTable.ClassDef1 = oldSubTable.ClassDef1.__class__() 1774 1775 coverage = oldSubTable.Coverage.glyphs 1776 classDefs = oldSubTable.ClassDef1.classDefs 1777 records = oldSubTable.Class1Record 1778 1779 oldCount = len(oldSubTable.Class1Record) // 2 1780 newGlyphs = set(k for k,v in classDefs.items() if v >= oldCount) 1781 1782 oldSubTable.Coverage.glyphs = [g for g in coverage if g not in newGlyphs] 1783 oldSubTable.ClassDef1.classDefs = {k:v for k,v in classDefs.items() if v < oldCount} 1784 oldSubTable.Class1Record = records[:oldCount] 1785 1786 newSubTable.Coverage.glyphs = [g for g in coverage if g in newGlyphs] 1787 newSubTable.ClassDef1.classDefs = {k:(v-oldCount) for k,v in classDefs.items() if v > oldCount} 1788 newSubTable.Class1Record = records[oldCount:] 1789 1790 oldSubTable.Class1Count = len(oldSubTable.Class1Record) 1791 newSubTable.Class1Count = len(newSubTable.Class1Record) 1792 1793 ok = True 1794 1795 return ok 1796 1797 1798def splitMarkBasePos(oldSubTable, newSubTable, overflowRecord): 1799 # split half of the mark classes to the new subtable 1800 classCount = oldSubTable.ClassCount 1801 if classCount < 2: 1802 # oh well, not much left to split... 1803 return False 1804 1805 oldClassCount = classCount // 2 1806 newClassCount = classCount - oldClassCount 1807 1808 oldMarkCoverage, oldMarkRecords = [], [] 1809 newMarkCoverage, newMarkRecords = [], [] 1810 for glyphName, markRecord in zip( 1811 oldSubTable.MarkCoverage.glyphs, 1812 oldSubTable.MarkArray.MarkRecord 1813 ): 1814 if markRecord.Class < oldClassCount: 1815 oldMarkCoverage.append(glyphName) 1816 oldMarkRecords.append(markRecord) 1817 else: 1818 markRecord.Class -= oldClassCount 1819 newMarkCoverage.append(glyphName) 1820 newMarkRecords.append(markRecord) 1821 1822 oldBaseRecords, newBaseRecords = [], [] 1823 for rec in oldSubTable.BaseArray.BaseRecord: 1824 oldBaseRecord, newBaseRecord = rec.__class__(), rec.__class__() 1825 oldBaseRecord.BaseAnchor = rec.BaseAnchor[:oldClassCount] 1826 newBaseRecord.BaseAnchor = rec.BaseAnchor[oldClassCount:] 1827 oldBaseRecords.append(oldBaseRecord) 1828 newBaseRecords.append(newBaseRecord) 1829 1830 newSubTable.Format = oldSubTable.Format 1831 1832 oldSubTable.MarkCoverage.glyphs = oldMarkCoverage 1833 newSubTable.MarkCoverage = oldSubTable.MarkCoverage.__class__() 1834 newSubTable.MarkCoverage.glyphs = newMarkCoverage 1835 1836 # share the same BaseCoverage in both halves 1837 newSubTable.BaseCoverage = oldSubTable.BaseCoverage 1838 1839 oldSubTable.ClassCount = oldClassCount 1840 newSubTable.ClassCount = newClassCount 1841 1842 oldSubTable.MarkArray.MarkRecord = oldMarkRecords 1843 newSubTable.MarkArray = oldSubTable.MarkArray.__class__() 1844 newSubTable.MarkArray.MarkRecord = newMarkRecords 1845 1846 oldSubTable.MarkArray.MarkCount = len(oldMarkRecords) 1847 newSubTable.MarkArray.MarkCount = len(newMarkRecords) 1848 1849 oldSubTable.BaseArray.BaseRecord = oldBaseRecords 1850 newSubTable.BaseArray = oldSubTable.BaseArray.__class__() 1851 newSubTable.BaseArray.BaseRecord = newBaseRecords 1852 1853 oldSubTable.BaseArray.BaseCount = len(oldBaseRecords) 1854 newSubTable.BaseArray.BaseCount = len(newBaseRecords) 1855 1856 return True 1857 1858 1859splitTable = { 'GSUB': { 1860# 1: splitSingleSubst, 1861 2: splitMultipleSubst, 1862 3: splitAlternateSubst, 1863 4: splitLigatureSubst, 1864# 5: splitContextSubst, 1865# 6: splitChainContextSubst, 1866# 7: splitExtensionSubst, 1867# 8: splitReverseChainSingleSubst, 1868 }, 1869 'GPOS': { 1870# 1: splitSinglePos, 1871 2: splitPairPos, 1872# 3: splitCursivePos, 1873 4: splitMarkBasePos, 1874# 5: splitMarkLigPos, 1875# 6: splitMarkMarkPos, 1876# 7: splitContextPos, 1877# 8: splitChainContextPos, 1878# 9: splitExtensionPos, 1879 } 1880 1881 } 1882 1883def fixSubTableOverFlows(ttf, overflowRecord): 1884 """ 1885 An offset has overflowed within a sub-table. We need to divide this subtable into smaller parts. 1886 """ 1887 table = ttf[overflowRecord.tableType].table 1888 lookup = table.LookupList.Lookup[overflowRecord.LookupListIndex] 1889 subIndex = overflowRecord.SubTableIndex 1890 subtable = lookup.SubTable[subIndex] 1891 1892 # First, try not sharing anything for this subtable... 1893 if not hasattr(subtable, "DontShare"): 1894 subtable.DontShare = True 1895 return True 1896 1897 if hasattr(subtable, 'ExtSubTable'): 1898 # We split the subtable of the Extension table, and add a new Extension table 1899 # to contain the new subtable. 1900 1901 subTableType = subtable.ExtSubTable.__class__.LookupType 1902 extSubTable = subtable 1903 subtable = extSubTable.ExtSubTable 1904 newExtSubTableClass = lookupTypes[overflowRecord.tableType][extSubTable.__class__.LookupType] 1905 newExtSubTable = newExtSubTableClass() 1906 newExtSubTable.Format = extSubTable.Format 1907 toInsert = newExtSubTable 1908 1909 newSubTableClass = lookupTypes[overflowRecord.tableType][subTableType] 1910 newSubTable = newSubTableClass() 1911 newExtSubTable.ExtSubTable = newSubTable 1912 else: 1913 subTableType = subtable.__class__.LookupType 1914 newSubTableClass = lookupTypes[overflowRecord.tableType][subTableType] 1915 newSubTable = newSubTableClass() 1916 toInsert = newSubTable 1917 1918 if hasattr(lookup, 'SubTableCount'): # may not be defined yet. 1919 lookup.SubTableCount = lookup.SubTableCount + 1 1920 1921 try: 1922 splitFunc = splitTable[overflowRecord.tableType][subTableType] 1923 except KeyError: 1924 log.error( 1925 "Don't know how to split %s lookup type %s", 1926 overflowRecord.tableType, 1927 subTableType, 1928 ) 1929 return False 1930 1931 ok = splitFunc(subtable, newSubTable, overflowRecord) 1932 if ok: 1933 lookup.SubTable.insert(subIndex + 1, toInsert) 1934 return ok 1935 1936# End of OverFlow logic 1937 1938 1939def _buildClasses(): 1940 import re 1941 from .otData import otData 1942 1943 formatPat = re.compile(r"([A-Za-z0-9]+)Format(\d+)$") 1944 namespace = globals() 1945 1946 # populate module with classes 1947 for name, table in otData: 1948 baseClass = BaseTable 1949 m = formatPat.match(name) 1950 if m: 1951 # XxxFormatN subtable, we only add the "base" table 1952 name = m.group(1) 1953 # the first row of a format-switching otData table describes the Format; 1954 # the first column defines the type of the Format field. 1955 # Currently this can be either 'uint16' or 'uint8'. 1956 formatType = table[0][0] 1957 baseClass = getFormatSwitchingBaseTableClass(formatType) 1958 if name not in namespace: 1959 # the class doesn't exist yet, so the base implementation is used. 1960 cls = type(name, (baseClass,), {}) 1961 if name in ('GSUB', 'GPOS'): 1962 cls.DontShare = True 1963 namespace[name] = cls 1964 1965 for base, alts in _equivalents.items(): 1966 base = namespace[base] 1967 for alt in alts: 1968 namespace[alt] = base 1969 1970 global lookupTypes 1971 lookupTypes = { 1972 'GSUB': { 1973 1: SingleSubst, 1974 2: MultipleSubst, 1975 3: AlternateSubst, 1976 4: LigatureSubst, 1977 5: ContextSubst, 1978 6: ChainContextSubst, 1979 7: ExtensionSubst, 1980 8: ReverseChainSingleSubst, 1981 }, 1982 'GPOS': { 1983 1: SinglePos, 1984 2: PairPos, 1985 3: CursivePos, 1986 4: MarkBasePos, 1987 5: MarkLigPos, 1988 6: MarkMarkPos, 1989 7: ContextPos, 1990 8: ChainContextPos, 1991 9: ExtensionPos, 1992 }, 1993 'mort': { 1994 4: NoncontextualMorph, 1995 }, 1996 'morx': { 1997 0: RearrangementMorph, 1998 1: ContextualMorph, 1999 2: LigatureMorph, 2000 # 3: Reserved, 2001 4: NoncontextualMorph, 2002 5: InsertionMorph, 2003 }, 2004 } 2005 lookupTypes['JSTF'] = lookupTypes['GPOS'] # JSTF contains GPOS 2006 for lookupEnum in lookupTypes.values(): 2007 for enum, cls in lookupEnum.items(): 2008 cls.LookupType = enum 2009 2010 global featureParamTypes 2011 featureParamTypes = { 2012 'size': FeatureParamsSize, 2013 } 2014 for i in range(1, 20+1): 2015 featureParamTypes['ss%02d' % i] = FeatureParamsStylisticSet 2016 for i in range(1, 99+1): 2017 featureParamTypes['cv%02d' % i] = FeatureParamsCharacterVariants 2018 2019 # add converters to classes 2020 from .otConverters import buildConverters 2021 for name, table in otData: 2022 m = formatPat.match(name) 2023 if m: 2024 # XxxFormatN subtable, add converter to "base" table 2025 name, format = m.groups() 2026 format = int(format) 2027 cls = namespace[name] 2028 if not hasattr(cls, "converters"): 2029 cls.converters = {} 2030 cls.convertersByName = {} 2031 converters, convertersByName = buildConverters(table[1:], namespace) 2032 cls.converters[format] = converters 2033 cls.convertersByName[format] = convertersByName 2034 # XXX Add staticSize? 2035 else: 2036 cls = namespace[name] 2037 cls.converters, cls.convertersByName = buildConverters(table, namespace) 2038 # XXX Add staticSize? 2039 2040 2041_buildClasses() 2042 2043 2044def _getGlyphsFromCoverageTable(coverage): 2045 if coverage is None: 2046 # empty coverage table 2047 return [] 2048 else: 2049 return coverage.glyphs 2050