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