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 603# The special 0xFFFFFFFF delta-set index is used to indicate that there 604# is no variation data in the ItemVariationStore for a given variable field 605NO_VARIATION_INDEX = 0xFFFFFFFF 606 607 608class DeltaSetIndexMap(getFormatSwitchingBaseTableClass("uint8")): 609 610 def populateDefaults(self, propagator=None): 611 if not hasattr(self, 'mapping'): 612 self.mapping = [] 613 614 def postRead(self, rawTable, font): 615 assert (rawTable['EntryFormat'] & 0xFFC0) == 0 616 self.mapping = rawTable['mapping'] 617 618 @staticmethod 619 def getEntryFormat(mapping): 620 ored = 0 621 for idx in mapping: 622 ored |= idx 623 624 inner = ored & 0xFFFF 625 innerBits = 0 626 while inner: 627 innerBits += 1 628 inner >>= 1 629 innerBits = max(innerBits, 1) 630 assert innerBits <= 16 631 632 ored = (ored >> (16-innerBits)) | (ored & ((1<<innerBits)-1)) 633 if ored <= 0x000000FF: 634 entrySize = 1 635 elif ored <= 0x0000FFFF: 636 entrySize = 2 637 elif ored <= 0x00FFFFFF: 638 entrySize = 3 639 else: 640 entrySize = 4 641 642 return ((entrySize - 1) << 4) | (innerBits - 1) 643 644 def preWrite(self, font): 645 mapping = getattr(self, "mapping", None) 646 if mapping is None: 647 mapping = self.mapping = [] 648 self.Format = 1 if len(mapping) > 0xFFFF else 0 649 rawTable = self.__dict__.copy() 650 rawTable['MappingCount'] = len(mapping) 651 rawTable['EntryFormat'] = self.getEntryFormat(mapping) 652 return rawTable 653 654 def toXML2(self, xmlWriter, font): 655 # Make xml dump less verbose, by omitting no-op entries like: 656 # <Map index="..." outer="65535" inner="65535"/> 657 xmlWriter.comment( 658 "Omitted values default to 0xFFFF/0xFFFF (no variations)" 659 ) 660 xmlWriter.newline() 661 for i, value in enumerate(getattr(self, "mapping", [])): 662 attrs = [('index', i)] 663 if value != NO_VARIATION_INDEX: 664 attrs.extend([ 665 ('outer', value >> 16), 666 ('inner', value & 0xFFFF), 667 ]) 668 xmlWriter.simpletag("Map", attrs) 669 xmlWriter.newline() 670 671 def fromXML(self, name, attrs, content, font): 672 mapping = getattr(self, "mapping", None) 673 if mapping is None: 674 self.mapping = mapping = [] 675 index = safeEval(attrs['index']) 676 outer = safeEval(attrs.get('outer', '0xFFFF')) 677 inner = safeEval(attrs.get('inner', '0xFFFF')) 678 assert inner <= 0xFFFF 679 mapping.insert(index, (outer << 16) | inner) 680 681 682class VarIdxMap(BaseTable): 683 684 def populateDefaults(self, propagator=None): 685 if not hasattr(self, 'mapping'): 686 self.mapping = {} 687 688 def postRead(self, rawTable, font): 689 assert (rawTable['EntryFormat'] & 0xFFC0) == 0 690 glyphOrder = font.getGlyphOrder() 691 mapList = rawTable['mapping'] 692 mapList.extend([mapList[-1]] * (len(glyphOrder) - len(mapList))) 693 self.mapping = dict(zip(glyphOrder, mapList)) 694 695 def preWrite(self, font): 696 mapping = getattr(self, "mapping", None) 697 if mapping is None: 698 mapping = self.mapping = {} 699 700 glyphOrder = font.getGlyphOrder() 701 mapping = [mapping[g] for g in glyphOrder] 702 while len(mapping) > 1 and mapping[-2] == mapping[-1]: 703 del mapping[-1] 704 705 rawTable = {'mapping': mapping} 706 rawTable['MappingCount'] = len(mapping) 707 rawTable['EntryFormat'] = DeltaSetIndexMap.getEntryFormat(mapping) 708 return rawTable 709 710 def toXML2(self, xmlWriter, font): 711 for glyph, value in sorted(getattr(self, "mapping", {}).items()): 712 attrs = ( 713 ('glyph', glyph), 714 ('outer', value >> 16), 715 ('inner', value & 0xFFFF), 716 ) 717 xmlWriter.simpletag("Map", attrs) 718 xmlWriter.newline() 719 720 def fromXML(self, name, attrs, content, font): 721 mapping = getattr(self, "mapping", None) 722 if mapping is None: 723 mapping = {} 724 self.mapping = mapping 725 try: 726 glyph = attrs['glyph'] 727 except: # https://github.com/fonttools/fonttools/commit/21cbab8ce9ded3356fef3745122da64dcaf314e9#commitcomment-27649836 728 glyph = font.getGlyphOrder()[attrs['index']] 729 outer = safeEval(attrs['outer']) 730 inner = safeEval(attrs['inner']) 731 assert inner <= 0xFFFF 732 mapping[glyph] = (outer << 16) | inner 733 734 735class VarRegionList(BaseTable): 736 737 def preWrite(self, font): 738 # The OT spec says VarStore.VarRegionList.RegionAxisCount should always 739 # be equal to the fvar.axisCount, and OTS < v8.0.0 enforces this rule 740 # even when the VarRegionList is empty. We can't treat RegionAxisCount 741 # like a normal propagated count (== len(Region[i].VarRegionAxis)), 742 # otherwise it would default to 0 if VarRegionList is empty. 743 # Thus, we force it to always be equal to fvar.axisCount. 744 # https://github.com/khaledhosny/ots/pull/192 745 fvarTable = font.get("fvar") 746 if fvarTable: 747 self.RegionAxisCount = len(fvarTable.axes) 748 return { 749 **self.__dict__, 750 "RegionAxisCount": CountReference(self.__dict__, "RegionAxisCount") 751 } 752 753 754class SingleSubst(FormatSwitchingBaseTable): 755 756 def populateDefaults(self, propagator=None): 757 if not hasattr(self, 'mapping'): 758 self.mapping = {} 759 760 def postRead(self, rawTable, font): 761 mapping = {} 762 input = _getGlyphsFromCoverageTable(rawTable["Coverage"]) 763 if self.Format == 1: 764 delta = rawTable["DeltaGlyphID"] 765 inputGIDS = font.getGlyphIDMany(input) 766 outGIDS = [ (glyphID + delta) % 65536 for glyphID in inputGIDS ] 767 outNames = font.getGlyphNameMany(outGIDS) 768 for inp, out in zip(input, outNames): 769 mapping[inp] = out 770 elif self.Format == 2: 771 assert len(input) == rawTable["GlyphCount"], \ 772 "invalid SingleSubstFormat2 table" 773 subst = rawTable["Substitute"] 774 for inp, sub in zip(input, subst): 775 mapping[inp] = sub 776 else: 777 assert 0, "unknown format: %s" % self.Format 778 self.mapping = mapping 779 del self.Format # Don't need this anymore 780 781 def preWrite(self, font): 782 mapping = getattr(self, "mapping", None) 783 if mapping is None: 784 mapping = self.mapping = {} 785 items = list(mapping.items()) 786 getGlyphID = font.getGlyphID 787 gidItems = [(getGlyphID(a), getGlyphID(b)) for a,b in items] 788 sortableItems = sorted(zip(gidItems, items)) 789 790 # figure out format 791 format = 2 792 delta = None 793 for inID, outID in gidItems: 794 if delta is None: 795 delta = (outID - inID) % 65536 796 797 if (inID + delta) % 65536 != outID: 798 break 799 else: 800 if delta is None: 801 # the mapping is empty, better use format 2 802 format = 2 803 else: 804 format = 1 805 806 rawTable = {} 807 self.Format = format 808 cov = Coverage() 809 input = [ item [1][0] for item in sortableItems] 810 subst = [ item [1][1] for item in sortableItems] 811 cov.glyphs = input 812 rawTable["Coverage"] = cov 813 if format == 1: 814 assert delta is not None 815 rawTable["DeltaGlyphID"] = delta 816 else: 817 rawTable["Substitute"] = subst 818 return rawTable 819 820 def toXML2(self, xmlWriter, font): 821 items = sorted(self.mapping.items()) 822 for inGlyph, outGlyph in items: 823 xmlWriter.simpletag("Substitution", 824 [("in", inGlyph), ("out", outGlyph)]) 825 xmlWriter.newline() 826 827 def fromXML(self, name, attrs, content, font): 828 mapping = getattr(self, "mapping", None) 829 if mapping is None: 830 mapping = {} 831 self.mapping = mapping 832 mapping[attrs["in"]] = attrs["out"] 833 834 835class MultipleSubst(FormatSwitchingBaseTable): 836 837 def populateDefaults(self, propagator=None): 838 if not hasattr(self, 'mapping'): 839 self.mapping = {} 840 841 def postRead(self, rawTable, font): 842 mapping = {} 843 if self.Format == 1: 844 glyphs = _getGlyphsFromCoverageTable(rawTable["Coverage"]) 845 subst = [s.Substitute for s in rawTable["Sequence"]] 846 mapping = dict(zip(glyphs, subst)) 847 else: 848 assert 0, "unknown format: %s" % self.Format 849 self.mapping = mapping 850 del self.Format # Don't need this anymore 851 852 def preWrite(self, font): 853 mapping = getattr(self, "mapping", None) 854 if mapping is None: 855 mapping = self.mapping = {} 856 cov = Coverage() 857 cov.glyphs = sorted(list(mapping.keys()), key=font.getGlyphID) 858 self.Format = 1 859 rawTable = { 860 "Coverage": cov, 861 "Sequence": [self.makeSequence_(mapping[glyph]) 862 for glyph in cov.glyphs], 863 } 864 return rawTable 865 866 def toXML2(self, xmlWriter, font): 867 items = sorted(self.mapping.items()) 868 for inGlyph, outGlyphs in items: 869 out = ",".join(outGlyphs) 870 xmlWriter.simpletag("Substitution", 871 [("in", inGlyph), ("out", out)]) 872 xmlWriter.newline() 873 874 def fromXML(self, name, attrs, content, font): 875 mapping = getattr(self, "mapping", None) 876 if mapping is None: 877 mapping = {} 878 self.mapping = mapping 879 880 # TTX v3.0 and earlier. 881 if name == "Coverage": 882 self.old_coverage_ = [] 883 for element in content: 884 if not isinstance(element, tuple): 885 continue 886 element_name, element_attrs, _ = element 887 if element_name == "Glyph": 888 self.old_coverage_.append(element_attrs["value"]) 889 return 890 if name == "Sequence": 891 index = int(attrs.get("index", len(mapping))) 892 glyph = self.old_coverage_[index] 893 glyph_mapping = mapping[glyph] = [] 894 for element in content: 895 if not isinstance(element, tuple): 896 continue 897 element_name, element_attrs, _ = element 898 if element_name == "Substitute": 899 glyph_mapping.append(element_attrs["value"]) 900 return 901 902 # TTX v3.1 and later. 903 outGlyphs = attrs["out"].split(",") if attrs["out"] else [] 904 mapping[attrs["in"]] = [g.strip() for g in outGlyphs] 905 906 @staticmethod 907 def makeSequence_(g): 908 seq = Sequence() 909 seq.Substitute = g 910 return seq 911 912 913class ClassDef(FormatSwitchingBaseTable): 914 915 def populateDefaults(self, propagator=None): 916 if not hasattr(self, 'classDefs'): 917 self.classDefs = {} 918 919 def postRead(self, rawTable, font): 920 classDefs = {} 921 922 if self.Format == 1: 923 start = rawTable["StartGlyph"] 924 classList = rawTable["ClassValueArray"] 925 startID = font.getGlyphID(start) 926 endID = startID + len(classList) 927 glyphNames = font.getGlyphNameMany(range(startID, endID)) 928 for glyphName, cls in zip(glyphNames, classList): 929 if cls: 930 classDefs[glyphName] = cls 931 932 elif self.Format == 2: 933 records = rawTable["ClassRangeRecord"] 934 for rec in records: 935 cls = rec.Class 936 if not cls: 937 continue 938 start = rec.Start 939 end = rec.End 940 startID = font.getGlyphID(start) 941 endID = font.getGlyphID(end) + 1 942 glyphNames = font.getGlyphNameMany(range(startID, endID)) 943 for glyphName in glyphNames: 944 classDefs[glyphName] = cls 945 else: 946 log.warning("Unknown ClassDef format: %s", self.Format) 947 self.classDefs = classDefs 948 del self.Format # Don't need this anymore 949 950 def _getClassRanges(self, font): 951 classDefs = getattr(self, "classDefs", None) 952 if classDefs is None: 953 self.classDefs = {} 954 return 955 getGlyphID = font.getGlyphID 956 items = [] 957 for glyphName, cls in classDefs.items(): 958 if not cls: 959 continue 960 items.append((getGlyphID(glyphName), glyphName, cls)) 961 if items: 962 items.sort() 963 last, lastName, lastCls = items[0] 964 ranges = [[lastCls, last, lastName]] 965 for glyphID, glyphName, cls in items[1:]: 966 if glyphID != last + 1 or cls != lastCls: 967 ranges[-1].extend([last, lastName]) 968 ranges.append([cls, glyphID, glyphName]) 969 last = glyphID 970 lastName = glyphName 971 lastCls = cls 972 ranges[-1].extend([last, lastName]) 973 return ranges 974 975 def preWrite(self, font): 976 format = 2 977 rawTable = {"ClassRangeRecord": []} 978 ranges = self._getClassRanges(font) 979 if ranges: 980 startGlyph = ranges[0][1] 981 endGlyph = ranges[-1][3] 982 glyphCount = endGlyph - startGlyph + 1 983 if len(ranges) * 3 < glyphCount + 1: 984 # Format 2 is more compact 985 for i in range(len(ranges)): 986 cls, start, startName, end, endName = ranges[i] 987 rec = ClassRangeRecord() 988 rec.Start = startName 989 rec.End = endName 990 rec.Class = cls 991 ranges[i] = rec 992 format = 2 993 rawTable = {"ClassRangeRecord": ranges} 994 else: 995 # Format 1 is more compact 996 startGlyphName = ranges[0][2] 997 classes = [0] * glyphCount 998 for cls, start, startName, end, endName in ranges: 999 for g in range(start - startGlyph, end - startGlyph + 1): 1000 classes[g] = cls 1001 format = 1 1002 rawTable = {"StartGlyph": startGlyphName, "ClassValueArray": classes} 1003 self.Format = format 1004 return rawTable 1005 1006 def toXML2(self, xmlWriter, font): 1007 items = sorted(self.classDefs.items()) 1008 for glyphName, cls in items: 1009 xmlWriter.simpletag("ClassDef", [("glyph", glyphName), ("class", cls)]) 1010 xmlWriter.newline() 1011 1012 def fromXML(self, name, attrs, content, font): 1013 classDefs = getattr(self, "classDefs", None) 1014 if classDefs is None: 1015 classDefs = {} 1016 self.classDefs = classDefs 1017 classDefs[attrs["glyph"]] = int(attrs["class"]) 1018 1019 1020class AlternateSubst(FormatSwitchingBaseTable): 1021 1022 def populateDefaults(self, propagator=None): 1023 if not hasattr(self, 'alternates'): 1024 self.alternates = {} 1025 1026 def postRead(self, rawTable, font): 1027 alternates = {} 1028 if self.Format == 1: 1029 input = _getGlyphsFromCoverageTable(rawTable["Coverage"]) 1030 alts = rawTable["AlternateSet"] 1031 assert len(input) == len(alts) 1032 for inp,alt in zip(input,alts): 1033 alternates[inp] = alt.Alternate 1034 else: 1035 assert 0, "unknown format: %s" % self.Format 1036 self.alternates = alternates 1037 del self.Format # Don't need this anymore 1038 1039 def preWrite(self, font): 1040 self.Format = 1 1041 alternates = getattr(self, "alternates", None) 1042 if alternates is None: 1043 alternates = self.alternates = {} 1044 items = list(alternates.items()) 1045 for i in range(len(items)): 1046 glyphName, set = items[i] 1047 items[i] = font.getGlyphID(glyphName), glyphName, set 1048 items.sort() 1049 cov = Coverage() 1050 cov.glyphs = [ item[1] for item in items] 1051 alternates = [] 1052 setList = [ item[-1] for item in items] 1053 for set in setList: 1054 alts = AlternateSet() 1055 alts.Alternate = set 1056 alternates.append(alts) 1057 # a special case to deal with the fact that several hundred Adobe Japan1-5 1058 # CJK fonts will overflow an offset if the coverage table isn't pushed to the end. 1059 # Also useful in that when splitting a sub-table because of an offset overflow 1060 # I don't need to calculate the change in the subtable offset due to the change in the coverage table size. 1061 # Allows packing more rules in subtable. 1062 self.sortCoverageLast = 1 1063 return {"Coverage": cov, "AlternateSet": alternates} 1064 1065 def toXML2(self, xmlWriter, font): 1066 items = sorted(self.alternates.items()) 1067 for glyphName, alternates in items: 1068 xmlWriter.begintag("AlternateSet", glyph=glyphName) 1069 xmlWriter.newline() 1070 for alt in alternates: 1071 xmlWriter.simpletag("Alternate", glyph=alt) 1072 xmlWriter.newline() 1073 xmlWriter.endtag("AlternateSet") 1074 xmlWriter.newline() 1075 1076 def fromXML(self, name, attrs, content, font): 1077 alternates = getattr(self, "alternates", None) 1078 if alternates is None: 1079 alternates = {} 1080 self.alternates = alternates 1081 glyphName = attrs["glyph"] 1082 set = [] 1083 alternates[glyphName] = set 1084 for element in content: 1085 if not isinstance(element, tuple): 1086 continue 1087 name, attrs, content = element 1088 set.append(attrs["glyph"]) 1089 1090 1091class LigatureSubst(FormatSwitchingBaseTable): 1092 1093 def populateDefaults(self, propagator=None): 1094 if not hasattr(self, 'ligatures'): 1095 self.ligatures = {} 1096 1097 def postRead(self, rawTable, font): 1098 ligatures = {} 1099 if self.Format == 1: 1100 input = _getGlyphsFromCoverageTable(rawTable["Coverage"]) 1101 ligSets = rawTable["LigatureSet"] 1102 assert len(input) == len(ligSets) 1103 for i in range(len(input)): 1104 ligatures[input[i]] = ligSets[i].Ligature 1105 else: 1106 assert 0, "unknown format: %s" % self.Format 1107 self.ligatures = ligatures 1108 del self.Format # Don't need this anymore 1109 1110 def preWrite(self, font): 1111 self.Format = 1 1112 ligatures = getattr(self, "ligatures", None) 1113 if ligatures is None: 1114 ligatures = self.ligatures = {} 1115 1116 if ligatures and isinstance(next(iter(ligatures)), tuple): 1117 # New high-level API in v3.1 and later. Note that we just support compiling this 1118 # for now. We don't load to this API, and don't do XML with it. 1119 1120 # ligatures is map from components-sequence to lig-glyph 1121 newLigatures = dict() 1122 for comps,lig in sorted(ligatures.items(), key=lambda item: (-len(item[0]), item[0])): 1123 ligature = Ligature() 1124 ligature.Component = comps[1:] 1125 ligature.CompCount = len(comps) 1126 ligature.LigGlyph = lig 1127 newLigatures.setdefault(comps[0], []).append(ligature) 1128 ligatures = newLigatures 1129 1130 items = list(ligatures.items()) 1131 for i in range(len(items)): 1132 glyphName, set = items[i] 1133 items[i] = font.getGlyphID(glyphName), glyphName, set 1134 items.sort() 1135 cov = Coverage() 1136 cov.glyphs = [ item[1] for item in items] 1137 1138 ligSets = [] 1139 setList = [ item[-1] for item in items ] 1140 for set in setList: 1141 ligSet = LigatureSet() 1142 ligs = ligSet.Ligature = [] 1143 for lig in set: 1144 ligs.append(lig) 1145 ligSets.append(ligSet) 1146 # Useful in that when splitting a sub-table because of an offset overflow 1147 # I don't need to calculate the change in subtabl offset due to the coverage table size. 1148 # Allows packing more rules in subtable. 1149 self.sortCoverageLast = 1 1150 return {"Coverage": cov, "LigatureSet": ligSets} 1151 1152 def toXML2(self, xmlWriter, font): 1153 items = sorted(self.ligatures.items()) 1154 for glyphName, ligSets in items: 1155 xmlWriter.begintag("LigatureSet", glyph=glyphName) 1156 xmlWriter.newline() 1157 for lig in ligSets: 1158 xmlWriter.simpletag("Ligature", glyph=lig.LigGlyph, 1159 components=",".join(lig.Component)) 1160 xmlWriter.newline() 1161 xmlWriter.endtag("LigatureSet") 1162 xmlWriter.newline() 1163 1164 def fromXML(self, name, attrs, content, font): 1165 ligatures = getattr(self, "ligatures", None) 1166 if ligatures is None: 1167 ligatures = {} 1168 self.ligatures = ligatures 1169 glyphName = attrs["glyph"] 1170 ligs = [] 1171 ligatures[glyphName] = ligs 1172 for element in content: 1173 if not isinstance(element, tuple): 1174 continue 1175 name, attrs, content = element 1176 lig = Ligature() 1177 lig.LigGlyph = attrs["glyph"] 1178 components = attrs["components"] 1179 lig.Component = components.split(",") if components else [] 1180 lig.CompCount = len(lig.Component) 1181 ligs.append(lig) 1182 1183 1184class COLR(BaseTable): 1185 1186 def decompile(self, reader, font): 1187 # COLRv0 is exceptional in that LayerRecordCount appears *after* the 1188 # LayerRecordArray it counts, but the parser logic expects Count fields 1189 # to always precede the arrays. Here we work around this by parsing the 1190 # LayerRecordCount before the rest of the table, and storing it in 1191 # the reader's local state. 1192 subReader = reader.getSubReader(offset=0) 1193 for conv in self.getConverters(): 1194 if conv.name != "LayerRecordCount": 1195 subReader.advance(conv.staticSize) 1196 continue 1197 reader[conv.name] = conv.read(subReader, font, tableDict={}) 1198 break 1199 else: 1200 raise AssertionError("LayerRecordCount converter not found") 1201 return BaseTable.decompile(self, reader, font) 1202 1203 def preWrite(self, font): 1204 # The writer similarly assumes Count values precede the things counted, 1205 # thus here we pre-initialize a CountReference; the actual count value 1206 # will be set to the lenght of the array by the time this is assembled. 1207 self.LayerRecordCount = None 1208 return { 1209 **self.__dict__, 1210 "LayerRecordCount": CountReference(self.__dict__, "LayerRecordCount") 1211 } 1212 1213 1214class LookupList(BaseTable): 1215 @property 1216 def table(self): 1217 for l in self.Lookup: 1218 for st in l.SubTable: 1219 if type(st).__name__.endswith("Subst"): 1220 return "GSUB" 1221 if type(st).__name__.endswith("Pos"): 1222 return "GPOS" 1223 raise ValueError 1224 1225 def toXML2(self, xmlWriter, font): 1226 if not font or "Debg" not in font or LOOKUP_DEBUG_INFO_KEY not in font["Debg"].data: 1227 return super().toXML2(xmlWriter, font) 1228 debugData = font["Debg"].data[LOOKUP_DEBUG_INFO_KEY][self.table] 1229 for conv in self.getConverters(): 1230 if conv.repeat: 1231 value = getattr(self, conv.name, []) 1232 for lookupIndex, item in enumerate(value): 1233 if str(lookupIndex) in debugData: 1234 info = LookupDebugInfo(*debugData[str(lookupIndex)]) 1235 tag = info.location 1236 if info.name: 1237 tag = f'{info.name}: {tag}' 1238 if info.feature: 1239 script,language,feature = info.feature 1240 tag = f'{tag} in {feature} ({script}/{language})' 1241 xmlWriter.comment(tag) 1242 xmlWriter.newline() 1243 1244 conv.xmlWrite(xmlWriter, font, item, conv.name, 1245 [("index", lookupIndex)]) 1246 else: 1247 if conv.aux and not eval(conv.aux, None, vars(self)): 1248 continue 1249 value = getattr(self, conv.name, None) # TODO Handle defaults instead of defaulting to None! 1250 conv.xmlWrite(xmlWriter, font, value, conv.name, []) 1251 1252class BaseGlyphRecordArray(BaseTable): 1253 1254 def preWrite(self, font): 1255 self.BaseGlyphRecord = sorted( 1256 self.BaseGlyphRecord, 1257 key=lambda rec: font.getGlyphID(rec.BaseGlyph) 1258 ) 1259 return self.__dict__.copy() 1260 1261 1262class BaseGlyphList(BaseTable): 1263 1264 def preWrite(self, font): 1265 self.BaseGlyphPaintRecord = sorted( 1266 self.BaseGlyphPaintRecord, 1267 key=lambda rec: font.getGlyphID(rec.BaseGlyph) 1268 ) 1269 return self.__dict__.copy() 1270 1271 1272class ClipBoxFormat(IntEnum): 1273 Static = 1 1274 Variable = 2 1275 1276 def is_variable(self): 1277 return self is self.Variable 1278 1279 def as_variable(self): 1280 return self.Variable 1281 1282 1283class ClipBox(getFormatSwitchingBaseTableClass("uint8")): 1284 formatEnum = ClipBoxFormat 1285 1286 def as_tuple(self): 1287 return tuple(getattr(self, conv.name) for conv in self.getConverters()) 1288 1289 def __repr__(self): 1290 return f"{self.__class__.__name__}{self.as_tuple()}" 1291 1292 1293class ClipList(getFormatSwitchingBaseTableClass("uint8")): 1294 1295 def populateDefaults(self, propagator=None): 1296 if not hasattr(self, "clips"): 1297 self.clips = {} 1298 1299 def postRead(self, rawTable, font): 1300 clips = {} 1301 glyphOrder = font.getGlyphOrder() 1302 for i, rec in enumerate(rawTable["ClipRecord"]): 1303 if rec.StartGlyphID > rec.EndGlyphID: 1304 log.warning( 1305 "invalid ClipRecord[%i].StartGlyphID (%i) > " 1306 "EndGlyphID (%i); skipped", 1307 i, 1308 rec.StartGlyphID, 1309 rec.EndGlyphID, 1310 ) 1311 continue 1312 redefinedGlyphs = [] 1313 missingGlyphs = [] 1314 for glyphID in range(rec.StartGlyphID, rec.EndGlyphID + 1): 1315 try: 1316 glyph = glyphOrder[glyphID] 1317 except IndexError: 1318 missingGlyphs.append(glyphID) 1319 continue 1320 if glyph not in clips: 1321 clips[glyph] = copy.copy(rec.ClipBox) 1322 else: 1323 redefinedGlyphs.append(glyphID) 1324 if redefinedGlyphs: 1325 log.warning( 1326 "ClipRecord[%i] overlaps previous records; " 1327 "ignoring redefined clip boxes for the " 1328 "following glyph ID range: [%i-%i]", 1329 i, 1330 min(redefinedGlyphs), 1331 max(redefinedGlyphs), 1332 ) 1333 if missingGlyphs: 1334 log.warning( 1335 "ClipRecord[%i] range references missing " 1336 "glyph IDs: [%i-%i]", 1337 i, 1338 min(missingGlyphs), 1339 max(missingGlyphs), 1340 ) 1341 self.clips = clips 1342 1343 def groups(self): 1344 glyphsByClip = defaultdict(list) 1345 uniqueClips = {} 1346 for glyphName, clipBox in self.clips.items(): 1347 key = clipBox.as_tuple() 1348 glyphsByClip[key].append(glyphName) 1349 if key not in uniqueClips: 1350 uniqueClips[key] = clipBox 1351 return { 1352 frozenset(glyphs): uniqueClips[key] 1353 for key, glyphs in glyphsByClip.items() 1354 } 1355 1356 def preWrite(self, font): 1357 if not hasattr(self, "clips"): 1358 self.clips = {} 1359 clipBoxRanges = {} 1360 glyphMap = font.getReverseGlyphMap() 1361 for glyphs, clipBox in self.groups().items(): 1362 glyphIDs = sorted( 1363 glyphMap[glyphName] for glyphName in glyphs 1364 if glyphName in glyphMap 1365 ) 1366 if not glyphIDs: 1367 continue 1368 last = glyphIDs[0] 1369 ranges = [[last]] 1370 for glyphID in glyphIDs[1:]: 1371 if glyphID != last + 1: 1372 ranges[-1].append(last) 1373 ranges.append([glyphID]) 1374 last = glyphID 1375 ranges[-1].append(last) 1376 for start, end in ranges: 1377 assert (start, end) not in clipBoxRanges 1378 clipBoxRanges[(start, end)] = clipBox 1379 1380 clipRecords = [] 1381 for (start, end), clipBox in sorted(clipBoxRanges.items()): 1382 record = ClipRecord() 1383 record.StartGlyphID = start 1384 record.EndGlyphID = end 1385 record.ClipBox = clipBox 1386 clipRecords.append(record) 1387 rawTable = { 1388 "ClipCount": len(clipRecords), 1389 "ClipRecord": clipRecords, 1390 } 1391 return rawTable 1392 1393 def toXML(self, xmlWriter, font, attrs=None, name=None): 1394 tableName = name if name else self.__class__.__name__ 1395 if attrs is None: 1396 attrs = [] 1397 if hasattr(self, "Format"): 1398 attrs.append(("Format", self.Format)) 1399 xmlWriter.begintag(tableName, attrs) 1400 xmlWriter.newline() 1401 # sort clips alphabetically to ensure deterministic XML dump 1402 for glyphs, clipBox in sorted( 1403 self.groups().items(), key=lambda item: min(item[0]) 1404 ): 1405 xmlWriter.begintag("Clip") 1406 xmlWriter.newline() 1407 for glyphName in sorted(glyphs): 1408 xmlWriter.simpletag("Glyph", value=glyphName) 1409 xmlWriter.newline() 1410 xmlWriter.begintag("ClipBox", [("Format", clipBox.Format)]) 1411 xmlWriter.newline() 1412 clipBox.toXML2(xmlWriter, font) 1413 xmlWriter.endtag("ClipBox") 1414 xmlWriter.newline() 1415 xmlWriter.endtag("Clip") 1416 xmlWriter.newline() 1417 xmlWriter.endtag(tableName) 1418 xmlWriter.newline() 1419 1420 def fromXML(self, name, attrs, content, font): 1421 clips = getattr(self, "clips", None) 1422 if clips is None: 1423 self.clips = clips = {} 1424 assert name == "Clip" 1425 glyphs = [] 1426 clipBox = None 1427 for elem in content: 1428 if not isinstance(elem, tuple): 1429 continue 1430 name, attrs, content = elem 1431 if name == "Glyph": 1432 glyphs.append(attrs["value"]) 1433 elif name == "ClipBox": 1434 clipBox = ClipBox() 1435 clipBox.Format = safeEval(attrs["Format"]) 1436 for elem in content: 1437 if not isinstance(elem, tuple): 1438 continue 1439 name, attrs, content = elem 1440 clipBox.fromXML(name, attrs, content, font) 1441 if clipBox: 1442 for glyphName in glyphs: 1443 clips[glyphName] = clipBox 1444 1445 1446class ExtendMode(IntEnum): 1447 PAD = 0 1448 REPEAT = 1 1449 REFLECT = 2 1450 1451 1452# Porter-Duff modes for COLRv1 PaintComposite: 1453# https://github.com/googlefonts/colr-gradients-spec/tree/off_sub_1#compositemode-enumeration 1454class CompositeMode(IntEnum): 1455 CLEAR = 0 1456 SRC = 1 1457 DEST = 2 1458 SRC_OVER = 3 1459 DEST_OVER = 4 1460 SRC_IN = 5 1461 DEST_IN = 6 1462 SRC_OUT = 7 1463 DEST_OUT = 8 1464 SRC_ATOP = 9 1465 DEST_ATOP = 10 1466 XOR = 11 1467 PLUS = 12 1468 SCREEN = 13 1469 OVERLAY = 14 1470 DARKEN = 15 1471 LIGHTEN = 16 1472 COLOR_DODGE = 17 1473 COLOR_BURN = 18 1474 HARD_LIGHT = 19 1475 SOFT_LIGHT = 20 1476 DIFFERENCE = 21 1477 EXCLUSION = 22 1478 MULTIPLY = 23 1479 HSL_HUE = 24 1480 HSL_SATURATION = 25 1481 HSL_COLOR = 26 1482 HSL_LUMINOSITY = 27 1483 1484 1485class PaintFormat(IntEnum): 1486 PaintColrLayers = 1 1487 PaintSolid = 2 1488 PaintVarSolid = 3, 1489 PaintLinearGradient = 4 1490 PaintVarLinearGradient = 5 1491 PaintRadialGradient = 6 1492 PaintVarRadialGradient = 7 1493 PaintSweepGradient = 8 1494 PaintVarSweepGradient = 9 1495 PaintGlyph = 10 1496 PaintColrGlyph = 11 1497 PaintTransform = 12 1498 PaintVarTransform = 13 1499 PaintTranslate = 14 1500 PaintVarTranslate = 15 1501 PaintScale = 16 1502 PaintVarScale = 17 1503 PaintScaleAroundCenter = 18 1504 PaintVarScaleAroundCenter = 19 1505 PaintScaleUniform = 20 1506 PaintVarScaleUniform = 21 1507 PaintScaleUniformAroundCenter = 22 1508 PaintVarScaleUniformAroundCenter = 23 1509 PaintRotate = 24 1510 PaintVarRotate = 25 1511 PaintRotateAroundCenter = 26 1512 PaintVarRotateAroundCenter = 27 1513 PaintSkew = 28 1514 PaintVarSkew = 29 1515 PaintSkewAroundCenter = 30 1516 PaintVarSkewAroundCenter = 31 1517 PaintComposite = 32 1518 1519 def is_variable(self): 1520 return self.name.startswith("PaintVar") 1521 1522 def as_variable(self): 1523 if self.is_variable(): 1524 return self 1525 try: 1526 return PaintFormat.__members__[f"PaintVar{self.name[5:]}"] 1527 except KeyError: 1528 return None 1529 1530 1531class Paint(getFormatSwitchingBaseTableClass("uint8")): 1532 formatEnum = PaintFormat 1533 1534 def getFormatName(self): 1535 try: 1536 return self.formatEnum(self.Format).name 1537 except ValueError: 1538 raise NotImplementedError(f"Unknown Paint format: {self.Format}") 1539 1540 def toXML(self, xmlWriter, font, attrs=None, name=None): 1541 tableName = name if name else self.__class__.__name__ 1542 if attrs is None: 1543 attrs = [] 1544 attrs.append(("Format", self.Format)) 1545 xmlWriter.begintag(tableName, attrs) 1546 xmlWriter.comment(self.getFormatName()) 1547 xmlWriter.newline() 1548 self.toXML2(xmlWriter, font) 1549 xmlWriter.endtag(tableName) 1550 xmlWriter.newline() 1551 1552 def getChildren(self, colr): 1553 if self.Format == PaintFormat.PaintColrLayers: 1554 # https://github.com/fonttools/fonttools/issues/2438: don't die when no LayerList exists 1555 layers = [] 1556 if colr.LayerList is not None: 1557 layers = colr.LayerList.Paint 1558 return layers[ 1559 self.FirstLayerIndex : self.FirstLayerIndex + self.NumLayers 1560 ] 1561 1562 if self.Format == PaintFormat.PaintColrGlyph: 1563 for record in colr.BaseGlyphList.BaseGlyphPaintRecord: 1564 if record.BaseGlyph == self.Glyph: 1565 return [record.Paint] 1566 else: 1567 raise KeyError(f"{self.Glyph!r} not in colr.BaseGlyphList") 1568 1569 children = [] 1570 for conv in self.getConverters(): 1571 if conv.tableClass is not None and issubclass(conv.tableClass, type(self)): 1572 children.append(getattr(self, conv.name)) 1573 1574 return children 1575 1576 def traverse(self, colr: COLR, callback): 1577 """Depth-first traversal of graph rooted at self, callback on each node.""" 1578 if not callable(callback): 1579 raise TypeError("callback must be callable") 1580 stack = [self] 1581 visited = set() 1582 while stack: 1583 current = stack.pop() 1584 if id(current) in visited: 1585 continue 1586 callback(current) 1587 visited.add(id(current)) 1588 stack.extend(reversed(current.getChildren(colr))) 1589 1590 1591# For each subtable format there is a class. However, we don't really distinguish 1592# between "field name" and "format name": often these are the same. Yet there's 1593# a whole bunch of fields with different names. The following dict is a mapping 1594# from "format name" to "field name". _buildClasses() uses this to create a 1595# subclass for each alternate field name. 1596# 1597_equivalents = { 1598 'MarkArray': ("Mark1Array",), 1599 'LangSys': ('DefaultLangSys',), 1600 'Coverage': ('MarkCoverage', 'BaseCoverage', 'LigatureCoverage', 'Mark1Coverage', 1601 'Mark2Coverage', 'BacktrackCoverage', 'InputCoverage', 1602 'LookAheadCoverage', 'VertGlyphCoverage', 'HorizGlyphCoverage', 1603 'TopAccentCoverage', 'ExtendedShapeCoverage', 'MathKernCoverage'), 1604 'ClassDef': ('ClassDef1', 'ClassDef2', 'BacktrackClassDef', 'InputClassDef', 1605 'LookAheadClassDef', 'GlyphClassDef', 'MarkAttachClassDef'), 1606 'Anchor': ('EntryAnchor', 'ExitAnchor', 'BaseAnchor', 'LigatureAnchor', 1607 'Mark2Anchor', 'MarkAnchor'), 1608 'Device': ('XPlaDevice', 'YPlaDevice', 'XAdvDevice', 'YAdvDevice', 1609 'XDeviceTable', 'YDeviceTable', 'DeviceTable'), 1610 'Axis': ('HorizAxis', 'VertAxis',), 1611 'MinMax': ('DefaultMinMax',), 1612 'BaseCoord': ('MinCoord', 'MaxCoord',), 1613 'JstfLangSys': ('DefJstfLangSys',), 1614 'JstfGSUBModList': ('ShrinkageEnableGSUB', 'ShrinkageDisableGSUB', 'ExtensionEnableGSUB', 1615 'ExtensionDisableGSUB',), 1616 'JstfGPOSModList': ('ShrinkageEnableGPOS', 'ShrinkageDisableGPOS', 'ExtensionEnableGPOS', 1617 'ExtensionDisableGPOS',), 1618 'JstfMax': ('ShrinkageJstfMax', 'ExtensionJstfMax',), 1619 'MathKern': ('TopRightMathKern', 'TopLeftMathKern', 'BottomRightMathKern', 1620 'BottomLeftMathKern'), 1621 'MathGlyphConstruction': ('VertGlyphConstruction', 'HorizGlyphConstruction'), 1622} 1623 1624# 1625# OverFlow logic, to automatically create ExtensionLookups 1626# XXX This should probably move to otBase.py 1627# 1628 1629def fixLookupOverFlows(ttf, overflowRecord): 1630 """ Either the offset from the LookupList to a lookup overflowed, or 1631 an offset from a lookup to a subtable overflowed. 1632 The table layout is: 1633 GPSO/GUSB 1634 Script List 1635 Feature List 1636 LookUpList 1637 Lookup[0] and contents 1638 SubTable offset list 1639 SubTable[0] and contents 1640 ... 1641 SubTable[n] and contents 1642 ... 1643 Lookup[n] and contents 1644 SubTable offset list 1645 SubTable[0] and contents 1646 ... 1647 SubTable[n] and contents 1648 If the offset to a lookup overflowed (SubTableIndex is None) 1649 we must promote the *previous* lookup to an Extension type. 1650 If the offset from a lookup to subtable overflowed, then we must promote it 1651 to an Extension Lookup type. 1652 """ 1653 ok = 0 1654 lookupIndex = overflowRecord.LookupListIndex 1655 if (overflowRecord.SubTableIndex is None): 1656 lookupIndex = lookupIndex - 1 1657 if lookupIndex < 0: 1658 return ok 1659 if overflowRecord.tableType == 'GSUB': 1660 extType = 7 1661 elif overflowRecord.tableType == 'GPOS': 1662 extType = 9 1663 1664 lookups = ttf[overflowRecord.tableType].table.LookupList.Lookup 1665 lookup = lookups[lookupIndex] 1666 # If the previous lookup is an extType, look further back. Very unlikely, but possible. 1667 while lookup.SubTable[0].__class__.LookupType == extType: 1668 lookupIndex = lookupIndex -1 1669 if lookupIndex < 0: 1670 return ok 1671 lookup = lookups[lookupIndex] 1672 1673 for lookupIndex in range(lookupIndex, len(lookups)): 1674 lookup = lookups[lookupIndex] 1675 if lookup.LookupType != extType: 1676 lookup.LookupType = extType 1677 for si in range(len(lookup.SubTable)): 1678 subTable = lookup.SubTable[si] 1679 extSubTableClass = lookupTypes[overflowRecord.tableType][extType] 1680 extSubTable = extSubTableClass() 1681 extSubTable.Format = 1 1682 extSubTable.ExtSubTable = subTable 1683 lookup.SubTable[si] = extSubTable 1684 ok = 1 1685 return ok 1686 1687def splitMultipleSubst(oldSubTable, newSubTable, overflowRecord): 1688 ok = 1 1689 oldMapping = sorted(oldSubTable.mapping.items()) 1690 oldLen = len(oldMapping) 1691 1692 if overflowRecord.itemName in ['Coverage', 'RangeRecord']: 1693 # Coverage table is written last. Overflow is to or within the 1694 # the coverage table. We will just cut the subtable in half. 1695 newLen = oldLen // 2 1696 1697 elif overflowRecord.itemName == 'Sequence': 1698 # We just need to back up by two items from the overflowed 1699 # Sequence index to make sure the offset to the Coverage table 1700 # doesn't overflow. 1701 newLen = overflowRecord.itemIndex - 1 1702 1703 newSubTable.mapping = {} 1704 for i in range(newLen, oldLen): 1705 item = oldMapping[i] 1706 key = item[0] 1707 newSubTable.mapping[key] = item[1] 1708 del oldSubTable.mapping[key] 1709 1710 return ok 1711 1712def splitAlternateSubst(oldSubTable, newSubTable, overflowRecord): 1713 ok = 1 1714 if hasattr(oldSubTable, 'sortCoverageLast'): 1715 newSubTable.sortCoverageLast = oldSubTable.sortCoverageLast 1716 1717 oldAlts = sorted(oldSubTable.alternates.items()) 1718 oldLen = len(oldAlts) 1719 1720 if overflowRecord.itemName in [ 'Coverage', 'RangeRecord']: 1721 # Coverage table is written last. overflow is to or within the 1722 # the coverage table. We will just cut the subtable in half. 1723 newLen = oldLen//2 1724 1725 elif overflowRecord.itemName == 'AlternateSet': 1726 # We just need to back up by two items 1727 # from the overflowed AlternateSet index to make sure the offset 1728 # to the Coverage table doesn't overflow. 1729 newLen = overflowRecord.itemIndex - 1 1730 1731 newSubTable.alternates = {} 1732 for i in range(newLen, oldLen): 1733 item = oldAlts[i] 1734 key = item[0] 1735 newSubTable.alternates[key] = item[1] 1736 del oldSubTable.alternates[key] 1737 1738 return ok 1739 1740 1741def splitLigatureSubst(oldSubTable, newSubTable, overflowRecord): 1742 ok = 1 1743 oldLigs = sorted(oldSubTable.ligatures.items()) 1744 oldLen = len(oldLigs) 1745 1746 if overflowRecord.itemName in [ 'Coverage', 'RangeRecord']: 1747 # Coverage table is written last. overflow is to or within the 1748 # the coverage table. We will just cut the subtable in half. 1749 newLen = oldLen//2 1750 1751 elif overflowRecord.itemName == 'LigatureSet': 1752 # We just need to back up by two items 1753 # from the overflowed AlternateSet index to make sure the offset 1754 # to the Coverage table doesn't overflow. 1755 newLen = overflowRecord.itemIndex - 1 1756 1757 newSubTable.ligatures = {} 1758 for i in range(newLen, oldLen): 1759 item = oldLigs[i] 1760 key = item[0] 1761 newSubTable.ligatures[key] = item[1] 1762 del oldSubTable.ligatures[key] 1763 1764 return ok 1765 1766 1767def splitPairPos(oldSubTable, newSubTable, overflowRecord): 1768 st = oldSubTable 1769 ok = False 1770 newSubTable.Format = oldSubTable.Format 1771 if oldSubTable.Format == 1 and len(oldSubTable.PairSet) > 1: 1772 for name in 'ValueFormat1', 'ValueFormat2': 1773 setattr(newSubTable, name, getattr(oldSubTable, name)) 1774 1775 # Move top half of coverage to new subtable 1776 1777 newSubTable.Coverage = oldSubTable.Coverage.__class__() 1778 1779 coverage = oldSubTable.Coverage.glyphs 1780 records = oldSubTable.PairSet 1781 1782 oldCount = len(oldSubTable.PairSet) // 2 1783 1784 oldSubTable.Coverage.glyphs = coverage[:oldCount] 1785 oldSubTable.PairSet = records[:oldCount] 1786 1787 newSubTable.Coverage.glyphs = coverage[oldCount:] 1788 newSubTable.PairSet = records[oldCount:] 1789 1790 oldSubTable.PairSetCount = len(oldSubTable.PairSet) 1791 newSubTable.PairSetCount = len(newSubTable.PairSet) 1792 1793 ok = True 1794 1795 elif oldSubTable.Format == 2 and len(oldSubTable.Class1Record) > 1: 1796 if not hasattr(oldSubTable, 'Class2Count'): 1797 oldSubTable.Class2Count = len(oldSubTable.Class1Record[0].Class2Record) 1798 for name in 'Class2Count', 'ClassDef2', 'ValueFormat1', 'ValueFormat2': 1799 setattr(newSubTable, name, getattr(oldSubTable, name)) 1800 1801 # The two subtables will still have the same ClassDef2 and the table 1802 # sharing will still cause the sharing to overflow. As such, disable 1803 # sharing on the one that is serialized second (that's oldSubTable). 1804 oldSubTable.DontShare = True 1805 1806 # Move top half of class numbers to new subtable 1807 1808 newSubTable.Coverage = oldSubTable.Coverage.__class__() 1809 newSubTable.ClassDef1 = oldSubTable.ClassDef1.__class__() 1810 1811 coverage = oldSubTable.Coverage.glyphs 1812 classDefs = oldSubTable.ClassDef1.classDefs 1813 records = oldSubTable.Class1Record 1814 1815 oldCount = len(oldSubTable.Class1Record) // 2 1816 newGlyphs = set(k for k,v in classDefs.items() if v >= oldCount) 1817 1818 oldSubTable.Coverage.glyphs = [g for g in coverage if g not in newGlyphs] 1819 oldSubTable.ClassDef1.classDefs = {k:v for k,v in classDefs.items() if v < oldCount} 1820 oldSubTable.Class1Record = records[:oldCount] 1821 1822 newSubTable.Coverage.glyphs = [g for g in coverage if g in newGlyphs] 1823 newSubTable.ClassDef1.classDefs = {k:(v-oldCount) for k,v in classDefs.items() if v > oldCount} 1824 newSubTable.Class1Record = records[oldCount:] 1825 1826 oldSubTable.Class1Count = len(oldSubTable.Class1Record) 1827 newSubTable.Class1Count = len(newSubTable.Class1Record) 1828 1829 ok = True 1830 1831 return ok 1832 1833 1834def splitMarkBasePos(oldSubTable, newSubTable, overflowRecord): 1835 # split half of the mark classes to the new subtable 1836 classCount = oldSubTable.ClassCount 1837 if classCount < 2: 1838 # oh well, not much left to split... 1839 return False 1840 1841 oldClassCount = classCount // 2 1842 newClassCount = classCount - oldClassCount 1843 1844 oldMarkCoverage, oldMarkRecords = [], [] 1845 newMarkCoverage, newMarkRecords = [], [] 1846 for glyphName, markRecord in zip( 1847 oldSubTable.MarkCoverage.glyphs, 1848 oldSubTable.MarkArray.MarkRecord 1849 ): 1850 if markRecord.Class < oldClassCount: 1851 oldMarkCoverage.append(glyphName) 1852 oldMarkRecords.append(markRecord) 1853 else: 1854 markRecord.Class -= oldClassCount 1855 newMarkCoverage.append(glyphName) 1856 newMarkRecords.append(markRecord) 1857 1858 oldBaseRecords, newBaseRecords = [], [] 1859 for rec in oldSubTable.BaseArray.BaseRecord: 1860 oldBaseRecord, newBaseRecord = rec.__class__(), rec.__class__() 1861 oldBaseRecord.BaseAnchor = rec.BaseAnchor[:oldClassCount] 1862 newBaseRecord.BaseAnchor = rec.BaseAnchor[oldClassCount:] 1863 oldBaseRecords.append(oldBaseRecord) 1864 newBaseRecords.append(newBaseRecord) 1865 1866 newSubTable.Format = oldSubTable.Format 1867 1868 oldSubTable.MarkCoverage.glyphs = oldMarkCoverage 1869 newSubTable.MarkCoverage = oldSubTable.MarkCoverage.__class__() 1870 newSubTable.MarkCoverage.glyphs = newMarkCoverage 1871 1872 # share the same BaseCoverage in both halves 1873 newSubTable.BaseCoverage = oldSubTable.BaseCoverage 1874 1875 oldSubTable.ClassCount = oldClassCount 1876 newSubTable.ClassCount = newClassCount 1877 1878 oldSubTable.MarkArray.MarkRecord = oldMarkRecords 1879 newSubTable.MarkArray = oldSubTable.MarkArray.__class__() 1880 newSubTable.MarkArray.MarkRecord = newMarkRecords 1881 1882 oldSubTable.MarkArray.MarkCount = len(oldMarkRecords) 1883 newSubTable.MarkArray.MarkCount = len(newMarkRecords) 1884 1885 oldSubTable.BaseArray.BaseRecord = oldBaseRecords 1886 newSubTable.BaseArray = oldSubTable.BaseArray.__class__() 1887 newSubTable.BaseArray.BaseRecord = newBaseRecords 1888 1889 oldSubTable.BaseArray.BaseCount = len(oldBaseRecords) 1890 newSubTable.BaseArray.BaseCount = len(newBaseRecords) 1891 1892 return True 1893 1894 1895splitTable = { 'GSUB': { 1896# 1: splitSingleSubst, 1897 2: splitMultipleSubst, 1898 3: splitAlternateSubst, 1899 4: splitLigatureSubst, 1900# 5: splitContextSubst, 1901# 6: splitChainContextSubst, 1902# 7: splitExtensionSubst, 1903# 8: splitReverseChainSingleSubst, 1904 }, 1905 'GPOS': { 1906# 1: splitSinglePos, 1907 2: splitPairPos, 1908# 3: splitCursivePos, 1909 4: splitMarkBasePos, 1910# 5: splitMarkLigPos, 1911# 6: splitMarkMarkPos, 1912# 7: splitContextPos, 1913# 8: splitChainContextPos, 1914# 9: splitExtensionPos, 1915 } 1916 1917 } 1918 1919def fixSubTableOverFlows(ttf, overflowRecord): 1920 """ 1921 An offset has overflowed within a sub-table. We need to divide this subtable into smaller parts. 1922 """ 1923 table = ttf[overflowRecord.tableType].table 1924 lookup = table.LookupList.Lookup[overflowRecord.LookupListIndex] 1925 subIndex = overflowRecord.SubTableIndex 1926 subtable = lookup.SubTable[subIndex] 1927 1928 # First, try not sharing anything for this subtable... 1929 if not hasattr(subtable, "DontShare"): 1930 subtable.DontShare = True 1931 return True 1932 1933 if hasattr(subtable, 'ExtSubTable'): 1934 # We split the subtable of the Extension table, and add a new Extension table 1935 # to contain the new subtable. 1936 1937 subTableType = subtable.ExtSubTable.__class__.LookupType 1938 extSubTable = subtable 1939 subtable = extSubTable.ExtSubTable 1940 newExtSubTableClass = lookupTypes[overflowRecord.tableType][extSubTable.__class__.LookupType] 1941 newExtSubTable = newExtSubTableClass() 1942 newExtSubTable.Format = extSubTable.Format 1943 toInsert = newExtSubTable 1944 1945 newSubTableClass = lookupTypes[overflowRecord.tableType][subTableType] 1946 newSubTable = newSubTableClass() 1947 newExtSubTable.ExtSubTable = newSubTable 1948 else: 1949 subTableType = subtable.__class__.LookupType 1950 newSubTableClass = lookupTypes[overflowRecord.tableType][subTableType] 1951 newSubTable = newSubTableClass() 1952 toInsert = newSubTable 1953 1954 if hasattr(lookup, 'SubTableCount'): # may not be defined yet. 1955 lookup.SubTableCount = lookup.SubTableCount + 1 1956 1957 try: 1958 splitFunc = splitTable[overflowRecord.tableType][subTableType] 1959 except KeyError: 1960 log.error( 1961 "Don't know how to split %s lookup type %s", 1962 overflowRecord.tableType, 1963 subTableType, 1964 ) 1965 return False 1966 1967 ok = splitFunc(subtable, newSubTable, overflowRecord) 1968 if ok: 1969 lookup.SubTable.insert(subIndex + 1, toInsert) 1970 return ok 1971 1972# End of OverFlow logic 1973 1974 1975def _buildClasses(): 1976 import re 1977 from .otData import otData 1978 1979 formatPat = re.compile(r"([A-Za-z0-9]+)Format(\d+)$") 1980 namespace = globals() 1981 1982 # populate module with classes 1983 for name, table in otData: 1984 baseClass = BaseTable 1985 m = formatPat.match(name) 1986 if m: 1987 # XxxFormatN subtable, we only add the "base" table 1988 name = m.group(1) 1989 # the first row of a format-switching otData table describes the Format; 1990 # the first column defines the type of the Format field. 1991 # Currently this can be either 'uint16' or 'uint8'. 1992 formatType = table[0][0] 1993 baseClass = getFormatSwitchingBaseTableClass(formatType) 1994 if name not in namespace: 1995 # the class doesn't exist yet, so the base implementation is used. 1996 cls = type(name, (baseClass,), {}) 1997 if name in ('GSUB', 'GPOS'): 1998 cls.DontShare = True 1999 namespace[name] = cls 2000 2001 # link Var{Table} <-> {Table} (e.g. ColorStop <-> VarColorStop, etc.) 2002 for name, _ in otData: 2003 if name.startswith("Var") and len(name) > 3 and name[3:] in namespace: 2004 varType = namespace[name] 2005 noVarType = namespace[name[3:]] 2006 varType.NoVarType = noVarType 2007 noVarType.VarType = varType 2008 2009 for base, alts in _equivalents.items(): 2010 base = namespace[base] 2011 for alt in alts: 2012 namespace[alt] = base 2013 2014 global lookupTypes 2015 lookupTypes = { 2016 'GSUB': { 2017 1: SingleSubst, 2018 2: MultipleSubst, 2019 3: AlternateSubst, 2020 4: LigatureSubst, 2021 5: ContextSubst, 2022 6: ChainContextSubst, 2023 7: ExtensionSubst, 2024 8: ReverseChainSingleSubst, 2025 }, 2026 'GPOS': { 2027 1: SinglePos, 2028 2: PairPos, 2029 3: CursivePos, 2030 4: MarkBasePos, 2031 5: MarkLigPos, 2032 6: MarkMarkPos, 2033 7: ContextPos, 2034 8: ChainContextPos, 2035 9: ExtensionPos, 2036 }, 2037 'mort': { 2038 4: NoncontextualMorph, 2039 }, 2040 'morx': { 2041 0: RearrangementMorph, 2042 1: ContextualMorph, 2043 2: LigatureMorph, 2044 # 3: Reserved, 2045 4: NoncontextualMorph, 2046 5: InsertionMorph, 2047 }, 2048 } 2049 lookupTypes['JSTF'] = lookupTypes['GPOS'] # JSTF contains GPOS 2050 for lookupEnum in lookupTypes.values(): 2051 for enum, cls in lookupEnum.items(): 2052 cls.LookupType = enum 2053 2054 global featureParamTypes 2055 featureParamTypes = { 2056 'size': FeatureParamsSize, 2057 } 2058 for i in range(1, 20+1): 2059 featureParamTypes['ss%02d' % i] = FeatureParamsStylisticSet 2060 for i in range(1, 99+1): 2061 featureParamTypes['cv%02d' % i] = FeatureParamsCharacterVariants 2062 2063 # add converters to classes 2064 from .otConverters import buildConverters 2065 for name, table in otData: 2066 m = formatPat.match(name) 2067 if m: 2068 # XxxFormatN subtable, add converter to "base" table 2069 name, format = m.groups() 2070 format = int(format) 2071 cls = namespace[name] 2072 if not hasattr(cls, "converters"): 2073 cls.converters = {} 2074 cls.convertersByName = {} 2075 converters, convertersByName = buildConverters(table[1:], namespace) 2076 cls.converters[format] = converters 2077 cls.convertersByName[format] = convertersByName 2078 # XXX Add staticSize? 2079 else: 2080 cls = namespace[name] 2081 cls.converters, cls.convertersByName = buildConverters(table, namespace) 2082 # XXX Add staticSize? 2083 2084 2085_buildClasses() 2086 2087 2088def _getGlyphsFromCoverageTable(coverage): 2089 if coverage is None: 2090 # empty coverage table 2091 return [] 2092 else: 2093 return coverage.glyphs 2094