Coverage for /usr/lib/python3/dist-packages/fontTools/ttLib/tables/otTables.py: 20%
1521 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
1# coding: utf-8
2"""fontTools.ttLib.tables.otTables -- A collection of classes representing the various
3OpenType subtables.
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
33if TYPE_CHECKING:
34 from fontTools.ttLib.ttGlyphSet import _TTGlyphSet
37log = logging.getLogger(__name__)
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}, ...]
47class AATState(object):
48 def __init__(self):
49 self.Transitions = {} # GlyphClass --> AATAction
52class AATAction(object):
53 _FLAGS = None
55 @staticmethod
56 def compileActions(font, states):
57 return (None, None)
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()
68 def _setFlag(self, flag):
69 assert flag in self._FLAGS, "unsupported flag %s" % flag
70 self.__dict__[flag] = True
73class RearrangementMorphAction(AATAction):
74 staticSize = 4
75 actionHeaderSize = 0
76 _FLAGS = ["MarkFirst", "DontAdvance", "MarkLast"]
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 }
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
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)
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
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()
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())
158class ContextualMorphAction(AATAction):
159 staticSize = 8
160 actionHeaderSize = 0
161 _FLAGS = ["SetMark", "DontAdvance"]
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
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)
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()
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()
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"])
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
238class LigatureMorphAction(AATAction):
239 staticSize = 6
241 # 4 bytes for each of {action,ligComponents,ligatures}Offset
242 actionHeaderSize = 12
244 _FLAGS = ["SetComponent", "DontAdvance"]
246 def __init__(self):
247 self.NewState = 0
248 self.SetComponent, self.DontAdvance = False, False
249 self.ReservedFlags = 0
250 self.Actions = []
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)
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 = []
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)
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)
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
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)
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()
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 ]
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 = [], []
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)
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 )
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))
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()
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
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
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)
524 def toXML(self, xmlWriter, font, attrs=None, name=None):
525 BaseTable.toXML(self, xmlWriter, font, attrs, name=self.__class__.__name__)
528class FeatureParamsSize(FeatureParams):
529 pass
532class FeatureParamsStylisticSet(FeatureParams):
533 pass
536class FeatureParamsCharacterVariants(FeatureParams):
537 pass
540class Coverage(FormatSwitchingBaseTable):
541 # manual implementation to get rid of glyphID dependencies
543 def populateDefaults(self, propagator=None):
544 if not hasattr(self, "glyphs"):
545 self.glyphs = []
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
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
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)
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
616 def toXML2(self, xmlWriter, font):
617 for glyphName in getattr(self, "glyphs", []):
618 xmlWriter.simpletag("Glyph", value=glyphName)
619 xmlWriter.newline()
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"])
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
634class DeltaSetIndexMap(getFormatSwitchingBaseTableClass("uint8")):
635 def populateDefaults(self, propagator=None):
636 if not hasattr(self, "mapping"):
637 self.mapping = []
639 def postRead(self, rawTable, font):
640 assert (rawTable["EntryFormat"] & 0xFFC0) == 0
641 self.mapping = rawTable["mapping"]
643 @staticmethod
644 def getEntryFormat(mapping):
645 ored = 0
646 for idx in mapping:
647 ored |= idx
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
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
667 return ((entrySize - 1) << 4) | (innerBits - 1)
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
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()
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)
707class VarIdxMap(BaseTable):
708 def populateDefaults(self, propagator=None):
709 if not hasattr(self, "mapping"):
710 self.mapping = {}
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))
719 def preWrite(self, font):
720 mapping = getattr(self, "mapping", None)
721 if mapping is None:
722 mapping = self.mapping = {}
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]
729 rawTable = {"mapping": mapping}
730 rawTable["MappingCount"] = len(mapping)
731 rawTable["EntryFormat"] = DeltaSetIndexMap.getEntryFormat(mapping)
732 return rawTable
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()
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
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 }
777class SingleSubst(FormatSwitchingBaseTable):
778 def populateDefaults(self, propagator=None):
779 if not hasattr(self, "mapping"):
780 self.mapping = {}
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
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))
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
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
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
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()
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"]
857class MultipleSubst(FormatSwitchingBaseTable):
858 def populateDefaults(self, propagator=None):
859 if not hasattr(self, "mapping"):
860 self.mapping = {}
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
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
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()
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
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
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]
925 @staticmethod
926 def makeSequence_(g):
927 seq = Sequence()
928 seq.Substitute = g
929 return seq
932class ClassDef(FormatSwitchingBaseTable):
933 def populateDefaults(self, propagator=None):
934 if not hasattr(self, "classDefs"):
935 self.classDefs = {}
937 def postRead(self, rawTable, font):
938 classDefs = {}
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
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
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
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
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()
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"])
1038class AlternateSubst(FormatSwitchingBaseTable):
1039 def populateDefaults(self, propagator=None):
1040 if not hasattr(self, "alternates"):
1041 self.alternates = {}
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
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}
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()
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"])
1108class LigatureSubst(FormatSwitchingBaseTable):
1109 def populateDefaults(self, propagator=None):
1110 if not hasattr(self, "ligatures"):
1111 self.ligatures = {}
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
1126 def preWrite(self, font):
1127 self.Format = 1
1128 ligatures = getattr(self, "ligatures", None)
1129 if ligatures is None:
1130 ligatures = self.ligatures = {}
1132 if ligatures and isinstance(next(iter(ligatures)), tuple):
1133 # New high-level API in v3.1 and later. Note that we just support compiling this
1134 # for now. We don't load to this API, and don't do XML with it.
1136 # ligatures is map from components-sequence to lig-glyph
1137 newLigatures = dict()
1138 for comps, lig in sorted(
1139 ligatures.items(), key=lambda item: (-len(item[0]), item[0])
1140 ):
1141 ligature = Ligature()
1142 ligature.Component = comps[1:]
1143 ligature.CompCount = len(comps)
1144 ligature.LigGlyph = lig
1145 newLigatures.setdefault(comps[0], []).append(ligature)
1146 ligatures = newLigatures
1148 items = list(ligatures.items())
1149 for i in range(len(items)):
1150 glyphName, set = items[i]
1151 items[i] = font.getGlyphID(glyphName), glyphName, set
1152 items.sort()
1153 cov = Coverage()
1154 cov.glyphs = [item[1] for item in items]
1156 ligSets = []
1157 setList = [item[-1] for item in items]
1158 for set in setList:
1159 ligSet = LigatureSet()
1160 ligs = ligSet.Ligature = []
1161 for lig in set:
1162 ligs.append(lig)
1163 ligSets.append(ligSet)
1164 # Useful in that when splitting a sub-table because of an offset overflow
1165 # I don't need to calculate the change in subtabl offset due to the coverage table size.
1166 # Allows packing more rules in subtable.
1167 self.sortCoverageLast = 1
1168 return {"Coverage": cov, "LigatureSet": ligSets}
1170 def toXML2(self, xmlWriter, font):
1171 items = sorted(self.ligatures.items())
1172 for glyphName, ligSets in items:
1173 xmlWriter.begintag("LigatureSet", glyph=glyphName)
1174 xmlWriter.newline()
1175 for lig in ligSets:
1176 xmlWriter.simpletag(
1177 "Ligature", glyph=lig.LigGlyph, components=",".join(lig.Component)
1178 )
1179 xmlWriter.newline()
1180 xmlWriter.endtag("LigatureSet")
1181 xmlWriter.newline()
1183 def fromXML(self, name, attrs, content, font):
1184 ligatures = getattr(self, "ligatures", None)
1185 if ligatures is None:
1186 ligatures = {}
1187 self.ligatures = ligatures
1188 glyphName = attrs["glyph"]
1189 ligs = []
1190 ligatures[glyphName] = ligs
1191 for element in content:
1192 if not isinstance(element, tuple):
1193 continue
1194 name, attrs, content = element
1195 lig = Ligature()
1196 lig.LigGlyph = attrs["glyph"]
1197 components = attrs["components"]
1198 lig.Component = components.split(",") if components else []
1199 lig.CompCount = len(lig.Component)
1200 ligs.append(lig)
1203class COLR(BaseTable):
1204 def decompile(self, reader, font):
1205 # COLRv0 is exceptional in that LayerRecordCount appears *after* the
1206 # LayerRecordArray it counts, but the parser logic expects Count fields
1207 # to always precede the arrays. Here we work around this by parsing the
1208 # LayerRecordCount before the rest of the table, and storing it in
1209 # the reader's local state.
1210 subReader = reader.getSubReader(offset=0)
1211 for conv in self.getConverters():
1212 if conv.name != "LayerRecordCount":
1213 subReader.advance(conv.staticSize)
1214 continue
1215 reader[conv.name] = conv.read(subReader, font, tableDict={})
1216 break
1217 else:
1218 raise AssertionError("LayerRecordCount converter not found")
1219 return BaseTable.decompile(self, reader, font)
1221 def preWrite(self, font):
1222 # The writer similarly assumes Count values precede the things counted,
1223 # thus here we pre-initialize a CountReference; the actual count value
1224 # will be set to the lenght of the array by the time this is assembled.
1225 self.LayerRecordCount = None
1226 return {
1227 **self.__dict__,
1228 "LayerRecordCount": CountReference(self.__dict__, "LayerRecordCount"),
1229 }
1231 def computeClipBoxes(self, glyphSet: "_TTGlyphSet", quantization: int = 1):
1232 if self.Version == 0:
1233 return
1235 clips = {}
1236 for rec in self.BaseGlyphList.BaseGlyphPaintRecord:
1237 try:
1238 clipBox = rec.Paint.computeClipBox(self, glyphSet, quantization)
1239 except Exception as e:
1240 from fontTools.ttLib import TTLibError
1242 raise TTLibError(
1243 f"Failed to compute COLR ClipBox for {rec.BaseGlyph!r}"
1244 ) from e
1246 if clipBox is not None:
1247 clips[rec.BaseGlyph] = clipBox
1249 hasClipList = hasattr(self, "ClipList") and self.ClipList is not None
1250 if not clips:
1251 if hasClipList:
1252 self.ClipList = None
1253 else:
1254 if not hasClipList:
1255 self.ClipList = ClipList()
1256 self.ClipList.Format = 1
1257 self.ClipList.clips = clips
1260class LookupList(BaseTable):
1261 @property
1262 def table(self):
1263 for l in self.Lookup:
1264 for st in l.SubTable:
1265 if type(st).__name__.endswith("Subst"):
1266 return "GSUB"
1267 if type(st).__name__.endswith("Pos"):
1268 return "GPOS"
1269 raise ValueError
1271 def toXML2(self, xmlWriter, font):
1272 if (
1273 not font
1274 or "Debg" not in font
1275 or LOOKUP_DEBUG_INFO_KEY not in font["Debg"].data
1276 ):
1277 return super().toXML2(xmlWriter, font)
1278 debugData = font["Debg"].data[LOOKUP_DEBUG_INFO_KEY][self.table]
1279 for conv in self.getConverters():
1280 if conv.repeat:
1281 value = getattr(self, conv.name, [])
1282 for lookupIndex, item in enumerate(value):
1283 if str(lookupIndex) in debugData:
1284 info = LookupDebugInfo(*debugData[str(lookupIndex)])
1285 tag = info.location
1286 if info.name:
1287 tag = f"{info.name}: {tag}"
1288 if info.feature:
1289 script, language, feature = info.feature
1290 tag = f"{tag} in {feature} ({script}/{language})"
1291 xmlWriter.comment(tag)
1292 xmlWriter.newline()
1294 conv.xmlWrite(
1295 xmlWriter, font, item, conv.name, [("index", lookupIndex)]
1296 )
1297 else:
1298 if conv.aux and not eval(conv.aux, None, vars(self)):
1299 continue
1300 value = getattr(
1301 self, conv.name, None
1302 ) # TODO Handle defaults instead of defaulting to None!
1303 conv.xmlWrite(xmlWriter, font, value, conv.name, [])
1306class BaseGlyphRecordArray(BaseTable):
1307 def preWrite(self, font):
1308 self.BaseGlyphRecord = sorted(
1309 self.BaseGlyphRecord, key=lambda rec: font.getGlyphID(rec.BaseGlyph)
1310 )
1311 return self.__dict__.copy()
1314class BaseGlyphList(BaseTable):
1315 def preWrite(self, font):
1316 self.BaseGlyphPaintRecord = sorted(
1317 self.BaseGlyphPaintRecord, key=lambda rec: font.getGlyphID(rec.BaseGlyph)
1318 )
1319 return self.__dict__.copy()
1322class ClipBoxFormat(IntEnum):
1323 Static = 1
1324 Variable = 2
1326 def is_variable(self):
1327 return self is self.Variable
1329 def as_variable(self):
1330 return self.Variable
1333class ClipBox(getFormatSwitchingBaseTableClass("uint8")):
1334 formatEnum = ClipBoxFormat
1336 def as_tuple(self):
1337 return tuple(getattr(self, conv.name) for conv in self.getConverters())
1339 def __repr__(self):
1340 return f"{self.__class__.__name__}{self.as_tuple()}"
1343class ClipList(getFormatSwitchingBaseTableClass("uint8")):
1344 def populateDefaults(self, propagator=None):
1345 if not hasattr(self, "clips"):
1346 self.clips = {}
1348 def postRead(self, rawTable, font):
1349 clips = {}
1350 glyphOrder = font.getGlyphOrder()
1351 for i, rec in enumerate(rawTable["ClipRecord"]):
1352 if rec.StartGlyphID > rec.EndGlyphID:
1353 log.warning(
1354 "invalid ClipRecord[%i].StartGlyphID (%i) > "
1355 "EndGlyphID (%i); skipped",
1356 i,
1357 rec.StartGlyphID,
1358 rec.EndGlyphID,
1359 )
1360 continue
1361 redefinedGlyphs = []
1362 missingGlyphs = []
1363 for glyphID in range(rec.StartGlyphID, rec.EndGlyphID + 1):
1364 try:
1365 glyph = glyphOrder[glyphID]
1366 except IndexError:
1367 missingGlyphs.append(glyphID)
1368 continue
1369 if glyph not in clips:
1370 clips[glyph] = copy.copy(rec.ClipBox)
1371 else:
1372 redefinedGlyphs.append(glyphID)
1373 if redefinedGlyphs:
1374 log.warning(
1375 "ClipRecord[%i] overlaps previous records; "
1376 "ignoring redefined clip boxes for the "
1377 "following glyph ID range: [%i-%i]",
1378 i,
1379 min(redefinedGlyphs),
1380 max(redefinedGlyphs),
1381 )
1382 if missingGlyphs:
1383 log.warning(
1384 "ClipRecord[%i] range references missing " "glyph IDs: [%i-%i]",
1385 i,
1386 min(missingGlyphs),
1387 max(missingGlyphs),
1388 )
1389 self.clips = clips
1391 def groups(self):
1392 glyphsByClip = defaultdict(list)
1393 uniqueClips = {}
1394 for glyphName, clipBox in self.clips.items():
1395 key = clipBox.as_tuple()
1396 glyphsByClip[key].append(glyphName)
1397 if key not in uniqueClips:
1398 uniqueClips[key] = clipBox
1399 return {
1400 frozenset(glyphs): uniqueClips[key] for key, glyphs in glyphsByClip.items()
1401 }
1403 def preWrite(self, font):
1404 if not hasattr(self, "clips"):
1405 self.clips = {}
1406 clipBoxRanges = {}
1407 glyphMap = font.getReverseGlyphMap()
1408 for glyphs, clipBox in self.groups().items():
1409 glyphIDs = sorted(
1410 glyphMap[glyphName] for glyphName in glyphs if glyphName in glyphMap
1411 )
1412 if not glyphIDs:
1413 continue
1414 last = glyphIDs[0]
1415 ranges = [[last]]
1416 for glyphID in glyphIDs[1:]:
1417 if glyphID != last + 1:
1418 ranges[-1].append(last)
1419 ranges.append([glyphID])
1420 last = glyphID
1421 ranges[-1].append(last)
1422 for start, end in ranges:
1423 assert (start, end) not in clipBoxRanges
1424 clipBoxRanges[(start, end)] = clipBox
1426 clipRecords = []
1427 for (start, end), clipBox in sorted(clipBoxRanges.items()):
1428 record = ClipRecord()
1429 record.StartGlyphID = start
1430 record.EndGlyphID = end
1431 record.ClipBox = clipBox
1432 clipRecords.append(record)
1433 rawTable = {
1434 "ClipCount": len(clipRecords),
1435 "ClipRecord": clipRecords,
1436 }
1437 return rawTable
1439 def toXML(self, xmlWriter, font, attrs=None, name=None):
1440 tableName = name if name else self.__class__.__name__
1441 if attrs is None:
1442 attrs = []
1443 if hasattr(self, "Format"):
1444 attrs.append(("Format", self.Format))
1445 xmlWriter.begintag(tableName, attrs)
1446 xmlWriter.newline()
1447 # sort clips alphabetically to ensure deterministic XML dump
1448 for glyphs, clipBox in sorted(
1449 self.groups().items(), key=lambda item: min(item[0])
1450 ):
1451 xmlWriter.begintag("Clip")
1452 xmlWriter.newline()
1453 for glyphName in sorted(glyphs):
1454 xmlWriter.simpletag("Glyph", value=glyphName)
1455 xmlWriter.newline()
1456 xmlWriter.begintag("ClipBox", [("Format", clipBox.Format)])
1457 xmlWriter.newline()
1458 clipBox.toXML2(xmlWriter, font)
1459 xmlWriter.endtag("ClipBox")
1460 xmlWriter.newline()
1461 xmlWriter.endtag("Clip")
1462 xmlWriter.newline()
1463 xmlWriter.endtag(tableName)
1464 xmlWriter.newline()
1466 def fromXML(self, name, attrs, content, font):
1467 clips = getattr(self, "clips", None)
1468 if clips is None:
1469 self.clips = clips = {}
1470 assert name == "Clip"
1471 glyphs = []
1472 clipBox = None
1473 for elem in content:
1474 if not isinstance(elem, tuple):
1475 continue
1476 name, attrs, content = elem
1477 if name == "Glyph":
1478 glyphs.append(attrs["value"])
1479 elif name == "ClipBox":
1480 clipBox = ClipBox()
1481 clipBox.Format = safeEval(attrs["Format"])
1482 for elem in content:
1483 if not isinstance(elem, tuple):
1484 continue
1485 name, attrs, content = elem
1486 clipBox.fromXML(name, attrs, content, font)
1487 if clipBox:
1488 for glyphName in glyphs:
1489 clips[glyphName] = clipBox
1492class ExtendMode(IntEnum):
1493 PAD = 0
1494 REPEAT = 1
1495 REFLECT = 2
1498# Porter-Duff modes for COLRv1 PaintComposite:
1499# https://github.com/googlefonts/colr-gradients-spec/tree/off_sub_1#compositemode-enumeration
1500class CompositeMode(IntEnum):
1501 CLEAR = 0
1502 SRC = 1
1503 DEST = 2
1504 SRC_OVER = 3
1505 DEST_OVER = 4
1506 SRC_IN = 5
1507 DEST_IN = 6
1508 SRC_OUT = 7
1509 DEST_OUT = 8
1510 SRC_ATOP = 9
1511 DEST_ATOP = 10
1512 XOR = 11
1513 PLUS = 12
1514 SCREEN = 13
1515 OVERLAY = 14
1516 DARKEN = 15
1517 LIGHTEN = 16
1518 COLOR_DODGE = 17
1519 COLOR_BURN = 18
1520 HARD_LIGHT = 19
1521 SOFT_LIGHT = 20
1522 DIFFERENCE = 21
1523 EXCLUSION = 22
1524 MULTIPLY = 23
1525 HSL_HUE = 24
1526 HSL_SATURATION = 25
1527 HSL_COLOR = 26
1528 HSL_LUMINOSITY = 27
1531class PaintFormat(IntEnum):
1532 PaintColrLayers = 1
1533 PaintSolid = 2
1534 PaintVarSolid = 3
1535 PaintLinearGradient = 4
1536 PaintVarLinearGradient = 5
1537 PaintRadialGradient = 6
1538 PaintVarRadialGradient = 7
1539 PaintSweepGradient = 8
1540 PaintVarSweepGradient = 9
1541 PaintGlyph = 10
1542 PaintColrGlyph = 11
1543 PaintTransform = 12
1544 PaintVarTransform = 13
1545 PaintTranslate = 14
1546 PaintVarTranslate = 15
1547 PaintScale = 16
1548 PaintVarScale = 17
1549 PaintScaleAroundCenter = 18
1550 PaintVarScaleAroundCenter = 19
1551 PaintScaleUniform = 20
1552 PaintVarScaleUniform = 21
1553 PaintScaleUniformAroundCenter = 22
1554 PaintVarScaleUniformAroundCenter = 23
1555 PaintRotate = 24
1556 PaintVarRotate = 25
1557 PaintRotateAroundCenter = 26
1558 PaintVarRotateAroundCenter = 27
1559 PaintSkew = 28
1560 PaintVarSkew = 29
1561 PaintSkewAroundCenter = 30
1562 PaintVarSkewAroundCenter = 31
1563 PaintComposite = 32
1565 def is_variable(self):
1566 return self.name.startswith("PaintVar")
1568 def as_variable(self):
1569 if self.is_variable():
1570 return self
1571 try:
1572 return PaintFormat.__members__[f"PaintVar{self.name[5:]}"]
1573 except KeyError:
1574 return None
1577class Paint(getFormatSwitchingBaseTableClass("uint8")):
1578 formatEnum = PaintFormat
1580 def getFormatName(self):
1581 try:
1582 return self.formatEnum(self.Format).name
1583 except ValueError:
1584 raise NotImplementedError(f"Unknown Paint format: {self.Format}")
1586 def toXML(self, xmlWriter, font, attrs=None, name=None):
1587 tableName = name if name else self.__class__.__name__
1588 if attrs is None:
1589 attrs = []
1590 attrs.append(("Format", self.Format))
1591 xmlWriter.begintag(tableName, attrs)
1592 xmlWriter.comment(self.getFormatName())
1593 xmlWriter.newline()
1594 self.toXML2(xmlWriter, font)
1595 xmlWriter.endtag(tableName)
1596 xmlWriter.newline()
1598 def iterPaintSubTables(self, colr: COLR) -> Iterator[BaseTable.SubTableEntry]:
1599 if self.Format == PaintFormat.PaintColrLayers:
1600 # https://github.com/fonttools/fonttools/issues/2438: don't die when no LayerList exists
1601 layers = []
1602 if colr.LayerList is not None:
1603 layers = colr.LayerList.Paint
1604 yield from (
1605 BaseTable.SubTableEntry(name="Layers", value=v, index=i)
1606 for i, v in enumerate(
1607 layers[self.FirstLayerIndex : self.FirstLayerIndex + self.NumLayers]
1608 )
1609 )
1610 return
1612 if self.Format == PaintFormat.PaintColrGlyph:
1613 for record in colr.BaseGlyphList.BaseGlyphPaintRecord:
1614 if record.BaseGlyph == self.Glyph:
1615 yield BaseTable.SubTableEntry(name="BaseGlyph", value=record.Paint)
1616 return
1617 else:
1618 raise KeyError(f"{self.Glyph!r} not in colr.BaseGlyphList")
1620 for conv in self.getConverters():
1621 if conv.tableClass is not None and issubclass(conv.tableClass, type(self)):
1622 value = getattr(self, conv.name)
1623 yield BaseTable.SubTableEntry(name=conv.name, value=value)
1625 def getChildren(self, colr) -> List["Paint"]:
1626 # this is kept for backward compatibility (e.g. it's used by the subsetter)
1627 return [p.value for p in self.iterPaintSubTables(colr)]
1629 def traverse(self, colr: COLR, callback):
1630 """Depth-first traversal of graph rooted at self, callback on each node."""
1631 if not callable(callback):
1632 raise TypeError("callback must be callable")
1634 for path in dfs_base_table(
1635 self, iter_subtables_fn=lambda paint: paint.iterPaintSubTables(colr)
1636 ):
1637 paint = path[-1].value
1638 callback(paint)
1640 def getTransform(self) -> Transform:
1641 if self.Format == PaintFormat.PaintTransform:
1642 t = self.Transform
1643 return Transform(t.xx, t.yx, t.xy, t.yy, t.dx, t.dy)
1644 elif self.Format == PaintFormat.PaintTranslate:
1645 return Identity.translate(self.dx, self.dy)
1646 elif self.Format == PaintFormat.PaintScale:
1647 return Identity.scale(self.scaleX, self.scaleY)
1648 elif self.Format == PaintFormat.PaintScaleAroundCenter:
1649 return (
1650 Identity.translate(self.centerX, self.centerY)
1651 .scale(self.scaleX, self.scaleY)
1652 .translate(-self.centerX, -self.centerY)
1653 )
1654 elif self.Format == PaintFormat.PaintScaleUniform:
1655 return Identity.scale(self.scale)
1656 elif self.Format == PaintFormat.PaintScaleUniformAroundCenter:
1657 return (
1658 Identity.translate(self.centerX, self.centerY)
1659 .scale(self.scale)
1660 .translate(-self.centerX, -self.centerY)
1661 )
1662 elif self.Format == PaintFormat.PaintRotate:
1663 return Identity.rotate(radians(self.angle))
1664 elif self.Format == PaintFormat.PaintRotateAroundCenter:
1665 return (
1666 Identity.translate(self.centerX, self.centerY)
1667 .rotate(radians(self.angle))
1668 .translate(-self.centerX, -self.centerY)
1669 )
1670 elif self.Format == PaintFormat.PaintSkew:
1671 return Identity.skew(radians(-self.xSkewAngle), radians(self.ySkewAngle))
1672 elif self.Format == PaintFormat.PaintSkewAroundCenter:
1673 return (
1674 Identity.translate(self.centerX, self.centerY)
1675 .skew(radians(-self.xSkewAngle), radians(self.ySkewAngle))
1676 .translate(-self.centerX, -self.centerY)
1677 )
1678 if PaintFormat(self.Format).is_variable():
1679 raise NotImplementedError(f"Variable Paints not supported: {self.Format}")
1681 return Identity
1683 def computeClipBox(
1684 self, colr: COLR, glyphSet: "_TTGlyphSet", quantization: int = 1
1685 ) -> Optional[ClipBox]:
1686 pen = ControlBoundsPen(glyphSet)
1687 for path in dfs_base_table(
1688 self, iter_subtables_fn=lambda paint: paint.iterPaintSubTables(colr)
1689 ):
1690 paint = path[-1].value
1691 if paint.Format == PaintFormat.PaintGlyph:
1692 transformation = reduce(
1693 Transform.transform,
1694 (st.value.getTransform() for st in path),
1695 Identity,
1696 )
1697 glyphSet[paint.Glyph].draw(TransformPen(pen, transformation))
1699 if pen.bounds is None:
1700 return None
1702 cb = ClipBox()
1703 cb.Format = int(ClipBoxFormat.Static)
1704 cb.xMin, cb.yMin, cb.xMax, cb.yMax = quantizeRect(pen.bounds, quantization)
1705 return cb
1708# For each subtable format there is a class. However, we don't really distinguish
1709# between "field name" and "format name": often these are the same. Yet there's
1710# a whole bunch of fields with different names. The following dict is a mapping
1711# from "format name" to "field name". _buildClasses() uses this to create a
1712# subclass for each alternate field name.
1713#
1714_equivalents = {
1715 "MarkArray": ("Mark1Array",),
1716 "LangSys": ("DefaultLangSys",),
1717 "Coverage": (
1718 "MarkCoverage",
1719 "BaseCoverage",
1720 "LigatureCoverage",
1721 "Mark1Coverage",
1722 "Mark2Coverage",
1723 "BacktrackCoverage",
1724 "InputCoverage",
1725 "LookAheadCoverage",
1726 "VertGlyphCoverage",
1727 "HorizGlyphCoverage",
1728 "TopAccentCoverage",
1729 "ExtendedShapeCoverage",
1730 "MathKernCoverage",
1731 ),
1732 "ClassDef": (
1733 "ClassDef1",
1734 "ClassDef2",
1735 "BacktrackClassDef",
1736 "InputClassDef",
1737 "LookAheadClassDef",
1738 "GlyphClassDef",
1739 "MarkAttachClassDef",
1740 ),
1741 "Anchor": (
1742 "EntryAnchor",
1743 "ExitAnchor",
1744 "BaseAnchor",
1745 "LigatureAnchor",
1746 "Mark2Anchor",
1747 "MarkAnchor",
1748 ),
1749 "Device": (
1750 "XPlaDevice",
1751 "YPlaDevice",
1752 "XAdvDevice",
1753 "YAdvDevice",
1754 "XDeviceTable",
1755 "YDeviceTable",
1756 "DeviceTable",
1757 ),
1758 "Axis": (
1759 "HorizAxis",
1760 "VertAxis",
1761 ),
1762 "MinMax": ("DefaultMinMax",),
1763 "BaseCoord": (
1764 "MinCoord",
1765 "MaxCoord",
1766 ),
1767 "JstfLangSys": ("DefJstfLangSys",),
1768 "JstfGSUBModList": (
1769 "ShrinkageEnableGSUB",
1770 "ShrinkageDisableGSUB",
1771 "ExtensionEnableGSUB",
1772 "ExtensionDisableGSUB",
1773 ),
1774 "JstfGPOSModList": (
1775 "ShrinkageEnableGPOS",
1776 "ShrinkageDisableGPOS",
1777 "ExtensionEnableGPOS",
1778 "ExtensionDisableGPOS",
1779 ),
1780 "JstfMax": (
1781 "ShrinkageJstfMax",
1782 "ExtensionJstfMax",
1783 ),
1784 "MathKern": (
1785 "TopRightMathKern",
1786 "TopLeftMathKern",
1787 "BottomRightMathKern",
1788 "BottomLeftMathKern",
1789 ),
1790 "MathGlyphConstruction": ("VertGlyphConstruction", "HorizGlyphConstruction"),
1791}
1793#
1794# OverFlow logic, to automatically create ExtensionLookups
1795# XXX This should probably move to otBase.py
1796#
1799def fixLookupOverFlows(ttf, overflowRecord):
1800 """Either the offset from the LookupList to a lookup overflowed, or
1801 an offset from a lookup to a subtable overflowed.
1802 The table layout is:
1803 GPSO/GUSB
1804 Script List
1805 Feature List
1806 LookUpList
1807 Lookup[0] and contents
1808 SubTable offset list
1809 SubTable[0] and contents
1810 ...
1811 SubTable[n] and contents
1812 ...
1813 Lookup[n] and contents
1814 SubTable offset list
1815 SubTable[0] and contents
1816 ...
1817 SubTable[n] and contents
1818 If the offset to a lookup overflowed (SubTableIndex is None)
1819 we must promote the *previous* lookup to an Extension type.
1820 If the offset from a lookup to subtable overflowed, then we must promote it
1821 to an Extension Lookup type.
1822 """
1823 ok = 0
1824 lookupIndex = overflowRecord.LookupListIndex
1825 if overflowRecord.SubTableIndex is None:
1826 lookupIndex = lookupIndex - 1
1827 if lookupIndex < 0:
1828 return ok
1829 if overflowRecord.tableType == "GSUB":
1830 extType = 7
1831 elif overflowRecord.tableType == "GPOS":
1832 extType = 9
1834 lookups = ttf[overflowRecord.tableType].table.LookupList.Lookup
1835 lookup = lookups[lookupIndex]
1836 # If the previous lookup is an extType, look further back. Very unlikely, but possible.
1837 while lookup.SubTable[0].__class__.LookupType == extType:
1838 lookupIndex = lookupIndex - 1
1839 if lookupIndex < 0:
1840 return ok
1841 lookup = lookups[lookupIndex]
1843 for lookupIndex in range(lookupIndex, len(lookups)):
1844 lookup = lookups[lookupIndex]
1845 if lookup.LookupType != extType:
1846 lookup.LookupType = extType
1847 for si in range(len(lookup.SubTable)):
1848 subTable = lookup.SubTable[si]
1849 extSubTableClass = lookupTypes[overflowRecord.tableType][extType]
1850 extSubTable = extSubTableClass()
1851 extSubTable.Format = 1
1852 extSubTable.ExtSubTable = subTable
1853 lookup.SubTable[si] = extSubTable
1854 ok = 1
1855 return ok
1858def splitMultipleSubst(oldSubTable, newSubTable, overflowRecord):
1859 ok = 1
1860 oldMapping = sorted(oldSubTable.mapping.items())
1861 oldLen = len(oldMapping)
1863 if overflowRecord.itemName in ["Coverage", "RangeRecord"]:
1864 # Coverage table is written last. Overflow is to or within the
1865 # the coverage table. We will just cut the subtable in half.
1866 newLen = oldLen // 2
1868 elif overflowRecord.itemName == "Sequence":
1869 # We just need to back up by two items from the overflowed
1870 # Sequence index to make sure the offset to the Coverage table
1871 # doesn't overflow.
1872 newLen = overflowRecord.itemIndex - 1
1874 newSubTable.mapping = {}
1875 for i in range(newLen, oldLen):
1876 item = oldMapping[i]
1877 key = item[0]
1878 newSubTable.mapping[key] = item[1]
1879 del oldSubTable.mapping[key]
1881 return ok
1884def splitAlternateSubst(oldSubTable, newSubTable, overflowRecord):
1885 ok = 1
1886 if hasattr(oldSubTable, "sortCoverageLast"):
1887 newSubTable.sortCoverageLast = oldSubTable.sortCoverageLast
1889 oldAlts = sorted(oldSubTable.alternates.items())
1890 oldLen = len(oldAlts)
1892 if overflowRecord.itemName in ["Coverage", "RangeRecord"]:
1893 # Coverage table is written last. overflow is to or within the
1894 # the coverage table. We will just cut the subtable in half.
1895 newLen = oldLen // 2
1897 elif overflowRecord.itemName == "AlternateSet":
1898 # We just need to back up by two items
1899 # from the overflowed AlternateSet index to make sure the offset
1900 # to the Coverage table doesn't overflow.
1901 newLen = overflowRecord.itemIndex - 1
1903 newSubTable.alternates = {}
1904 for i in range(newLen, oldLen):
1905 item = oldAlts[i]
1906 key = item[0]
1907 newSubTable.alternates[key] = item[1]
1908 del oldSubTable.alternates[key]
1910 return ok
1913def splitLigatureSubst(oldSubTable, newSubTable, overflowRecord):
1914 ok = 1
1915 oldLigs = sorted(oldSubTable.ligatures.items())
1916 oldLen = len(oldLigs)
1918 if overflowRecord.itemName in ["Coverage", "RangeRecord"]:
1919 # Coverage table is written last. overflow is to or within the
1920 # the coverage table. We will just cut the subtable in half.
1921 newLen = oldLen // 2
1923 elif overflowRecord.itemName == "LigatureSet":
1924 # We just need to back up by two items
1925 # from the overflowed AlternateSet index to make sure the offset
1926 # to the Coverage table doesn't overflow.
1927 newLen = overflowRecord.itemIndex - 1
1929 newSubTable.ligatures = {}
1930 for i in range(newLen, oldLen):
1931 item = oldLigs[i]
1932 key = item[0]
1933 newSubTable.ligatures[key] = item[1]
1934 del oldSubTable.ligatures[key]
1936 return ok
1939def splitPairPos(oldSubTable, newSubTable, overflowRecord):
1940 st = oldSubTable
1941 ok = False
1942 newSubTable.Format = oldSubTable.Format
1943 if oldSubTable.Format == 1 and len(oldSubTable.PairSet) > 1:
1944 for name in "ValueFormat1", "ValueFormat2":
1945 setattr(newSubTable, name, getattr(oldSubTable, name))
1947 # Move top half of coverage to new subtable
1949 newSubTable.Coverage = oldSubTable.Coverage.__class__()
1951 coverage = oldSubTable.Coverage.glyphs
1952 records = oldSubTable.PairSet
1954 oldCount = len(oldSubTable.PairSet) // 2
1956 oldSubTable.Coverage.glyphs = coverage[:oldCount]
1957 oldSubTable.PairSet = records[:oldCount]
1959 newSubTable.Coverage.glyphs = coverage[oldCount:]
1960 newSubTable.PairSet = records[oldCount:]
1962 oldSubTable.PairSetCount = len(oldSubTable.PairSet)
1963 newSubTable.PairSetCount = len(newSubTable.PairSet)
1965 ok = True
1967 elif oldSubTable.Format == 2 and len(oldSubTable.Class1Record) > 1:
1968 if not hasattr(oldSubTable, "Class2Count"):
1969 oldSubTable.Class2Count = len(oldSubTable.Class1Record[0].Class2Record)
1970 for name in "Class2Count", "ClassDef2", "ValueFormat1", "ValueFormat2":
1971 setattr(newSubTable, name, getattr(oldSubTable, name))
1973 # The two subtables will still have the same ClassDef2 and the table
1974 # sharing will still cause the sharing to overflow. As such, disable
1975 # sharing on the one that is serialized second (that's oldSubTable).
1976 oldSubTable.DontShare = True
1978 # Move top half of class numbers to new subtable
1980 newSubTable.Coverage = oldSubTable.Coverage.__class__()
1981 newSubTable.ClassDef1 = oldSubTable.ClassDef1.__class__()
1983 coverage = oldSubTable.Coverage.glyphs
1984 classDefs = oldSubTable.ClassDef1.classDefs
1985 records = oldSubTable.Class1Record
1987 oldCount = len(oldSubTable.Class1Record) // 2
1988 newGlyphs = set(k for k, v in classDefs.items() if v >= oldCount)
1990 oldSubTable.Coverage.glyphs = [g for g in coverage if g not in newGlyphs]
1991 oldSubTable.ClassDef1.classDefs = {
1992 k: v for k, v in classDefs.items() if v < oldCount
1993 }
1994 oldSubTable.Class1Record = records[:oldCount]
1996 newSubTable.Coverage.glyphs = [g for g in coverage if g in newGlyphs]
1997 newSubTable.ClassDef1.classDefs = {
1998 k: (v - oldCount) for k, v in classDefs.items() if v > oldCount
1999 }
2000 newSubTable.Class1Record = records[oldCount:]
2002 oldSubTable.Class1Count = len(oldSubTable.Class1Record)
2003 newSubTable.Class1Count = len(newSubTable.Class1Record)
2005 ok = True
2007 return ok
2010def splitMarkBasePos(oldSubTable, newSubTable, overflowRecord):
2011 # split half of the mark classes to the new subtable
2012 classCount = oldSubTable.ClassCount
2013 if classCount < 2:
2014 # oh well, not much left to split...
2015 return False
2017 oldClassCount = classCount // 2
2018 newClassCount = classCount - oldClassCount
2020 oldMarkCoverage, oldMarkRecords = [], []
2021 newMarkCoverage, newMarkRecords = [], []
2022 for glyphName, markRecord in zip(
2023 oldSubTable.MarkCoverage.glyphs, oldSubTable.MarkArray.MarkRecord
2024 ):
2025 if markRecord.Class < oldClassCount:
2026 oldMarkCoverage.append(glyphName)
2027 oldMarkRecords.append(markRecord)
2028 else:
2029 markRecord.Class -= oldClassCount
2030 newMarkCoverage.append(glyphName)
2031 newMarkRecords.append(markRecord)
2033 oldBaseRecords, newBaseRecords = [], []
2034 for rec in oldSubTable.BaseArray.BaseRecord:
2035 oldBaseRecord, newBaseRecord = rec.__class__(), rec.__class__()
2036 oldBaseRecord.BaseAnchor = rec.BaseAnchor[:oldClassCount]
2037 newBaseRecord.BaseAnchor = rec.BaseAnchor[oldClassCount:]
2038 oldBaseRecords.append(oldBaseRecord)
2039 newBaseRecords.append(newBaseRecord)
2041 newSubTable.Format = oldSubTable.Format
2043 oldSubTable.MarkCoverage.glyphs = oldMarkCoverage
2044 newSubTable.MarkCoverage = oldSubTable.MarkCoverage.__class__()
2045 newSubTable.MarkCoverage.glyphs = newMarkCoverage
2047 # share the same BaseCoverage in both halves
2048 newSubTable.BaseCoverage = oldSubTable.BaseCoverage
2050 oldSubTable.ClassCount = oldClassCount
2051 newSubTable.ClassCount = newClassCount
2053 oldSubTable.MarkArray.MarkRecord = oldMarkRecords
2054 newSubTable.MarkArray = oldSubTable.MarkArray.__class__()
2055 newSubTable.MarkArray.MarkRecord = newMarkRecords
2057 oldSubTable.MarkArray.MarkCount = len(oldMarkRecords)
2058 newSubTable.MarkArray.MarkCount = len(newMarkRecords)
2060 oldSubTable.BaseArray.BaseRecord = oldBaseRecords
2061 newSubTable.BaseArray = oldSubTable.BaseArray.__class__()
2062 newSubTable.BaseArray.BaseRecord = newBaseRecords
2064 oldSubTable.BaseArray.BaseCount = len(oldBaseRecords)
2065 newSubTable.BaseArray.BaseCount = len(newBaseRecords)
2067 return True
2070splitTable = {
2071 "GSUB": {
2072 # 1: splitSingleSubst,
2073 2: splitMultipleSubst,
2074 3: splitAlternateSubst,
2075 4: splitLigatureSubst,
2076 # 5: splitContextSubst,
2077 # 6: splitChainContextSubst,
2078 # 7: splitExtensionSubst,
2079 # 8: splitReverseChainSingleSubst,
2080 },
2081 "GPOS": {
2082 # 1: splitSinglePos,
2083 2: splitPairPos,
2084 # 3: splitCursivePos,
2085 4: splitMarkBasePos,
2086 # 5: splitMarkLigPos,
2087 # 6: splitMarkMarkPos,
2088 # 7: splitContextPos,
2089 # 8: splitChainContextPos,
2090 # 9: splitExtensionPos,
2091 },
2092}
2095def fixSubTableOverFlows(ttf, overflowRecord):
2096 """
2097 An offset has overflowed within a sub-table. We need to divide this subtable into smaller parts.
2098 """
2099 table = ttf[overflowRecord.tableType].table
2100 lookup = table.LookupList.Lookup[overflowRecord.LookupListIndex]
2101 subIndex = overflowRecord.SubTableIndex
2102 subtable = lookup.SubTable[subIndex]
2104 # First, try not sharing anything for this subtable...
2105 if not hasattr(subtable, "DontShare"):
2106 subtable.DontShare = True
2107 return True
2109 if hasattr(subtable, "ExtSubTable"):
2110 # We split the subtable of the Extension table, and add a new Extension table
2111 # to contain the new subtable.
2113 subTableType = subtable.ExtSubTable.__class__.LookupType
2114 extSubTable = subtable
2115 subtable = extSubTable.ExtSubTable
2116 newExtSubTableClass = lookupTypes[overflowRecord.tableType][
2117 extSubTable.__class__.LookupType
2118 ]
2119 newExtSubTable = newExtSubTableClass()
2120 newExtSubTable.Format = extSubTable.Format
2121 toInsert = newExtSubTable
2123 newSubTableClass = lookupTypes[overflowRecord.tableType][subTableType]
2124 newSubTable = newSubTableClass()
2125 newExtSubTable.ExtSubTable = newSubTable
2126 else:
2127 subTableType = subtable.__class__.LookupType
2128 newSubTableClass = lookupTypes[overflowRecord.tableType][subTableType]
2129 newSubTable = newSubTableClass()
2130 toInsert = newSubTable
2132 if hasattr(lookup, "SubTableCount"): # may not be defined yet.
2133 lookup.SubTableCount = lookup.SubTableCount + 1
2135 try:
2136 splitFunc = splitTable[overflowRecord.tableType][subTableType]
2137 except KeyError:
2138 log.error(
2139 "Don't know how to split %s lookup type %s",
2140 overflowRecord.tableType,
2141 subTableType,
2142 )
2143 return False
2145 ok = splitFunc(subtable, newSubTable, overflowRecord)
2146 if ok:
2147 lookup.SubTable.insert(subIndex + 1, toInsert)
2148 return ok
2151# End of OverFlow logic
2154def _buildClasses():
2155 import re
2156 from .otData import otData
2158 formatPat = re.compile(r"([A-Za-z0-9]+)Format(\d+)$")
2159 namespace = globals()
2161 # populate module with classes
2162 for name, table in otData:
2163 baseClass = BaseTable
2164 m = formatPat.match(name)
2165 if m:
2166 # XxxFormatN subtable, we only add the "base" table
2167 name = m.group(1)
2168 # the first row of a format-switching otData table describes the Format;
2169 # the first column defines the type of the Format field.
2170 # Currently this can be either 'uint16' or 'uint8'.
2171 formatType = table[0][0]
2172 baseClass = getFormatSwitchingBaseTableClass(formatType)
2173 if name not in namespace:
2174 # the class doesn't exist yet, so the base implementation is used.
2175 cls = type(name, (baseClass,), {})
2176 if name in ("GSUB", "GPOS"):
2177 cls.DontShare = True
2178 namespace[name] = cls
2180 # link Var{Table} <-> {Table} (e.g. ColorStop <-> VarColorStop, etc.)
2181 for name, _ in otData:
2182 if name.startswith("Var") and len(name) > 3 and name[3:] in namespace:
2183 varType = namespace[name]
2184 noVarType = namespace[name[3:]]
2185 varType.NoVarType = noVarType
2186 noVarType.VarType = varType
2188 for base, alts in _equivalents.items():
2189 base = namespace[base]
2190 for alt in alts:
2191 namespace[alt] = base
2193 global lookupTypes
2194 lookupTypes = {
2195 "GSUB": {
2196 1: SingleSubst,
2197 2: MultipleSubst,
2198 3: AlternateSubst,
2199 4: LigatureSubst,
2200 5: ContextSubst,
2201 6: ChainContextSubst,
2202 7: ExtensionSubst,
2203 8: ReverseChainSingleSubst,
2204 },
2205 "GPOS": {
2206 1: SinglePos,
2207 2: PairPos,
2208 3: CursivePos,
2209 4: MarkBasePos,
2210 5: MarkLigPos,
2211 6: MarkMarkPos,
2212 7: ContextPos,
2213 8: ChainContextPos,
2214 9: ExtensionPos,
2215 },
2216 "mort": {
2217 4: NoncontextualMorph,
2218 },
2219 "morx": {
2220 0: RearrangementMorph,
2221 1: ContextualMorph,
2222 2: LigatureMorph,
2223 # 3: Reserved,
2224 4: NoncontextualMorph,
2225 5: InsertionMorph,
2226 },
2227 }
2228 lookupTypes["JSTF"] = lookupTypes["GPOS"] # JSTF contains GPOS
2229 for lookupEnum in lookupTypes.values():
2230 for enum, cls in lookupEnum.items():
2231 cls.LookupType = enum
2233 global featureParamTypes
2234 featureParamTypes = {
2235 "size": FeatureParamsSize,
2236 }
2237 for i in range(1, 20 + 1):
2238 featureParamTypes["ss%02d" % i] = FeatureParamsStylisticSet
2239 for i in range(1, 99 + 1):
2240 featureParamTypes["cv%02d" % i] = FeatureParamsCharacterVariants
2242 # add converters to classes
2243 from .otConverters import buildConverters
2245 for name, table in otData:
2246 m = formatPat.match(name)
2247 if m:
2248 # XxxFormatN subtable, add converter to "base" table
2249 name, format = m.groups()
2250 format = int(format)
2251 cls = namespace[name]
2252 if not hasattr(cls, "converters"):
2253 cls.converters = {}
2254 cls.convertersByName = {}
2255 converters, convertersByName = buildConverters(table[1:], namespace)
2256 cls.converters[format] = converters
2257 cls.convertersByName[format] = convertersByName
2258 # XXX Add staticSize?
2259 else:
2260 cls = namespace[name]
2261 cls.converters, cls.convertersByName = buildConverters(table, namespace)
2262 # XXX Add staticSize?
2265_buildClasses()
2268def _getGlyphsFromCoverageTable(coverage):
2269 if coverage is None:
2270 # empty coverage table
2271 return []
2272 else:
2273 return coverage.glyphs