Coverage for /usr/lib/python3/dist-packages/fontTools/otlLib/builder.py: 17%
1121 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
1from collections import namedtuple, OrderedDict
2import os
3from fontTools.misc.fixedTools import fixedToFloat
4from fontTools import ttLib
5from fontTools.ttLib.tables import otTables as ot
6from fontTools.ttLib.tables.otBase import (
7 ValueRecord,
8 valueRecordFormatDict,
9 OTTableWriter,
10 CountReference,
11)
12from fontTools.ttLib.tables import otBase
13from fontTools.feaLib.ast import STATNameStatement
14from fontTools.otlLib.optimize.gpos import (
15 _compression_level_from_env,
16 compact_lookup,
17)
18from fontTools.otlLib.error import OpenTypeLibError
19from functools import reduce
20import logging
21import copy
24log = logging.getLogger(__name__)
27def buildCoverage(glyphs, glyphMap):
28 """Builds a coverage table.
30 Coverage tables (as defined in the `OpenType spec <https://docs.microsoft.com/en-gb/typography/opentype/spec/chapter2#coverage-table>`__)
31 are used in all OpenType Layout lookups apart from the Extension type, and
32 define the glyphs involved in a layout subtable. This allows shaping engines
33 to compare the glyph stream with the coverage table and quickly determine
34 whether a subtable should be involved in a shaping operation.
36 This function takes a list of glyphs and a glyphname-to-ID map, and
37 returns a ``Coverage`` object representing the coverage table.
39 Example::
41 glyphMap = font.getReverseGlyphMap()
42 glyphs = [ "A", "B", "C" ]
43 coverage = buildCoverage(glyphs, glyphMap)
45 Args:
46 glyphs: a sequence of glyph names.
47 glyphMap: a glyph name to ID map, typically returned from
48 ``font.getReverseGlyphMap()``.
50 Returns:
51 An ``otTables.Coverage`` object or ``None`` if there are no glyphs
52 supplied.
53 """
55 if not glyphs:
56 return None
57 self = ot.Coverage()
58 try:
59 self.glyphs = sorted(set(glyphs), key=glyphMap.__getitem__)
60 except KeyError as e:
61 raise ValueError(f"Could not find glyph {e} in font") from e
63 return self
66LOOKUP_FLAG_RIGHT_TO_LEFT = 0x0001
67LOOKUP_FLAG_IGNORE_BASE_GLYPHS = 0x0002
68LOOKUP_FLAG_IGNORE_LIGATURES = 0x0004
69LOOKUP_FLAG_IGNORE_MARKS = 0x0008
70LOOKUP_FLAG_USE_MARK_FILTERING_SET = 0x0010
73def buildLookup(subtables, flags=0, markFilterSet=None):
74 """Turns a collection of rules into a lookup.
76 A Lookup (as defined in the `OpenType Spec <https://docs.microsoft.com/en-gb/typography/opentype/spec/chapter2#lookupTbl>`__)
77 wraps the individual rules in a layout operation (substitution or
78 positioning) in a data structure expressing their overall lookup type -
79 for example, single substitution, mark-to-base attachment, and so on -
80 as well as the lookup flags and any mark filtering sets. You may import
81 the following constants to express lookup flags:
83 - ``LOOKUP_FLAG_RIGHT_TO_LEFT``
84 - ``LOOKUP_FLAG_IGNORE_BASE_GLYPHS``
85 - ``LOOKUP_FLAG_IGNORE_LIGATURES``
86 - ``LOOKUP_FLAG_IGNORE_MARKS``
87 - ``LOOKUP_FLAG_USE_MARK_FILTERING_SET``
89 Args:
90 subtables: A list of layout subtable objects (e.g.
91 ``MultipleSubst``, ``PairPos``, etc.) or ``None``.
92 flags (int): This lookup's flags.
93 markFilterSet: Either ``None`` if no mark filtering set is used, or
94 an integer representing the filtering set to be used for this
95 lookup. If a mark filtering set is provided,
96 `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
97 flags.
99 Returns:
100 An ``otTables.Lookup`` object or ``None`` if there are no subtables
101 supplied.
102 """
103 if subtables is None:
104 return None
105 subtables = [st for st in subtables if st is not None]
106 if not subtables:
107 return None
108 assert all(
109 t.LookupType == subtables[0].LookupType for t in subtables
110 ), "all subtables must have the same LookupType; got %s" % repr(
111 [t.LookupType for t in subtables]
112 )
113 self = ot.Lookup()
114 self.LookupType = subtables[0].LookupType
115 self.LookupFlag = flags
116 self.SubTable = subtables
117 self.SubTableCount = len(self.SubTable)
118 if markFilterSet is not None:
119 self.LookupFlag |= LOOKUP_FLAG_USE_MARK_FILTERING_SET
120 assert isinstance(markFilterSet, int), markFilterSet
121 self.MarkFilteringSet = markFilterSet
122 else:
123 assert (self.LookupFlag & LOOKUP_FLAG_USE_MARK_FILTERING_SET) == 0, (
124 "if markFilterSet is None, flags must not set "
125 "LOOKUP_FLAG_USE_MARK_FILTERING_SET; flags=0x%04x" % flags
126 )
127 return self
130class LookupBuilder(object):
131 SUBTABLE_BREAK_ = "SUBTABLE_BREAK"
133 def __init__(self, font, location, table, lookup_type):
134 self.font = font
135 self.glyphMap = font.getReverseGlyphMap()
136 self.location = location
137 self.table, self.lookup_type = table, lookup_type
138 self.lookupflag = 0
139 self.markFilterSet = None
140 self.lookup_index = None # assigned when making final tables
141 assert table in ("GPOS", "GSUB")
143 def equals(self, other):
144 return (
145 isinstance(other, self.__class__)
146 and self.table == other.table
147 and self.lookupflag == other.lookupflag
148 and self.markFilterSet == other.markFilterSet
149 )
151 def inferGlyphClasses(self):
152 """Infers glyph glasses for the GDEF table, such as {"cedilla":3}."""
153 return {}
155 def getAlternateGlyphs(self):
156 """Helper for building 'aalt' features."""
157 return {}
159 def buildLookup_(self, subtables):
160 return buildLookup(subtables, self.lookupflag, self.markFilterSet)
162 def buildMarkClasses_(self, marks):
163 """{"cedilla": ("BOTTOM", ast.Anchor), ...} --> {"BOTTOM":0, "TOP":1}
165 Helper for MarkBasePostBuilder, MarkLigPosBuilder, and
166 MarkMarkPosBuilder. Seems to return the same numeric IDs
167 for mark classes as the AFDKO makeotf tool.
168 """
169 ids = {}
170 for mark in sorted(marks.keys(), key=self.font.getGlyphID):
171 markClassName, _markAnchor = marks[mark]
172 if markClassName not in ids:
173 ids[markClassName] = len(ids)
174 return ids
176 def setBacktrackCoverage_(self, prefix, subtable):
177 subtable.BacktrackGlyphCount = len(prefix)
178 subtable.BacktrackCoverage = []
179 for p in reversed(prefix):
180 coverage = buildCoverage(p, self.glyphMap)
181 subtable.BacktrackCoverage.append(coverage)
183 def setLookAheadCoverage_(self, suffix, subtable):
184 subtable.LookAheadGlyphCount = len(suffix)
185 subtable.LookAheadCoverage = []
186 for s in suffix:
187 coverage = buildCoverage(s, self.glyphMap)
188 subtable.LookAheadCoverage.append(coverage)
190 def setInputCoverage_(self, glyphs, subtable):
191 subtable.InputGlyphCount = len(glyphs)
192 subtable.InputCoverage = []
193 for g in glyphs:
194 coverage = buildCoverage(g, self.glyphMap)
195 subtable.InputCoverage.append(coverage)
197 def setCoverage_(self, glyphs, subtable):
198 subtable.GlyphCount = len(glyphs)
199 subtable.Coverage = []
200 for g in glyphs:
201 coverage = buildCoverage(g, self.glyphMap)
202 subtable.Coverage.append(coverage)
204 def build_subst_subtables(self, mapping, klass):
205 substitutions = [{}]
206 for key in mapping:
207 if key[0] == self.SUBTABLE_BREAK_:
208 substitutions.append({})
209 else:
210 substitutions[-1][key] = mapping[key]
211 subtables = [klass(s) for s in substitutions]
212 return subtables
214 def add_subtable_break(self, location):
215 """Add an explicit subtable break.
217 Args:
218 location: A string or tuple representing the location in the
219 original source which produced this break, or ``None`` if
220 no location is provided.
221 """
222 log.warning(
223 OpenTypeLibError(
224 'unsupported "subtable" statement for lookup type', location
225 )
226 )
229class AlternateSubstBuilder(LookupBuilder):
230 """Builds an Alternate Substitution (GSUB3) lookup.
232 Users are expected to manually add alternate glyph substitutions to
233 the ``alternates`` attribute after the object has been initialized,
234 e.g.::
236 builder.alternates["A"] = ["A.alt1", "A.alt2"]
238 Attributes:
239 font (``fontTools.TTLib.TTFont``): A font object.
240 location: A string or tuple representing the location in the original
241 source which produced this lookup.
242 alternates: An ordered dictionary of alternates, mapping glyph names
243 to a list of names of alternates.
244 lookupflag (int): The lookup's flag
245 markFilterSet: Either ``None`` if no mark filtering set is used, or
246 an integer representing the filtering set to be used for this
247 lookup. If a mark filtering set is provided,
248 `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
249 flags.
250 """
252 def __init__(self, font, location):
253 LookupBuilder.__init__(self, font, location, "GSUB", 3)
254 self.alternates = OrderedDict()
256 def equals(self, other):
257 return LookupBuilder.equals(self, other) and self.alternates == other.alternates
259 def build(self):
260 """Build the lookup.
262 Returns:
263 An ``otTables.Lookup`` object representing the alternate
264 substitution lookup.
265 """
266 subtables = self.build_subst_subtables(
267 self.alternates, buildAlternateSubstSubtable
268 )
269 return self.buildLookup_(subtables)
271 def getAlternateGlyphs(self):
272 return self.alternates
274 def add_subtable_break(self, location):
275 self.alternates[(self.SUBTABLE_BREAK_, location)] = self.SUBTABLE_BREAK_
278class ChainContextualRule(
279 namedtuple("ChainContextualRule", ["prefix", "glyphs", "suffix", "lookups"])
280):
281 @property
282 def is_subtable_break(self):
283 return self.prefix == LookupBuilder.SUBTABLE_BREAK_
286class ChainContextualRuleset:
287 def __init__(self):
288 self.rules = []
290 def addRule(self, rule):
291 self.rules.append(rule)
293 @property
294 def hasPrefixOrSuffix(self):
295 # Do we have any prefixes/suffixes? If this is False for all
296 # rulesets, we can express the whole lookup as GPOS5/GSUB7.
297 for rule in self.rules:
298 if len(rule.prefix) > 0 or len(rule.suffix) > 0:
299 return True
300 return False
302 @property
303 def hasAnyGlyphClasses(self):
304 # Do we use glyph classes anywhere in the rules? If this is False
305 # we can express this subtable as a Format 1.
306 for rule in self.rules:
307 for coverage in (rule.prefix, rule.glyphs, rule.suffix):
308 if any(len(x) > 1 for x in coverage):
309 return True
310 return False
312 def format2ClassDefs(self):
313 PREFIX, GLYPHS, SUFFIX = 0, 1, 2
314 classDefBuilders = []
315 for ix in [PREFIX, GLYPHS, SUFFIX]:
316 context = []
317 for r in self.rules:
318 context.append(r[ix])
319 classes = self._classBuilderForContext(context)
320 if not classes:
321 return None
322 classDefBuilders.append(classes)
323 return classDefBuilders
325 def _classBuilderForContext(self, context):
326 classdefbuilder = ClassDefBuilder(useClass0=False)
327 for position in context:
328 for glyphset in position:
329 glyphs = set(glyphset)
330 if not classdefbuilder.canAdd(glyphs):
331 return None
332 classdefbuilder.add(glyphs)
333 return classdefbuilder
336class ChainContextualBuilder(LookupBuilder):
337 def equals(self, other):
338 return LookupBuilder.equals(self, other) and self.rules == other.rules
340 def rulesets(self):
341 # Return a list of ChainContextRuleset objects, taking explicit
342 # subtable breaks into account
343 ruleset = [ChainContextualRuleset()]
344 for rule in self.rules:
345 if rule.is_subtable_break:
346 ruleset.append(ChainContextualRuleset())
347 continue
348 ruleset[-1].addRule(rule)
349 # Squish any empty subtables
350 return [x for x in ruleset if len(x.rules) > 0]
352 def getCompiledSize_(self, subtables):
353 size = 0
354 for st in subtables:
355 w = OTTableWriter()
356 w["LookupType"] = CountReference(
357 {"LookupType": st.LookupType}, "LookupType"
358 )
359 # We need to make a copy here because compiling
360 # modifies the subtable (finalizing formats etc.)
361 copy.deepcopy(st).compile(w, self.font)
362 size += len(w.getAllData())
363 return size
365 def build(self):
366 """Build the lookup.
368 Returns:
369 An ``otTables.Lookup`` object representing the chained
370 contextual positioning lookup.
371 """
372 subtables = []
374 rulesets = self.rulesets()
375 chaining = any(ruleset.hasPrefixOrSuffix for ruleset in rulesets)
377 # https://github.com/fonttools/fonttools/issues/2539
378 #
379 # Unfortunately, as of 2022-03-07, Apple's CoreText renderer does not
380 # correctly process GPOS7 lookups, so for now we force contextual
381 # positioning lookups to be chaining (GPOS8).
382 #
383 # This seems to be fixed as of macOS 13.2, but we keep disabling this
384 # for now until we are no longer concerned about old macOS versions.
385 # But we allow people to opt-out of this with the config key below.
386 write_gpos7 = self.font.cfg.get("fontTools.otlLib.builder:WRITE_GPOS7")
387 # horrible separation of concerns breach
388 if not write_gpos7 and self.subtable_type == "Pos":
389 chaining = True
391 for ruleset in rulesets:
392 # Determine format strategy. We try to build formats 1, 2 and 3
393 # subtables and then work out which is best. candidates list holds
394 # the subtables in each format for this ruleset (including a dummy
395 # "format 0" to make the addressing match the format numbers).
397 # We can always build a format 3 lookup by accumulating each of
398 # the rules into a list, so start with that.
399 candidates = [None, None, None, []]
400 for rule in ruleset.rules:
401 candidates[3].append(self.buildFormat3Subtable(rule, chaining))
403 # Can we express the whole ruleset as a format 2 subtable?
404 classdefs = ruleset.format2ClassDefs()
405 if classdefs:
406 candidates[2] = [
407 self.buildFormat2Subtable(ruleset, classdefs, chaining)
408 ]
410 if not ruleset.hasAnyGlyphClasses:
411 candidates[1] = [self.buildFormat1Subtable(ruleset, chaining)]
413 for i in [1, 2, 3]:
414 if candidates[i]:
415 try:
416 self.getCompiledSize_(candidates[i])
417 except Exception as e:
418 log.warning(
419 "Contextual format %i at %s overflowed (%s)"
420 % (i, str(self.location), e)
421 )
422 candidates[i] = None
424 candidates = [x for x in candidates if x is not None]
425 if not candidates:
426 raise OpenTypeLibError("All candidates overflowed", self.location)
428 winner = min(candidates, key=self.getCompiledSize_)
429 subtables.extend(winner)
431 # If we are not chaining, lookup type will be automatically fixed by
432 # buildLookup_
433 return self.buildLookup_(subtables)
435 def buildFormat1Subtable(self, ruleset, chaining=True):
436 st = self.newSubtable_(chaining=chaining)
437 st.Format = 1
438 st.populateDefaults()
439 coverage = set()
440 rulesetsByFirstGlyph = {}
441 ruleAttr = self.ruleAttr_(format=1, chaining=chaining)
443 for rule in ruleset.rules:
444 ruleAsSubtable = self.newRule_(format=1, chaining=chaining)
446 if chaining:
447 ruleAsSubtable.BacktrackGlyphCount = len(rule.prefix)
448 ruleAsSubtable.LookAheadGlyphCount = len(rule.suffix)
449 ruleAsSubtable.Backtrack = [list(x)[0] for x in reversed(rule.prefix)]
450 ruleAsSubtable.LookAhead = [list(x)[0] for x in rule.suffix]
452 ruleAsSubtable.InputGlyphCount = len(rule.glyphs)
453 else:
454 ruleAsSubtable.GlyphCount = len(rule.glyphs)
456 ruleAsSubtable.Input = [list(x)[0] for x in rule.glyphs[1:]]
458 self.buildLookupList(rule, ruleAsSubtable)
460 firstGlyph = list(rule.glyphs[0])[0]
461 if firstGlyph not in rulesetsByFirstGlyph:
462 coverage.add(firstGlyph)
463 rulesetsByFirstGlyph[firstGlyph] = []
464 rulesetsByFirstGlyph[firstGlyph].append(ruleAsSubtable)
466 st.Coverage = buildCoverage(coverage, self.glyphMap)
467 ruleSets = []
468 for g in st.Coverage.glyphs:
469 ruleSet = self.newRuleSet_(format=1, chaining=chaining)
470 setattr(ruleSet, ruleAttr, rulesetsByFirstGlyph[g])
471 setattr(ruleSet, f"{ruleAttr}Count", len(rulesetsByFirstGlyph[g]))
472 ruleSets.append(ruleSet)
474 setattr(st, self.ruleSetAttr_(format=1, chaining=chaining), ruleSets)
475 setattr(
476 st, self.ruleSetAttr_(format=1, chaining=chaining) + "Count", len(ruleSets)
477 )
479 return st
481 def buildFormat2Subtable(self, ruleset, classdefs, chaining=True):
482 st = self.newSubtable_(chaining=chaining)
483 st.Format = 2
484 st.populateDefaults()
486 if chaining:
487 (
488 st.BacktrackClassDef,
489 st.InputClassDef,
490 st.LookAheadClassDef,
491 ) = [c.build() for c in classdefs]
492 else:
493 st.ClassDef = classdefs[1].build()
495 inClasses = classdefs[1].classes()
497 classSets = []
498 for _ in inClasses:
499 classSet = self.newRuleSet_(format=2, chaining=chaining)
500 classSets.append(classSet)
502 coverage = set()
503 classRuleAttr = self.ruleAttr_(format=2, chaining=chaining)
505 for rule in ruleset.rules:
506 ruleAsSubtable = self.newRule_(format=2, chaining=chaining)
507 if chaining:
508 ruleAsSubtable.BacktrackGlyphCount = len(rule.prefix)
509 ruleAsSubtable.LookAheadGlyphCount = len(rule.suffix)
510 # The glyphs in the rule may be list, tuple, odict_keys...
511 # Order is not important anyway because they are guaranteed
512 # to be members of the same class.
513 ruleAsSubtable.Backtrack = [
514 st.BacktrackClassDef.classDefs[list(x)[0]]
515 for x in reversed(rule.prefix)
516 ]
517 ruleAsSubtable.LookAhead = [
518 st.LookAheadClassDef.classDefs[list(x)[0]] for x in rule.suffix
519 ]
521 ruleAsSubtable.InputGlyphCount = len(rule.glyphs)
522 ruleAsSubtable.Input = [
523 st.InputClassDef.classDefs[list(x)[0]] for x in rule.glyphs[1:]
524 ]
525 setForThisRule = classSets[
526 st.InputClassDef.classDefs[list(rule.glyphs[0])[0]]
527 ]
528 else:
529 ruleAsSubtable.GlyphCount = len(rule.glyphs)
530 ruleAsSubtable.Class = [ # The spec calls this InputSequence
531 st.ClassDef.classDefs[list(x)[0]] for x in rule.glyphs[1:]
532 ]
533 setForThisRule = classSets[
534 st.ClassDef.classDefs[list(rule.glyphs[0])[0]]
535 ]
537 self.buildLookupList(rule, ruleAsSubtable)
538 coverage |= set(rule.glyphs[0])
540 getattr(setForThisRule, classRuleAttr).append(ruleAsSubtable)
541 setattr(
542 setForThisRule,
543 f"{classRuleAttr}Count",
544 getattr(setForThisRule, f"{classRuleAttr}Count") + 1,
545 )
546 setattr(st, self.ruleSetAttr_(format=2, chaining=chaining), classSets)
547 setattr(
548 st, self.ruleSetAttr_(format=2, chaining=chaining) + "Count", len(classSets)
549 )
550 st.Coverage = buildCoverage(coverage, self.glyphMap)
551 return st
553 def buildFormat3Subtable(self, rule, chaining=True):
554 st = self.newSubtable_(chaining=chaining)
555 st.Format = 3
556 if chaining:
557 self.setBacktrackCoverage_(rule.prefix, st)
558 self.setLookAheadCoverage_(rule.suffix, st)
559 self.setInputCoverage_(rule.glyphs, st)
560 else:
561 self.setCoverage_(rule.glyphs, st)
562 self.buildLookupList(rule, st)
563 return st
565 def buildLookupList(self, rule, st):
566 for sequenceIndex, lookupList in enumerate(rule.lookups):
567 if lookupList is not None:
568 if not isinstance(lookupList, list):
569 # Can happen with synthesised lookups
570 lookupList = [lookupList]
571 for l in lookupList:
572 if l.lookup_index is None:
573 if isinstance(self, ChainContextPosBuilder):
574 other = "substitution"
575 else:
576 other = "positioning"
577 raise OpenTypeLibError(
578 "Missing index of the specified "
579 f"lookup, might be a {other} lookup",
580 self.location,
581 )
582 rec = self.newLookupRecord_(st)
583 rec.SequenceIndex = sequenceIndex
584 rec.LookupListIndex = l.lookup_index
586 def add_subtable_break(self, location):
587 self.rules.append(
588 ChainContextualRule(
589 self.SUBTABLE_BREAK_,
590 self.SUBTABLE_BREAK_,
591 self.SUBTABLE_BREAK_,
592 [self.SUBTABLE_BREAK_],
593 )
594 )
596 def newSubtable_(self, chaining=True):
597 subtablename = f"Context{self.subtable_type}"
598 if chaining:
599 subtablename = "Chain" + subtablename
600 st = getattr(ot, subtablename)() # ot.ChainContextPos()/ot.ChainSubst()/etc.
601 setattr(st, f"{self.subtable_type}Count", 0)
602 setattr(st, f"{self.subtable_type}LookupRecord", [])
603 return st
605 # Format 1 and format 2 GSUB5/GSUB6/GPOS7/GPOS8 rulesets and rules form a family:
606 #
607 # format 1 ruleset format 1 rule format 2 ruleset format 2 rule
608 # GSUB5 SubRuleSet SubRule SubClassSet SubClassRule
609 # GSUB6 ChainSubRuleSet ChainSubRule ChainSubClassSet ChainSubClassRule
610 # GPOS7 PosRuleSet PosRule PosClassSet PosClassRule
611 # GPOS8 ChainPosRuleSet ChainPosRule ChainPosClassSet ChainPosClassRule
612 #
613 # The following functions generate the attribute names and subtables according
614 # to this naming convention.
615 def ruleSetAttr_(self, format=1, chaining=True):
616 if format == 1:
617 formatType = "Rule"
618 elif format == 2:
619 formatType = "Class"
620 else:
621 raise AssertionError(formatType)
622 subtablename = f"{self.subtable_type[0:3]}{formatType}Set" # Sub, not Subst.
623 if chaining:
624 subtablename = "Chain" + subtablename
625 return subtablename
627 def ruleAttr_(self, format=1, chaining=True):
628 if format == 1:
629 formatType = ""
630 elif format == 2:
631 formatType = "Class"
632 else:
633 raise AssertionError(formatType)
634 subtablename = f"{self.subtable_type[0:3]}{formatType}Rule" # Sub, not Subst.
635 if chaining:
636 subtablename = "Chain" + subtablename
637 return subtablename
639 def newRuleSet_(self, format=1, chaining=True):
640 st = getattr(
641 ot, self.ruleSetAttr_(format, chaining)
642 )() # ot.ChainPosRuleSet()/ot.SubRuleSet()/etc.
643 st.populateDefaults()
644 return st
646 def newRule_(self, format=1, chaining=True):
647 st = getattr(
648 ot, self.ruleAttr_(format, chaining)
649 )() # ot.ChainPosClassRule()/ot.SubClassRule()/etc.
650 st.populateDefaults()
651 return st
653 def attachSubtableWithCount_(
654 self, st, subtable_name, count_name, existing=None, index=None, chaining=False
655 ):
656 if chaining:
657 subtable_name = "Chain" + subtable_name
658 count_name = "Chain" + count_name
660 if not hasattr(st, count_name):
661 setattr(st, count_name, 0)
662 setattr(st, subtable_name, [])
664 if existing:
665 new_subtable = existing
666 else:
667 # Create a new, empty subtable from otTables
668 new_subtable = getattr(ot, subtable_name)()
670 setattr(st, count_name, getattr(st, count_name) + 1)
672 if index:
673 getattr(st, subtable_name).insert(index, new_subtable)
674 else:
675 getattr(st, subtable_name).append(new_subtable)
677 return new_subtable
679 def newLookupRecord_(self, st):
680 return self.attachSubtableWithCount_(
681 st,
682 f"{self.subtable_type}LookupRecord",
683 f"{self.subtable_type}Count",
684 chaining=False,
685 ) # Oddly, it isn't ChainSubstLookupRecord
688class ChainContextPosBuilder(ChainContextualBuilder):
689 """Builds a Chained Contextual Positioning (GPOS8) lookup.
691 Users are expected to manually add rules to the ``rules`` attribute after
692 the object has been initialized, e.g.::
694 # pos [A B] [C D] x' lookup lu1 y' z' lookup lu2 E;
696 prefix = [ ["A", "B"], ["C", "D"] ]
697 suffix = [ ["E"] ]
698 glyphs = [ ["x"], ["y"], ["z"] ]
699 lookups = [ [lu1], None, [lu2] ]
700 builder.rules.append( (prefix, glyphs, suffix, lookups) )
702 Attributes:
703 font (``fontTools.TTLib.TTFont``): A font object.
704 location: A string or tuple representing the location in the original
705 source which produced this lookup.
706 rules: A list of tuples representing the rules in this lookup.
707 lookupflag (int): The lookup's flag
708 markFilterSet: Either ``None`` if no mark filtering set is used, or
709 an integer representing the filtering set to be used for this
710 lookup. If a mark filtering set is provided,
711 `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
712 flags.
713 """
715 def __init__(self, font, location):
716 LookupBuilder.__init__(self, font, location, "GPOS", 8)
717 self.rules = []
718 self.subtable_type = "Pos"
720 def find_chainable_single_pos(self, lookups, glyphs, value):
721 """Helper for add_single_pos_chained_()"""
722 res = None
723 for lookup in lookups[::-1]:
724 if lookup == self.SUBTABLE_BREAK_:
725 return res
726 if isinstance(lookup, SinglePosBuilder) and all(
727 lookup.can_add(glyph, value) for glyph in glyphs
728 ):
729 res = lookup
730 return res
733class ChainContextSubstBuilder(ChainContextualBuilder):
734 """Builds a Chained Contextual Substitution (GSUB6) lookup.
736 Users are expected to manually add rules to the ``rules`` attribute after
737 the object has been initialized, e.g.::
739 # sub [A B] [C D] x' lookup lu1 y' z' lookup lu2 E;
741 prefix = [ ["A", "B"], ["C", "D"] ]
742 suffix = [ ["E"] ]
743 glyphs = [ ["x"], ["y"], ["z"] ]
744 lookups = [ [lu1], None, [lu2] ]
745 builder.rules.append( (prefix, glyphs, suffix, lookups) )
747 Attributes:
748 font (``fontTools.TTLib.TTFont``): A font object.
749 location: A string or tuple representing the location in the original
750 source which produced this lookup.
751 rules: A list of tuples representing the rules in this lookup.
752 lookupflag (int): The lookup's flag
753 markFilterSet: Either ``None`` if no mark filtering set is used, or
754 an integer representing the filtering set to be used for this
755 lookup. If a mark filtering set is provided,
756 `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
757 flags.
758 """
760 def __init__(self, font, location):
761 LookupBuilder.__init__(self, font, location, "GSUB", 6)
762 self.rules = [] # (prefix, input, suffix, lookups)
763 self.subtable_type = "Subst"
765 def getAlternateGlyphs(self):
766 result = {}
767 for rule in self.rules:
768 if rule.is_subtable_break:
769 continue
770 for lookups in rule.lookups:
771 if not isinstance(lookups, list):
772 lookups = [lookups]
773 for lookup in lookups:
774 if lookup is not None:
775 alts = lookup.getAlternateGlyphs()
776 for glyph, replacements in alts.items():
777 result.setdefault(glyph, set()).update(replacements)
778 return result
780 def find_chainable_single_subst(self, mapping):
781 """Helper for add_single_subst_chained_()"""
782 res = None
783 for rule in self.rules[::-1]:
784 if rule.is_subtable_break:
785 return res
786 for sub in rule.lookups:
787 if isinstance(sub, SingleSubstBuilder) and not any(
788 g in mapping and mapping[g] != sub.mapping[g] for g in sub.mapping
789 ):
790 res = sub
791 return res
794class LigatureSubstBuilder(LookupBuilder):
795 """Builds a Ligature Substitution (GSUB4) lookup.
797 Users are expected to manually add ligatures to the ``ligatures``
798 attribute after the object has been initialized, e.g.::
800 # sub f i by f_i;
801 builder.ligatures[("f","f","i")] = "f_f_i"
803 Attributes:
804 font (``fontTools.TTLib.TTFont``): A font object.
805 location: A string or tuple representing the location in the original
806 source which produced this lookup.
807 ligatures: An ordered dictionary mapping a tuple of glyph names to the
808 ligature glyphname.
809 lookupflag (int): The lookup's flag
810 markFilterSet: Either ``None`` if no mark filtering set is used, or
811 an integer representing the filtering set to be used for this
812 lookup. If a mark filtering set is provided,
813 `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
814 flags.
815 """
817 def __init__(self, font, location):
818 LookupBuilder.__init__(self, font, location, "GSUB", 4)
819 self.ligatures = OrderedDict() # {('f','f','i'): 'f_f_i'}
821 def equals(self, other):
822 return LookupBuilder.equals(self, other) and self.ligatures == other.ligatures
824 def build(self):
825 """Build the lookup.
827 Returns:
828 An ``otTables.Lookup`` object representing the ligature
829 substitution lookup.
830 """
831 subtables = self.build_subst_subtables(
832 self.ligatures, buildLigatureSubstSubtable
833 )
834 return self.buildLookup_(subtables)
836 def add_subtable_break(self, location):
837 self.ligatures[(self.SUBTABLE_BREAK_, location)] = self.SUBTABLE_BREAK_
840class MultipleSubstBuilder(LookupBuilder):
841 """Builds a Multiple Substitution (GSUB2) lookup.
843 Users are expected to manually add substitutions to the ``mapping``
844 attribute after the object has been initialized, e.g.::
846 # sub uni06C0 by uni06D5.fina hamza.above;
847 builder.mapping["uni06C0"] = [ "uni06D5.fina", "hamza.above"]
849 Attributes:
850 font (``fontTools.TTLib.TTFont``): A font object.
851 location: A string or tuple representing the location in the original
852 source which produced this lookup.
853 mapping: An ordered dictionary mapping a glyph name to a list of
854 substituted glyph names.
855 lookupflag (int): The lookup's flag
856 markFilterSet: Either ``None`` if no mark filtering set is used, or
857 an integer representing the filtering set to be used for this
858 lookup. If a mark filtering set is provided,
859 `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
860 flags.
861 """
863 def __init__(self, font, location):
864 LookupBuilder.__init__(self, font, location, "GSUB", 2)
865 self.mapping = OrderedDict()
867 def equals(self, other):
868 return LookupBuilder.equals(self, other) and self.mapping == other.mapping
870 def build(self):
871 subtables = self.build_subst_subtables(self.mapping, buildMultipleSubstSubtable)
872 return self.buildLookup_(subtables)
874 def add_subtable_break(self, location):
875 self.mapping[(self.SUBTABLE_BREAK_, location)] = self.SUBTABLE_BREAK_
878class CursivePosBuilder(LookupBuilder):
879 """Builds a Cursive Positioning (GPOS3) lookup.
881 Attributes:
882 font (``fontTools.TTLib.TTFont``): A font object.
883 location: A string or tuple representing the location in the original
884 source which produced this lookup.
885 attachments: An ordered dictionary mapping a glyph name to a two-element
886 tuple of ``otTables.Anchor`` objects.
887 lookupflag (int): The lookup's flag
888 markFilterSet: Either ``None`` if no mark filtering set is used, or
889 an integer representing the filtering set to be used for this
890 lookup. If a mark filtering set is provided,
891 `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
892 flags.
893 """
895 def __init__(self, font, location):
896 LookupBuilder.__init__(self, font, location, "GPOS", 3)
897 self.attachments = {}
899 def equals(self, other):
900 return (
901 LookupBuilder.equals(self, other) and self.attachments == other.attachments
902 )
904 def add_attachment(self, location, glyphs, entryAnchor, exitAnchor):
905 """Adds attachment information to the cursive positioning lookup.
907 Args:
908 location: A string or tuple representing the location in the
909 original source which produced this lookup. (Unused.)
910 glyphs: A list of glyph names sharing these entry and exit
911 anchor locations.
912 entryAnchor: A ``otTables.Anchor`` object representing the
913 entry anchor, or ``None`` if no entry anchor is present.
914 exitAnchor: A ``otTables.Anchor`` object representing the
915 exit anchor, or ``None`` if no exit anchor is present.
916 """
917 for glyph in glyphs:
918 self.attachments[glyph] = (entryAnchor, exitAnchor)
920 def build(self):
921 """Build the lookup.
923 Returns:
924 An ``otTables.Lookup`` object representing the cursive
925 positioning lookup.
926 """
927 st = buildCursivePosSubtable(self.attachments, self.glyphMap)
928 return self.buildLookup_([st])
931class MarkBasePosBuilder(LookupBuilder):
932 """Builds a Mark-To-Base Positioning (GPOS4) lookup.
934 Users are expected to manually add marks and bases to the ``marks``
935 and ``bases`` attributes after the object has been initialized, e.g.::
937 builder.marks["acute"] = (0, a1)
938 builder.marks["grave"] = (0, a1)
939 builder.marks["cedilla"] = (1, a2)
940 builder.bases["a"] = {0: a3, 1: a5}
941 builder.bases["b"] = {0: a4, 1: a5}
943 Attributes:
944 font (``fontTools.TTLib.TTFont``): A font object.
945 location: A string or tuple representing the location in the original
946 source which produced this lookup.
947 marks: An dictionary mapping a glyph name to a two-element
948 tuple containing a mark class ID and ``otTables.Anchor`` object.
949 bases: An dictionary mapping a glyph name to a dictionary of
950 mark class IDs and ``otTables.Anchor`` object.
951 lookupflag (int): The lookup's flag
952 markFilterSet: Either ``None`` if no mark filtering set is used, or
953 an integer representing the filtering set to be used for this
954 lookup. If a mark filtering set is provided,
955 `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
956 flags.
957 """
959 def __init__(self, font, location):
960 LookupBuilder.__init__(self, font, location, "GPOS", 4)
961 self.marks = {} # glyphName -> (markClassName, anchor)
962 self.bases = {} # glyphName -> {markClassName: anchor}
964 def equals(self, other):
965 return (
966 LookupBuilder.equals(self, other)
967 and self.marks == other.marks
968 and self.bases == other.bases
969 )
971 def inferGlyphClasses(self):
972 result = {glyph: 1 for glyph in self.bases}
973 result.update({glyph: 3 for glyph in self.marks})
974 return result
976 def build(self):
977 """Build the lookup.
979 Returns:
980 An ``otTables.Lookup`` object representing the mark-to-base
981 positioning lookup.
982 """
983 markClasses = self.buildMarkClasses_(self.marks)
984 marks = {}
985 for mark, (mc, anchor) in self.marks.items():
986 if mc not in markClasses:
987 raise ValueError(
988 "Mark class %s not found for mark glyph %s" % (mc, mark)
989 )
990 marks[mark] = (markClasses[mc], anchor)
991 bases = {}
992 for glyph, anchors in self.bases.items():
993 bases[glyph] = {}
994 for mc, anchor in anchors.items():
995 if mc not in markClasses:
996 raise ValueError(
997 "Mark class %s not found for base glyph %s" % (mc, glyph)
998 )
999 bases[glyph][markClasses[mc]] = anchor
1000 subtables = buildMarkBasePos(marks, bases, self.glyphMap)
1001 return self.buildLookup_(subtables)
1004class MarkLigPosBuilder(LookupBuilder):
1005 """Builds a Mark-To-Ligature Positioning (GPOS5) lookup.
1007 Users are expected to manually add marks and bases to the ``marks``
1008 and ``ligatures`` attributes after the object has been initialized, e.g.::
1010 builder.marks["acute"] = (0, a1)
1011 builder.marks["grave"] = (0, a1)
1012 builder.marks["cedilla"] = (1, a2)
1013 builder.ligatures["f_i"] = [
1014 { 0: a3, 1: a5 }, # f
1015 { 0: a4, 1: a5 } # i
1016 ]
1018 Attributes:
1019 font (``fontTools.TTLib.TTFont``): A font object.
1020 location: A string or tuple representing the location in the original
1021 source which produced this lookup.
1022 marks: An dictionary mapping a glyph name to a two-element
1023 tuple containing a mark class ID and ``otTables.Anchor`` object.
1024 ligatures: An dictionary mapping a glyph name to an array with one
1025 element for each ligature component. Each array element should be
1026 a dictionary mapping mark class IDs to ``otTables.Anchor`` objects.
1027 lookupflag (int): The lookup's flag
1028 markFilterSet: Either ``None`` if no mark filtering set is used, or
1029 an integer representing the filtering set to be used for this
1030 lookup. If a mark filtering set is provided,
1031 `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
1032 flags.
1033 """
1035 def __init__(self, font, location):
1036 LookupBuilder.__init__(self, font, location, "GPOS", 5)
1037 self.marks = {} # glyphName -> (markClassName, anchor)
1038 self.ligatures = {} # glyphName -> [{markClassName: anchor}, ...]
1040 def equals(self, other):
1041 return (
1042 LookupBuilder.equals(self, other)
1043 and self.marks == other.marks
1044 and self.ligatures == other.ligatures
1045 )
1047 def inferGlyphClasses(self):
1048 result = {glyph: 2 for glyph in self.ligatures}
1049 result.update({glyph: 3 for glyph in self.marks})
1050 return result
1052 def build(self):
1053 """Build the lookup.
1055 Returns:
1056 An ``otTables.Lookup`` object representing the mark-to-ligature
1057 positioning lookup.
1058 """
1059 markClasses = self.buildMarkClasses_(self.marks)
1060 marks = {
1061 mark: (markClasses[mc], anchor) for mark, (mc, anchor) in self.marks.items()
1062 }
1063 ligs = {}
1064 for lig, components in self.ligatures.items():
1065 ligs[lig] = []
1066 for c in components:
1067 ligs[lig].append({markClasses[mc]: a for mc, a in c.items()})
1068 subtables = buildMarkLigPos(marks, ligs, self.glyphMap)
1069 return self.buildLookup_(subtables)
1072class MarkMarkPosBuilder(LookupBuilder):
1073 """Builds a Mark-To-Mark Positioning (GPOS6) lookup.
1075 Users are expected to manually add marks and bases to the ``marks``
1076 and ``baseMarks`` attributes after the object has been initialized, e.g.::
1078 builder.marks["acute"] = (0, a1)
1079 builder.marks["grave"] = (0, a1)
1080 builder.marks["cedilla"] = (1, a2)
1081 builder.baseMarks["acute"] = {0: a3}
1083 Attributes:
1084 font (``fontTools.TTLib.TTFont``): A font object.
1085 location: A string or tuple representing the location in the original
1086 source which produced this lookup.
1087 marks: An dictionary mapping a glyph name to a two-element
1088 tuple containing a mark class ID and ``otTables.Anchor`` object.
1089 baseMarks: An dictionary mapping a glyph name to a dictionary
1090 containing one item: a mark class ID and a ``otTables.Anchor`` object.
1091 lookupflag (int): The lookup's flag
1092 markFilterSet: Either ``None`` if no mark filtering set is used, or
1093 an integer representing the filtering set to be used for this
1094 lookup. If a mark filtering set is provided,
1095 `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
1096 flags.
1097 """
1099 def __init__(self, font, location):
1100 LookupBuilder.__init__(self, font, location, "GPOS", 6)
1101 self.marks = {} # glyphName -> (markClassName, anchor)
1102 self.baseMarks = {} # glyphName -> {markClassName: anchor}
1104 def equals(self, other):
1105 return (
1106 LookupBuilder.equals(self, other)
1107 and self.marks == other.marks
1108 and self.baseMarks == other.baseMarks
1109 )
1111 def inferGlyphClasses(self):
1112 result = {glyph: 3 for glyph in self.baseMarks}
1113 result.update({glyph: 3 for glyph in self.marks})
1114 return result
1116 def build(self):
1117 """Build the lookup.
1119 Returns:
1120 An ``otTables.Lookup`` object representing the mark-to-mark
1121 positioning lookup.
1122 """
1123 markClasses = self.buildMarkClasses_(self.marks)
1124 markClassList = sorted(markClasses.keys(), key=markClasses.get)
1125 marks = {
1126 mark: (markClasses[mc], anchor) for mark, (mc, anchor) in self.marks.items()
1127 }
1129 st = ot.MarkMarkPos()
1130 st.Format = 1
1131 st.ClassCount = len(markClasses)
1132 st.Mark1Coverage = buildCoverage(marks, self.glyphMap)
1133 st.Mark2Coverage = buildCoverage(self.baseMarks, self.glyphMap)
1134 st.Mark1Array = buildMarkArray(marks, self.glyphMap)
1135 st.Mark2Array = ot.Mark2Array()
1136 st.Mark2Array.Mark2Count = len(st.Mark2Coverage.glyphs)
1137 st.Mark2Array.Mark2Record = []
1138 for base in st.Mark2Coverage.glyphs:
1139 anchors = [self.baseMarks[base].get(mc) for mc in markClassList]
1140 st.Mark2Array.Mark2Record.append(buildMark2Record(anchors))
1141 return self.buildLookup_([st])
1144class ReverseChainSingleSubstBuilder(LookupBuilder):
1145 """Builds a Reverse Chaining Contextual Single Substitution (GSUB8) lookup.
1147 Users are expected to manually add substitutions to the ``substitutions``
1148 attribute after the object has been initialized, e.g.::
1150 # reversesub [a e n] d' by d.alt;
1151 prefix = [ ["a", "e", "n"] ]
1152 suffix = []
1153 mapping = { "d": "d.alt" }
1154 builder.substitutions.append( (prefix, suffix, mapping) )
1156 Attributes:
1157 font (``fontTools.TTLib.TTFont``): A font object.
1158 location: A string or tuple representing the location in the original
1159 source which produced this lookup.
1160 substitutions: A three-element tuple consisting of a prefix sequence,
1161 a suffix sequence, and a dictionary of single substitutions.
1162 lookupflag (int): The lookup's flag
1163 markFilterSet: Either ``None`` if no mark filtering set is used, or
1164 an integer representing the filtering set to be used for this
1165 lookup. If a mark filtering set is provided,
1166 `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
1167 flags.
1168 """
1170 def __init__(self, font, location):
1171 LookupBuilder.__init__(self, font, location, "GSUB", 8)
1172 self.rules = [] # (prefix, suffix, mapping)
1174 def equals(self, other):
1175 return LookupBuilder.equals(self, other) and self.rules == other.rules
1177 def build(self):
1178 """Build the lookup.
1180 Returns:
1181 An ``otTables.Lookup`` object representing the chained
1182 contextual substitution lookup.
1183 """
1184 subtables = []
1185 for prefix, suffix, mapping in self.rules:
1186 st = ot.ReverseChainSingleSubst()
1187 st.Format = 1
1188 self.setBacktrackCoverage_(prefix, st)
1189 self.setLookAheadCoverage_(suffix, st)
1190 st.Coverage = buildCoverage(mapping.keys(), self.glyphMap)
1191 st.GlyphCount = len(mapping)
1192 st.Substitute = [mapping[g] for g in st.Coverage.glyphs]
1193 subtables.append(st)
1194 return self.buildLookup_(subtables)
1196 def add_subtable_break(self, location):
1197 # Nothing to do here, each substitution is in its own subtable.
1198 pass
1201class SingleSubstBuilder(LookupBuilder):
1202 """Builds a Single Substitution (GSUB1) lookup.
1204 Users are expected to manually add substitutions to the ``mapping``
1205 attribute after the object has been initialized, e.g.::
1207 # sub x by y;
1208 builder.mapping["x"] = "y"
1210 Attributes:
1211 font (``fontTools.TTLib.TTFont``): A font object.
1212 location: A string or tuple representing the location in the original
1213 source which produced this lookup.
1214 mapping: A dictionary mapping a single glyph name to another glyph name.
1215 lookupflag (int): The lookup's flag
1216 markFilterSet: Either ``None`` if no mark filtering set is used, or
1217 an integer representing the filtering set to be used for this
1218 lookup. If a mark filtering set is provided,
1219 `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
1220 flags.
1221 """
1223 def __init__(self, font, location):
1224 LookupBuilder.__init__(self, font, location, "GSUB", 1)
1225 self.mapping = OrderedDict()
1227 def equals(self, other):
1228 return LookupBuilder.equals(self, other) and self.mapping == other.mapping
1230 def build(self):
1231 """Build the lookup.
1233 Returns:
1234 An ``otTables.Lookup`` object representing the multiple
1235 substitution lookup.
1236 """
1237 subtables = self.build_subst_subtables(self.mapping, buildSingleSubstSubtable)
1238 return self.buildLookup_(subtables)
1240 def getAlternateGlyphs(self):
1241 return {glyph: set([repl]) for glyph, repl in self.mapping.items()}
1243 def add_subtable_break(self, location):
1244 self.mapping[(self.SUBTABLE_BREAK_, location)] = self.SUBTABLE_BREAK_
1247class ClassPairPosSubtableBuilder(object):
1248 """Builds class-based Pair Positioning (GPOS2 format 2) subtables.
1250 Note that this does *not* build a GPOS2 ``otTables.Lookup`` directly,
1251 but builds a list of ``otTables.PairPos`` subtables. It is used by the
1252 :class:`PairPosBuilder` below.
1254 Attributes:
1255 builder (PairPosBuilder): A pair positioning lookup builder.
1256 """
1258 def __init__(self, builder):
1259 self.builder_ = builder
1260 self.classDef1_, self.classDef2_ = None, None
1261 self.values_ = {} # (glyphclass1, glyphclass2) --> (value1, value2)
1262 self.forceSubtableBreak_ = False
1263 self.subtables_ = []
1265 def addPair(self, gc1, value1, gc2, value2):
1266 """Add a pair positioning rule.
1268 Args:
1269 gc1: A set of glyph names for the "left" glyph
1270 value1: An ``otTables.ValueRecord`` object for the left glyph's
1271 positioning.
1272 gc2: A set of glyph names for the "right" glyph
1273 value2: An ``otTables.ValueRecord`` object for the right glyph's
1274 positioning.
1275 """
1276 mergeable = (
1277 not self.forceSubtableBreak_
1278 and self.classDef1_ is not None
1279 and self.classDef1_.canAdd(gc1)
1280 and self.classDef2_ is not None
1281 and self.classDef2_.canAdd(gc2)
1282 )
1283 if not mergeable:
1284 self.flush_()
1285 self.classDef1_ = ClassDefBuilder(useClass0=True)
1286 self.classDef2_ = ClassDefBuilder(useClass0=False)
1287 self.values_ = {}
1288 self.classDef1_.add(gc1)
1289 self.classDef2_.add(gc2)
1290 self.values_[(gc1, gc2)] = (value1, value2)
1292 def addSubtableBreak(self):
1293 """Add an explicit subtable break at this point."""
1294 self.forceSubtableBreak_ = True
1296 def subtables(self):
1297 """Return the list of ``otTables.PairPos`` subtables constructed."""
1298 self.flush_()
1299 return self.subtables_
1301 def flush_(self):
1302 if self.classDef1_ is None or self.classDef2_ is None:
1303 return
1304 st = buildPairPosClassesSubtable(self.values_, self.builder_.glyphMap)
1305 if st.Coverage is None:
1306 return
1307 self.subtables_.append(st)
1308 self.forceSubtableBreak_ = False
1311class PairPosBuilder(LookupBuilder):
1312 """Builds a Pair Positioning (GPOS2) lookup.
1314 Attributes:
1315 font (``fontTools.TTLib.TTFont``): A font object.
1316 location: A string or tuple representing the location in the original
1317 source which produced this lookup.
1318 pairs: An array of class-based pair positioning tuples. Usually
1319 manipulated with the :meth:`addClassPair` method below.
1320 glyphPairs: A dictionary mapping a tuple of glyph names to a tuple
1321 of ``otTables.ValueRecord`` objects. Usually manipulated with the
1322 :meth:`addGlyphPair` method below.
1323 lookupflag (int): The lookup's flag
1324 markFilterSet: Either ``None`` if no mark filtering set is used, or
1325 an integer representing the filtering set to be used for this
1326 lookup. If a mark filtering set is provided,
1327 `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
1328 flags.
1329 """
1331 def __init__(self, font, location):
1332 LookupBuilder.__init__(self, font, location, "GPOS", 2)
1333 self.pairs = [] # [(gc1, value1, gc2, value2)*]
1334 self.glyphPairs = {} # (glyph1, glyph2) --> (value1, value2)
1335 self.locations = {} # (gc1, gc2) --> (filepath, line, column)
1337 def addClassPair(self, location, glyphclass1, value1, glyphclass2, value2):
1338 """Add a class pair positioning rule to the current lookup.
1340 Args:
1341 location: A string or tuple representing the location in the
1342 original source which produced this rule. Unused.
1343 glyphclass1: A set of glyph names for the "left" glyph in the pair.
1344 value1: A ``otTables.ValueRecord`` for positioning the left glyph.
1345 glyphclass2: A set of glyph names for the "right" glyph in the pair.
1346 value2: A ``otTables.ValueRecord`` for positioning the right glyph.
1347 """
1348 self.pairs.append((glyphclass1, value1, glyphclass2, value2))
1350 def addGlyphPair(self, location, glyph1, value1, glyph2, value2):
1351 """Add a glyph pair positioning rule to the current lookup.
1353 Args:
1354 location: A string or tuple representing the location in the
1355 original source which produced this rule.
1356 glyph1: A glyph name for the "left" glyph in the pair.
1357 value1: A ``otTables.ValueRecord`` for positioning the left glyph.
1358 glyph2: A glyph name for the "right" glyph in the pair.
1359 value2: A ``otTables.ValueRecord`` for positioning the right glyph.
1360 """
1361 key = (glyph1, glyph2)
1362 oldValue = self.glyphPairs.get(key, None)
1363 if oldValue is not None:
1364 # the Feature File spec explicitly allows specific pairs generated
1365 # by an 'enum' rule to be overridden by preceding single pairs
1366 otherLoc = self.locations[key]
1367 log.debug(
1368 "Already defined position for pair %s %s at %s; "
1369 "choosing the first value",
1370 glyph1,
1371 glyph2,
1372 otherLoc,
1373 )
1374 else:
1375 self.glyphPairs[key] = (value1, value2)
1376 self.locations[key] = location
1378 def add_subtable_break(self, location):
1379 self.pairs.append(
1380 (
1381 self.SUBTABLE_BREAK_,
1382 self.SUBTABLE_BREAK_,
1383 self.SUBTABLE_BREAK_,
1384 self.SUBTABLE_BREAK_,
1385 )
1386 )
1388 def equals(self, other):
1389 return (
1390 LookupBuilder.equals(self, other)
1391 and self.glyphPairs == other.glyphPairs
1392 and self.pairs == other.pairs
1393 )
1395 def build(self):
1396 """Build the lookup.
1398 Returns:
1399 An ``otTables.Lookup`` object representing the pair positioning
1400 lookup.
1401 """
1402 builders = {}
1403 builder = ClassPairPosSubtableBuilder(self)
1404 for glyphclass1, value1, glyphclass2, value2 in self.pairs:
1405 if glyphclass1 is self.SUBTABLE_BREAK_:
1406 builder.addSubtableBreak()
1407 continue
1408 builder.addPair(glyphclass1, value1, glyphclass2, value2)
1409 subtables = []
1410 if self.glyphPairs:
1411 subtables.extend(buildPairPosGlyphs(self.glyphPairs, self.glyphMap))
1412 subtables.extend(builder.subtables())
1413 lookup = self.buildLookup_(subtables)
1415 # Compact the lookup
1416 # This is a good moment to do it because the compaction should create
1417 # smaller subtables, which may prevent overflows from happening.
1418 # Keep reading the value from the ENV until ufo2ft switches to the config system
1419 level = self.font.cfg.get(
1420 "fontTools.otlLib.optimize.gpos:COMPRESSION_LEVEL",
1421 default=_compression_level_from_env(),
1422 )
1423 if level != 0:
1424 log.info("Compacting GPOS...")
1425 compact_lookup(self.font, level, lookup)
1427 return lookup
1430class SinglePosBuilder(LookupBuilder):
1431 """Builds a Single Positioning (GPOS1) lookup.
1433 Attributes:
1434 font (``fontTools.TTLib.TTFont``): A font object.
1435 location: A string or tuple representing the location in the original
1436 source which produced this lookup.
1437 mapping: A dictionary mapping a glyph name to a ``otTables.ValueRecord``
1438 objects. Usually manipulated with the :meth:`add_pos` method below.
1439 lookupflag (int): The lookup's flag
1440 markFilterSet: Either ``None`` if no mark filtering set is used, or
1441 an integer representing the filtering set to be used for this
1442 lookup. If a mark filtering set is provided,
1443 `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
1444 flags.
1445 """
1447 def __init__(self, font, location):
1448 LookupBuilder.__init__(self, font, location, "GPOS", 1)
1449 self.locations = {} # glyph -> (filename, line, column)
1450 self.mapping = {} # glyph -> ot.ValueRecord
1452 def add_pos(self, location, glyph, otValueRecord):
1453 """Add a single positioning rule.
1455 Args:
1456 location: A string or tuple representing the location in the
1457 original source which produced this lookup.
1458 glyph: A glyph name.
1459 otValueRection: A ``otTables.ValueRecord`` used to position the
1460 glyph.
1461 """
1462 if not self.can_add(glyph, otValueRecord):
1463 otherLoc = self.locations[glyph]
1464 raise OpenTypeLibError(
1465 'Already defined different position for glyph "%s" at %s'
1466 % (glyph, otherLoc),
1467 location,
1468 )
1469 if otValueRecord:
1470 self.mapping[glyph] = otValueRecord
1471 self.locations[glyph] = location
1473 def can_add(self, glyph, value):
1474 assert isinstance(value, ValueRecord)
1475 curValue = self.mapping.get(glyph)
1476 return curValue is None or curValue == value
1478 def equals(self, other):
1479 return LookupBuilder.equals(self, other) and self.mapping == other.mapping
1481 def build(self):
1482 """Build the lookup.
1484 Returns:
1485 An ``otTables.Lookup`` object representing the single positioning
1486 lookup.
1487 """
1488 subtables = buildSinglePos(self.mapping, self.glyphMap)
1489 return self.buildLookup_(subtables)
1492# GSUB
1495def buildSingleSubstSubtable(mapping):
1496 """Builds a single substitution (GSUB1) subtable.
1498 Note that if you are implementing a layout compiler, you may find it more
1499 flexible to use
1500 :py:class:`fontTools.otlLib.lookupBuilders.SingleSubstBuilder` instead.
1502 Args:
1503 mapping: A dictionary mapping input glyph names to output glyph names.
1505 Returns:
1506 An ``otTables.SingleSubst`` object, or ``None`` if the mapping dictionary
1507 is empty.
1508 """
1509 if not mapping:
1510 return None
1511 self = ot.SingleSubst()
1512 self.mapping = dict(mapping)
1513 return self
1516def buildMultipleSubstSubtable(mapping):
1517 """Builds a multiple substitution (GSUB2) subtable.
1519 Note that if you are implementing a layout compiler, you may find it more
1520 flexible to use
1521 :py:class:`fontTools.otlLib.lookupBuilders.MultipleSubstBuilder` instead.
1523 Example::
1525 # sub uni06C0 by uni06D5.fina hamza.above
1526 # sub uni06C2 by uni06C1.fina hamza.above;
1528 subtable = buildMultipleSubstSubtable({
1529 "uni06C0": [ "uni06D5.fina", "hamza.above"],
1530 "uni06C2": [ "uni06D1.fina", "hamza.above"]
1531 })
1533 Args:
1534 mapping: A dictionary mapping input glyph names to a list of output
1535 glyph names.
1537 Returns:
1538 An ``otTables.MultipleSubst`` object or ``None`` if the mapping dictionary
1539 is empty.
1540 """
1541 if not mapping:
1542 return None
1543 self = ot.MultipleSubst()
1544 self.mapping = dict(mapping)
1545 return self
1548def buildAlternateSubstSubtable(mapping):
1549 """Builds an alternate substitution (GSUB3) subtable.
1551 Note that if you are implementing a layout compiler, you may find it more
1552 flexible to use
1553 :py:class:`fontTools.otlLib.lookupBuilders.AlternateSubstBuilder` instead.
1555 Args:
1556 mapping: A dictionary mapping input glyph names to a list of output
1557 glyph names.
1559 Returns:
1560 An ``otTables.AlternateSubst`` object or ``None`` if the mapping dictionary
1561 is empty.
1562 """
1563 if not mapping:
1564 return None
1565 self = ot.AlternateSubst()
1566 self.alternates = dict(mapping)
1567 return self
1570def _getLigatureKey(components):
1571 # Computes a key for ordering ligatures in a GSUB Type-4 lookup.
1573 # When building the OpenType lookup, we need to make sure that
1574 # the longest sequence of components is listed first, so we
1575 # use the negative length as the primary key for sorting.
1576 # To make buildLigatureSubstSubtable() deterministic, we use the
1577 # component sequence as the secondary key.
1579 # For example, this will sort (f,f,f) < (f,f,i) < (f,f) < (f,i) < (f,l).
1580 return (-len(components), components)
1583def buildLigatureSubstSubtable(mapping):
1584 """Builds a ligature substitution (GSUB4) subtable.
1586 Note that if you are implementing a layout compiler, you may find it more
1587 flexible to use
1588 :py:class:`fontTools.otlLib.lookupBuilders.LigatureSubstBuilder` instead.
1590 Example::
1592 # sub f f i by f_f_i;
1593 # sub f i by f_i;
1595 subtable = buildLigatureSubstSubtable({
1596 ("f", "f", "i"): "f_f_i",
1597 ("f", "i"): "f_i",
1598 })
1600 Args:
1601 mapping: A dictionary mapping tuples of glyph names to output
1602 glyph names.
1604 Returns:
1605 An ``otTables.LigatureSubst`` object or ``None`` if the mapping dictionary
1606 is empty.
1607 """
1609 if not mapping:
1610 return None
1611 self = ot.LigatureSubst()
1612 # The following single line can replace the rest of this function
1613 # with fontTools >= 3.1:
1614 # self.ligatures = dict(mapping)
1615 self.ligatures = {}
1616 for components in sorted(mapping.keys(), key=_getLigatureKey):
1617 ligature = ot.Ligature()
1618 ligature.Component = components[1:]
1619 ligature.CompCount = len(ligature.Component) + 1
1620 ligature.LigGlyph = mapping[components]
1621 firstGlyph = components[0]
1622 self.ligatures.setdefault(firstGlyph, []).append(ligature)
1623 return self
1626# GPOS
1629def buildAnchor(x, y, point=None, deviceX=None, deviceY=None):
1630 """Builds an Anchor table.
1632 This determines the appropriate anchor format based on the passed parameters.
1634 Args:
1635 x (int): X coordinate.
1636 y (int): Y coordinate.
1637 point (int): Index of glyph contour point, if provided.
1638 deviceX (``otTables.Device``): X coordinate device table, if provided.
1639 deviceY (``otTables.Device``): Y coordinate device table, if provided.
1641 Returns:
1642 An ``otTables.Anchor`` object.
1643 """
1644 self = ot.Anchor()
1645 self.XCoordinate, self.YCoordinate = x, y
1646 self.Format = 1
1647 if point is not None:
1648 self.AnchorPoint = point
1649 self.Format = 2
1650 if deviceX is not None or deviceY is not None:
1651 assert (
1652 self.Format == 1
1653 ), "Either point, or both of deviceX/deviceY, must be None."
1654 self.XDeviceTable = deviceX
1655 self.YDeviceTable = deviceY
1656 self.Format = 3
1657 return self
1660def buildBaseArray(bases, numMarkClasses, glyphMap):
1661 """Builds a base array record.
1663 As part of building mark-to-base positioning rules, you will need to define
1664 a ``BaseArray`` record, which "defines for each base glyph an array of
1665 anchors, one for each mark class." This function builds the base array
1666 subtable.
1668 Example::
1670 bases = {"a": {0: a3, 1: a5}, "b": {0: a4, 1: a5}}
1671 basearray = buildBaseArray(bases, 2, font.getReverseGlyphMap())
1673 Args:
1674 bases (dict): A dictionary mapping anchors to glyphs; the keys being
1675 glyph names, and the values being dictionaries mapping mark class ID
1676 to the appropriate ``otTables.Anchor`` object used for attaching marks
1677 of that class.
1678 numMarkClasses (int): The total number of mark classes for which anchors
1679 are defined.
1680 glyphMap: a glyph name to ID map, typically returned from
1681 ``font.getReverseGlyphMap()``.
1683 Returns:
1684 An ``otTables.BaseArray`` object.
1685 """
1686 self = ot.BaseArray()
1687 self.BaseRecord = []
1688 for base in sorted(bases, key=glyphMap.__getitem__):
1689 b = bases[base]
1690 anchors = [b.get(markClass) for markClass in range(numMarkClasses)]
1691 self.BaseRecord.append(buildBaseRecord(anchors))
1692 self.BaseCount = len(self.BaseRecord)
1693 return self
1696def buildBaseRecord(anchors):
1697 # [otTables.Anchor, otTables.Anchor, ...] --> otTables.BaseRecord
1698 self = ot.BaseRecord()
1699 self.BaseAnchor = anchors
1700 return self
1703def buildComponentRecord(anchors):
1704 """Builds a component record.
1706 As part of building mark-to-ligature positioning rules, you will need to
1707 define ``ComponentRecord`` objects, which contain "an array of offsets...
1708 to the Anchor tables that define all the attachment points used to attach
1709 marks to the component." This function builds the component record.
1711 Args:
1712 anchors: A list of ``otTables.Anchor`` objects or ``None``.
1714 Returns:
1715 A ``otTables.ComponentRecord`` object or ``None`` if no anchors are
1716 supplied.
1717 """
1718 if not anchors:
1719 return None
1720 self = ot.ComponentRecord()
1721 self.LigatureAnchor = anchors
1722 return self
1725def buildCursivePosSubtable(attach, glyphMap):
1726 """Builds a cursive positioning (GPOS3) subtable.
1728 Cursive positioning lookups are made up of a coverage table of glyphs,
1729 and a set of ``EntryExitRecord`` records containing the anchors for
1730 each glyph. This function builds the cursive positioning subtable.
1732 Example::
1734 subtable = buildCursivePosSubtable({
1735 "AlifIni": (None, buildAnchor(0, 50)),
1736 "BehMed": (buildAnchor(500,250), buildAnchor(0,50)),
1737 # ...
1738 }, font.getReverseGlyphMap())
1740 Args:
1741 attach (dict): A mapping between glyph names and a tuple of two
1742 ``otTables.Anchor`` objects representing entry and exit anchors.
1743 glyphMap: a glyph name to ID map, typically returned from
1744 ``font.getReverseGlyphMap()``.
1746 Returns:
1747 An ``otTables.CursivePos`` object, or ``None`` if the attachment
1748 dictionary was empty.
1749 """
1750 if not attach:
1751 return None
1752 self = ot.CursivePos()
1753 self.Format = 1
1754 self.Coverage = buildCoverage(attach.keys(), glyphMap)
1755 self.EntryExitRecord = []
1756 for glyph in self.Coverage.glyphs:
1757 entryAnchor, exitAnchor = attach[glyph]
1758 rec = ot.EntryExitRecord()
1759 rec.EntryAnchor = entryAnchor
1760 rec.ExitAnchor = exitAnchor
1761 self.EntryExitRecord.append(rec)
1762 self.EntryExitCount = len(self.EntryExitRecord)
1763 return self
1766def buildDevice(deltas):
1767 """Builds a Device record as part of a ValueRecord or Anchor.
1769 Device tables specify size-specific adjustments to value records
1770 and anchors to reflect changes based on the resolution of the output.
1771 For example, one could specify that an anchor's Y position should be
1772 increased by 1 pixel when displayed at 8 pixels per em. This routine
1773 builds device records.
1775 Args:
1776 deltas: A dictionary mapping pixels-per-em sizes to the delta
1777 adjustment in pixels when the font is displayed at that size.
1779 Returns:
1780 An ``otTables.Device`` object if any deltas were supplied, or
1781 ``None`` otherwise.
1782 """
1783 if not deltas:
1784 return None
1785 self = ot.Device()
1786 keys = deltas.keys()
1787 self.StartSize = startSize = min(keys)
1788 self.EndSize = endSize = max(keys)
1789 assert 0 <= startSize <= endSize
1790 self.DeltaValue = deltaValues = [
1791 deltas.get(size, 0) for size in range(startSize, endSize + 1)
1792 ]
1793 maxDelta = max(deltaValues)
1794 minDelta = min(deltaValues)
1795 assert minDelta > -129 and maxDelta < 128
1796 if minDelta > -3 and maxDelta < 2:
1797 self.DeltaFormat = 1
1798 elif minDelta > -9 and maxDelta < 8:
1799 self.DeltaFormat = 2
1800 else:
1801 self.DeltaFormat = 3
1802 return self
1805def buildLigatureArray(ligs, numMarkClasses, glyphMap):
1806 """Builds a LigatureArray subtable.
1808 As part of building a mark-to-ligature lookup, you will need to define
1809 the set of anchors (for each mark class) on each component of the ligature
1810 where marks can be attached. For example, for an Arabic divine name ligature
1811 (lam lam heh), you may want to specify mark attachment positioning for
1812 superior marks (fatha, etc.) and inferior marks (kasra, etc.) on each glyph
1813 of the ligature. This routine builds the ligature array record.
1815 Example::
1817 buildLigatureArray({
1818 "lam-lam-heh": [
1819 { 0: superiorAnchor1, 1: inferiorAnchor1 }, # attach points for lam1
1820 { 0: superiorAnchor2, 1: inferiorAnchor2 }, # attach points for lam2
1821 { 0: superiorAnchor3, 1: inferiorAnchor3 }, # attach points for heh
1822 ]
1823 }, 2, font.getReverseGlyphMap())
1825 Args:
1826 ligs (dict): A mapping of ligature names to an array of dictionaries:
1827 for each component glyph in the ligature, an dictionary mapping
1828 mark class IDs to anchors.
1829 numMarkClasses (int): The number of mark classes.
1830 glyphMap: a glyph name to ID map, typically returned from
1831 ``font.getReverseGlyphMap()``.
1833 Returns:
1834 An ``otTables.LigatureArray`` object if deltas were supplied.
1835 """
1836 self = ot.LigatureArray()
1837 self.LigatureAttach = []
1838 for lig in sorted(ligs, key=glyphMap.__getitem__):
1839 anchors = []
1840 for component in ligs[lig]:
1841 anchors.append([component.get(mc) for mc in range(numMarkClasses)])
1842 self.LigatureAttach.append(buildLigatureAttach(anchors))
1843 self.LigatureCount = len(self.LigatureAttach)
1844 return self
1847def buildLigatureAttach(components):
1848 # [[Anchor, Anchor], [Anchor, Anchor, Anchor]] --> LigatureAttach
1849 self = ot.LigatureAttach()
1850 self.ComponentRecord = [buildComponentRecord(c) for c in components]
1851 self.ComponentCount = len(self.ComponentRecord)
1852 return self
1855def buildMarkArray(marks, glyphMap):
1856 """Builds a mark array subtable.
1858 As part of building mark-to-* positioning rules, you will need to define
1859 a MarkArray subtable, which "defines the class and the anchor point
1860 for a mark glyph." This function builds the mark array subtable.
1862 Example::
1864 mark = {
1865 "acute": (0, buildAnchor(300,712)),
1866 # ...
1867 }
1868 markarray = buildMarkArray(marks, font.getReverseGlyphMap())
1870 Args:
1871 marks (dict): A dictionary mapping anchors to glyphs; the keys being
1872 glyph names, and the values being a tuple of mark class number and
1873 an ``otTables.Anchor`` object representing the mark's attachment
1874 point.
1875 glyphMap: a glyph name to ID map, typically returned from
1876 ``font.getReverseGlyphMap()``.
1878 Returns:
1879 An ``otTables.MarkArray`` object.
1880 """
1881 self = ot.MarkArray()
1882 self.MarkRecord = []
1883 for mark in sorted(marks.keys(), key=glyphMap.__getitem__):
1884 markClass, anchor = marks[mark]
1885 markrec = buildMarkRecord(markClass, anchor)
1886 self.MarkRecord.append(markrec)
1887 self.MarkCount = len(self.MarkRecord)
1888 return self
1891def buildMarkBasePos(marks, bases, glyphMap):
1892 """Build a list of MarkBasePos (GPOS4) subtables.
1894 This routine turns a set of marks and bases into a list of mark-to-base
1895 positioning subtables. Currently the list will contain a single subtable
1896 containing all marks and bases, although at a later date it may return the
1897 optimal list of subtables subsetting the marks and bases into groups which
1898 save space. See :func:`buildMarkBasePosSubtable` below.
1900 Note that if you are implementing a layout compiler, you may find it more
1901 flexible to use
1902 :py:class:`fontTools.otlLib.lookupBuilders.MarkBasePosBuilder` instead.
1904 Example::
1906 # a1, a2, a3, a4, a5 = buildAnchor(500, 100), ...
1908 marks = {"acute": (0, a1), "grave": (0, a1), "cedilla": (1, a2)}
1909 bases = {"a": {0: a3, 1: a5}, "b": {0: a4, 1: a5}}
1910 markbaseposes = buildMarkBasePos(marks, bases, font.getReverseGlyphMap())
1912 Args:
1913 marks (dict): A dictionary mapping anchors to glyphs; the keys being
1914 glyph names, and the values being a tuple of mark class number and
1915 an ``otTables.Anchor`` object representing the mark's attachment
1916 point. (See :func:`buildMarkArray`.)
1917 bases (dict): A dictionary mapping anchors to glyphs; the keys being
1918 glyph names, and the values being dictionaries mapping mark class ID
1919 to the appropriate ``otTables.Anchor`` object used for attaching marks
1920 of that class. (See :func:`buildBaseArray`.)
1921 glyphMap: a glyph name to ID map, typically returned from
1922 ``font.getReverseGlyphMap()``.
1924 Returns:
1925 A list of ``otTables.MarkBasePos`` objects.
1926 """
1927 # TODO: Consider emitting multiple subtables to save space.
1928 # Partition the marks and bases into disjoint subsets, so that
1929 # MarkBasePos rules would only access glyphs from a single
1930 # subset. This would likely lead to smaller mark/base
1931 # matrices, so we might be able to omit many of the empty
1932 # anchor tables that we currently produce. Of course, this
1933 # would only work if the MarkBasePos rules of real-world fonts
1934 # allow partitioning into multiple subsets. We should find out
1935 # whether this is the case; if so, implement the optimization.
1936 # On the other hand, a very large number of subtables could
1937 # slow down layout engines; so this would need profiling.
1938 return [buildMarkBasePosSubtable(marks, bases, glyphMap)]
1941def buildMarkBasePosSubtable(marks, bases, glyphMap):
1942 """Build a single MarkBasePos (GPOS4) subtable.
1944 This builds a mark-to-base lookup subtable containing all of the referenced
1945 marks and bases. See :func:`buildMarkBasePos`.
1947 Args:
1948 marks (dict): A dictionary mapping anchors to glyphs; the keys being
1949 glyph names, and the values being a tuple of mark class number and
1950 an ``otTables.Anchor`` object representing the mark's attachment
1951 point. (See :func:`buildMarkArray`.)
1952 bases (dict): A dictionary mapping anchors to glyphs; the keys being
1953 glyph names, and the values being dictionaries mapping mark class ID
1954 to the appropriate ``otTables.Anchor`` object used for attaching marks
1955 of that class. (See :func:`buildBaseArray`.)
1956 glyphMap: a glyph name to ID map, typically returned from
1957 ``font.getReverseGlyphMap()``.
1959 Returns:
1960 A ``otTables.MarkBasePos`` object.
1961 """
1962 self = ot.MarkBasePos()
1963 self.Format = 1
1964 self.MarkCoverage = buildCoverage(marks, glyphMap)
1965 self.MarkArray = buildMarkArray(marks, glyphMap)
1966 self.ClassCount = max([mc for mc, _ in marks.values()]) + 1
1967 self.BaseCoverage = buildCoverage(bases, glyphMap)
1968 self.BaseArray = buildBaseArray(bases, self.ClassCount, glyphMap)
1969 return self
1972def buildMarkLigPos(marks, ligs, glyphMap):
1973 """Build a list of MarkLigPos (GPOS5) subtables.
1975 This routine turns a set of marks and ligatures into a list of mark-to-ligature
1976 positioning subtables. Currently the list will contain a single subtable
1977 containing all marks and ligatures, although at a later date it may return
1978 the optimal list of subtables subsetting the marks and ligatures into groups
1979 which save space. See :func:`buildMarkLigPosSubtable` below.
1981 Note that if you are implementing a layout compiler, you may find it more
1982 flexible to use
1983 :py:class:`fontTools.otlLib.lookupBuilders.MarkLigPosBuilder` instead.
1985 Example::
1987 # a1, a2, a3, a4, a5 = buildAnchor(500, 100), ...
1988 marks = {
1989 "acute": (0, a1),
1990 "grave": (0, a1),
1991 "cedilla": (1, a2)
1992 }
1993 ligs = {
1994 "f_i": [
1995 { 0: a3, 1: a5 }, # f
1996 { 0: a4, 1: a5 } # i
1997 ],
1998 # "c_t": [{...}, {...}]
1999 }
2000 markligposes = buildMarkLigPos(marks, ligs,
2001 font.getReverseGlyphMap())
2003 Args:
2004 marks (dict): A dictionary mapping anchors to glyphs; the keys being
2005 glyph names, and the values being a tuple of mark class number and
2006 an ``otTables.Anchor`` object representing the mark's attachment
2007 point. (See :func:`buildMarkArray`.)
2008 ligs (dict): A mapping of ligature names to an array of dictionaries:
2009 for each component glyph in the ligature, an dictionary mapping
2010 mark class IDs to anchors. (See :func:`buildLigatureArray`.)
2011 glyphMap: a glyph name to ID map, typically returned from
2012 ``font.getReverseGlyphMap()``.
2014 Returns:
2015 A list of ``otTables.MarkLigPos`` objects.
2017 """
2018 # TODO: Consider splitting into multiple subtables to save space,
2019 # as with MarkBasePos, this would be a trade-off that would need
2020 # profiling. And, depending on how typical fonts are structured,
2021 # it might not be worth doing at all.
2022 return [buildMarkLigPosSubtable(marks, ligs, glyphMap)]
2025def buildMarkLigPosSubtable(marks, ligs, glyphMap):
2026 """Build a single MarkLigPos (GPOS5) subtable.
2028 This builds a mark-to-base lookup subtable containing all of the referenced
2029 marks and bases. See :func:`buildMarkLigPos`.
2031 Args:
2032 marks (dict): A dictionary mapping anchors to glyphs; the keys being
2033 glyph names, and the values being a tuple of mark class number and
2034 an ``otTables.Anchor`` object representing the mark's attachment
2035 point. (See :func:`buildMarkArray`.)
2036 ligs (dict): A mapping of ligature names to an array of dictionaries:
2037 for each component glyph in the ligature, an dictionary mapping
2038 mark class IDs to anchors. (See :func:`buildLigatureArray`.)
2039 glyphMap: a glyph name to ID map, typically returned from
2040 ``font.getReverseGlyphMap()``.
2042 Returns:
2043 A ``otTables.MarkLigPos`` object.
2044 """
2045 self = ot.MarkLigPos()
2046 self.Format = 1
2047 self.MarkCoverage = buildCoverage(marks, glyphMap)
2048 self.MarkArray = buildMarkArray(marks, glyphMap)
2049 self.ClassCount = max([mc for mc, _ in marks.values()]) + 1
2050 self.LigatureCoverage = buildCoverage(ligs, glyphMap)
2051 self.LigatureArray = buildLigatureArray(ligs, self.ClassCount, glyphMap)
2052 return self
2055def buildMarkRecord(classID, anchor):
2056 assert isinstance(classID, int)
2057 assert isinstance(anchor, ot.Anchor)
2058 self = ot.MarkRecord()
2059 self.Class = classID
2060 self.MarkAnchor = anchor
2061 return self
2064def buildMark2Record(anchors):
2065 # [otTables.Anchor, otTables.Anchor, ...] --> otTables.Mark2Record
2066 self = ot.Mark2Record()
2067 self.Mark2Anchor = anchors
2068 return self
2071def _getValueFormat(f, values, i):
2072 # Helper for buildPairPos{Glyphs|Classes}Subtable.
2073 if f is not None:
2074 return f
2075 mask = 0
2076 for value in values:
2077 if value is not None and value[i] is not None:
2078 mask |= value[i].getFormat()
2079 return mask
2082def buildPairPosClassesSubtable(pairs, glyphMap, valueFormat1=None, valueFormat2=None):
2083 """Builds a class pair adjustment (GPOS2 format 2) subtable.
2085 Kerning tables are generally expressed as pair positioning tables using
2086 class-based pair adjustments. This routine builds format 2 PairPos
2087 subtables.
2089 Note that if you are implementing a layout compiler, you may find it more
2090 flexible to use
2091 :py:class:`fontTools.otlLib.lookupBuilders.ClassPairPosSubtableBuilder`
2092 instead, as this takes care of ensuring that the supplied pairs can be
2093 formed into non-overlapping classes and emitting individual subtables
2094 whenever the non-overlapping requirement means that a new subtable is
2095 required.
2097 Example::
2099 pairs = {}
2101 pairs[(
2102 [ "K", "X" ],
2103 [ "W", "V" ]
2104 )] = ( buildValue(xAdvance=+5), buildValue() )
2105 # pairs[(... , ...)] = (..., ...)
2107 pairpos = buildPairPosClassesSubtable(pairs, font.getReverseGlyphMap())
2109 Args:
2110 pairs (dict): Pair positioning data; the keys being a two-element
2111 tuple of lists of glyphnames, and the values being a two-element
2112 tuple of ``otTables.ValueRecord`` objects.
2113 glyphMap: a glyph name to ID map, typically returned from
2114 ``font.getReverseGlyphMap()``.
2115 valueFormat1: Force the "left" value records to the given format.
2116 valueFormat2: Force the "right" value records to the given format.
2118 Returns:
2119 A ``otTables.PairPos`` object.
2120 """
2121 coverage = set()
2122 classDef1 = ClassDefBuilder(useClass0=True)
2123 classDef2 = ClassDefBuilder(useClass0=False)
2124 for gc1, gc2 in sorted(pairs):
2125 coverage.update(gc1)
2126 classDef1.add(gc1)
2127 classDef2.add(gc2)
2128 self = ot.PairPos()
2129 self.Format = 2
2130 valueFormat1 = self.ValueFormat1 = _getValueFormat(valueFormat1, pairs.values(), 0)
2131 valueFormat2 = self.ValueFormat2 = _getValueFormat(valueFormat2, pairs.values(), 1)
2132 self.Coverage = buildCoverage(coverage, glyphMap)
2133 self.ClassDef1 = classDef1.build()
2134 self.ClassDef2 = classDef2.build()
2135 classes1 = classDef1.classes()
2136 classes2 = classDef2.classes()
2137 self.Class1Record = []
2138 for c1 in classes1:
2139 rec1 = ot.Class1Record()
2140 rec1.Class2Record = []
2141 self.Class1Record.append(rec1)
2142 for c2 in classes2:
2143 rec2 = ot.Class2Record()
2144 val1, val2 = pairs.get((c1, c2), (None, None))
2145 rec2.Value1 = (
2146 ValueRecord(src=val1, valueFormat=valueFormat1)
2147 if valueFormat1
2148 else None
2149 )
2150 rec2.Value2 = (
2151 ValueRecord(src=val2, valueFormat=valueFormat2)
2152 if valueFormat2
2153 else None
2154 )
2155 rec1.Class2Record.append(rec2)
2156 self.Class1Count = len(self.Class1Record)
2157 self.Class2Count = len(classes2)
2158 return self
2161def buildPairPosGlyphs(pairs, glyphMap):
2162 """Builds a list of glyph-based pair adjustment (GPOS2 format 1) subtables.
2164 This organises a list of pair positioning adjustments into subtables based
2165 on common value record formats.
2167 Note that if you are implementing a layout compiler, you may find it more
2168 flexible to use
2169 :py:class:`fontTools.otlLib.lookupBuilders.PairPosBuilder`
2170 instead.
2172 Example::
2174 pairs = {
2175 ("K", "W"): ( buildValue(xAdvance=+5), buildValue() ),
2176 ("K", "V"): ( buildValue(xAdvance=+5), buildValue() ),
2177 # ...
2178 }
2180 subtables = buildPairPosGlyphs(pairs, font.getReverseGlyphMap())
2182 Args:
2183 pairs (dict): Pair positioning data; the keys being a two-element
2184 tuple of glyphnames, and the values being a two-element
2185 tuple of ``otTables.ValueRecord`` objects.
2186 glyphMap: a glyph name to ID map, typically returned from
2187 ``font.getReverseGlyphMap()``.
2189 Returns:
2190 A list of ``otTables.PairPos`` objects.
2191 """
2193 p = {} # (formatA, formatB) --> {(glyphA, glyphB): (valA, valB)}
2194 for (glyphA, glyphB), (valA, valB) in pairs.items():
2195 formatA = valA.getFormat() if valA is not None else 0
2196 formatB = valB.getFormat() if valB is not None else 0
2197 pos = p.setdefault((formatA, formatB), {})
2198 pos[(glyphA, glyphB)] = (valA, valB)
2199 return [
2200 buildPairPosGlyphsSubtable(pos, glyphMap, formatA, formatB)
2201 for ((formatA, formatB), pos) in sorted(p.items())
2202 ]
2205def buildPairPosGlyphsSubtable(pairs, glyphMap, valueFormat1=None, valueFormat2=None):
2206 """Builds a single glyph-based pair adjustment (GPOS2 format 1) subtable.
2208 This builds a PairPos subtable from a dictionary of glyph pairs and
2209 their positioning adjustments. See also :func:`buildPairPosGlyphs`.
2211 Note that if you are implementing a layout compiler, you may find it more
2212 flexible to use
2213 :py:class:`fontTools.otlLib.lookupBuilders.PairPosBuilder` instead.
2215 Example::
2217 pairs = {
2218 ("K", "W"): ( buildValue(xAdvance=+5), buildValue() ),
2219 ("K", "V"): ( buildValue(xAdvance=+5), buildValue() ),
2220 # ...
2221 }
2223 pairpos = buildPairPosGlyphsSubtable(pairs, font.getReverseGlyphMap())
2225 Args:
2226 pairs (dict): Pair positioning data; the keys being a two-element
2227 tuple of glyphnames, and the values being a two-element
2228 tuple of ``otTables.ValueRecord`` objects.
2229 glyphMap: a glyph name to ID map, typically returned from
2230 ``font.getReverseGlyphMap()``.
2231 valueFormat1: Force the "left" value records to the given format.
2232 valueFormat2: Force the "right" value records to the given format.
2234 Returns:
2235 A ``otTables.PairPos`` object.
2236 """
2237 self = ot.PairPos()
2238 self.Format = 1
2239 valueFormat1 = self.ValueFormat1 = _getValueFormat(valueFormat1, pairs.values(), 0)
2240 valueFormat2 = self.ValueFormat2 = _getValueFormat(valueFormat2, pairs.values(), 1)
2241 p = {}
2242 for (glyphA, glyphB), (valA, valB) in pairs.items():
2243 p.setdefault(glyphA, []).append((glyphB, valA, valB))
2244 self.Coverage = buildCoverage({g for g, _ in pairs.keys()}, glyphMap)
2245 self.PairSet = []
2246 for glyph in self.Coverage.glyphs:
2247 ps = ot.PairSet()
2248 ps.PairValueRecord = []
2249 self.PairSet.append(ps)
2250 for glyph2, val1, val2 in sorted(p[glyph], key=lambda x: glyphMap[x[0]]):
2251 pvr = ot.PairValueRecord()
2252 pvr.SecondGlyph = glyph2
2253 pvr.Value1 = (
2254 ValueRecord(src=val1, valueFormat=valueFormat1)
2255 if valueFormat1
2256 else None
2257 )
2258 pvr.Value2 = (
2259 ValueRecord(src=val2, valueFormat=valueFormat2)
2260 if valueFormat2
2261 else None
2262 )
2263 ps.PairValueRecord.append(pvr)
2264 ps.PairValueCount = len(ps.PairValueRecord)
2265 self.PairSetCount = len(self.PairSet)
2266 return self
2269def buildSinglePos(mapping, glyphMap):
2270 """Builds a list of single adjustment (GPOS1) subtables.
2272 This builds a list of SinglePos subtables from a dictionary of glyph
2273 names and their positioning adjustments. The format of the subtables are
2274 determined to optimize the size of the resulting subtables.
2275 See also :func:`buildSinglePosSubtable`.
2277 Note that if you are implementing a layout compiler, you may find it more
2278 flexible to use
2279 :py:class:`fontTools.otlLib.lookupBuilders.SinglePosBuilder` instead.
2281 Example::
2283 mapping = {
2284 "V": buildValue({ "xAdvance" : +5 }),
2285 # ...
2286 }
2288 subtables = buildSinglePos(pairs, font.getReverseGlyphMap())
2290 Args:
2291 mapping (dict): A mapping between glyphnames and
2292 ``otTables.ValueRecord`` objects.
2293 glyphMap: a glyph name to ID map, typically returned from
2294 ``font.getReverseGlyphMap()``.
2296 Returns:
2297 A list of ``otTables.SinglePos`` objects.
2298 """
2299 result, handled = [], set()
2300 # In SinglePos format 1, the covered glyphs all share the same ValueRecord.
2301 # In format 2, each glyph has its own ValueRecord, but these records
2302 # all have the same properties (eg., all have an X but no Y placement).
2303 coverages, masks, values = {}, {}, {}
2304 for glyph, value in mapping.items():
2305 key = _getSinglePosValueKey(value)
2306 coverages.setdefault(key, []).append(glyph)
2307 masks.setdefault(key[0], []).append(key)
2308 values[key] = value
2310 # If a ValueRecord is shared between multiple glyphs, we generate
2311 # a SinglePos format 1 subtable; that is the most compact form.
2312 for key, glyphs in coverages.items():
2313 # 5 ushorts is the length of introducing another sublookup
2314 if len(glyphs) * _getSinglePosValueSize(key) > 5:
2315 format1Mapping = {g: values[key] for g in glyphs}
2316 result.append(buildSinglePosSubtable(format1Mapping, glyphMap))
2317 handled.add(key)
2319 # In the remaining ValueRecords, look for those whose valueFormat
2320 # (the set of used properties) is shared between multiple records.
2321 # These will get encoded in format 2.
2322 for valueFormat, keys in masks.items():
2323 f2 = [k for k in keys if k not in handled]
2324 if len(f2) > 1:
2325 format2Mapping = {}
2326 for k in f2:
2327 format2Mapping.update((g, values[k]) for g in coverages[k])
2328 result.append(buildSinglePosSubtable(format2Mapping, glyphMap))
2329 handled.update(f2)
2331 # The remaining ValueRecords are only used by a few glyphs, normally
2332 # one. We encode these in format 1 again.
2333 for key, glyphs in coverages.items():
2334 if key not in handled:
2335 for g in glyphs:
2336 st = buildSinglePosSubtable({g: values[key]}, glyphMap)
2337 result.append(st)
2339 # When the OpenType layout engine traverses the subtables, it will
2340 # stop after the first matching subtable. Therefore, we sort the
2341 # resulting subtables by decreasing coverage size; this increases
2342 # the chance that the layout engine can do an early exit. (Of course,
2343 # this would only be true if all glyphs were equally frequent, which
2344 # is not really the case; but we do not know their distribution).
2345 # If two subtables cover the same number of glyphs, we sort them
2346 # by glyph ID so that our output is deterministic.
2347 result.sort(key=lambda t: _getSinglePosTableKey(t, glyphMap))
2348 return result
2351def buildSinglePosSubtable(values, glyphMap):
2352 """Builds a single adjustment (GPOS1) subtable.
2354 This builds a list of SinglePos subtables from a dictionary of glyph
2355 names and their positioning adjustments. The format of the subtable is
2356 determined to optimize the size of the output.
2357 See also :func:`buildSinglePos`.
2359 Note that if you are implementing a layout compiler, you may find it more
2360 flexible to use
2361 :py:class:`fontTools.otlLib.lookupBuilders.SinglePosBuilder` instead.
2363 Example::
2365 mapping = {
2366 "V": buildValue({ "xAdvance" : +5 }),
2367 # ...
2368 }
2370 subtable = buildSinglePos(pairs, font.getReverseGlyphMap())
2372 Args:
2373 mapping (dict): A mapping between glyphnames and
2374 ``otTables.ValueRecord`` objects.
2375 glyphMap: a glyph name to ID map, typically returned from
2376 ``font.getReverseGlyphMap()``.
2378 Returns:
2379 A ``otTables.SinglePos`` object.
2380 """
2381 self = ot.SinglePos()
2382 self.Coverage = buildCoverage(values.keys(), glyphMap)
2383 valueFormat = self.ValueFormat = reduce(
2384 int.__or__, [v.getFormat() for v in values.values()], 0
2385 )
2386 valueRecords = [
2387 ValueRecord(src=values[g], valueFormat=valueFormat)
2388 for g in self.Coverage.glyphs
2389 ]
2390 if all(v == valueRecords[0] for v in valueRecords):
2391 self.Format = 1
2392 if self.ValueFormat != 0:
2393 self.Value = valueRecords[0]
2394 else:
2395 self.Value = None
2396 else:
2397 self.Format = 2
2398 self.Value = valueRecords
2399 self.ValueCount = len(self.Value)
2400 return self
2403def _getSinglePosTableKey(subtable, glyphMap):
2404 assert isinstance(subtable, ot.SinglePos), subtable
2405 glyphs = subtable.Coverage.glyphs
2406 return (-len(glyphs), glyphMap[glyphs[0]])
2409def _getSinglePosValueKey(valueRecord):
2410 # otBase.ValueRecord --> (2, ("YPlacement": 12))
2411 assert isinstance(valueRecord, ValueRecord), valueRecord
2412 valueFormat, result = 0, []
2413 for name, value in valueRecord.__dict__.items():
2414 if isinstance(value, ot.Device):
2415 result.append((name, _makeDeviceTuple(value)))
2416 else:
2417 result.append((name, value))
2418 valueFormat |= valueRecordFormatDict[name][0]
2419 result.sort()
2420 result.insert(0, valueFormat)
2421 return tuple(result)
2424_DeviceTuple = namedtuple("_DeviceTuple", "DeltaFormat StartSize EndSize DeltaValue")
2427def _makeDeviceTuple(device):
2428 # otTables.Device --> tuple, for making device tables unique
2429 return _DeviceTuple(
2430 device.DeltaFormat,
2431 device.StartSize,
2432 device.EndSize,
2433 () if device.DeltaFormat & 0x8000 else tuple(device.DeltaValue),
2434 )
2437def _getSinglePosValueSize(valueKey):
2438 # Returns how many ushorts this valueKey (short form of ValueRecord) takes up
2439 count = 0
2440 for _, v in valueKey[1:]:
2441 if isinstance(v, _DeviceTuple):
2442 count += len(v.DeltaValue) + 3
2443 else:
2444 count += 1
2445 return count
2448def buildValue(value):
2449 """Builds a positioning value record.
2451 Value records are used to specify coordinates and adjustments for
2452 positioning and attaching glyphs. Many of the positioning functions
2453 in this library take ``otTables.ValueRecord`` objects as arguments.
2454 This function builds value records from dictionaries.
2456 Args:
2457 value (dict): A dictionary with zero or more of the following keys:
2458 - ``xPlacement``
2459 - ``yPlacement``
2460 - ``xAdvance``
2461 - ``yAdvance``
2462 - ``xPlaDevice``
2463 - ``yPlaDevice``
2464 - ``xAdvDevice``
2465 - ``yAdvDevice``
2467 Returns:
2468 An ``otTables.ValueRecord`` object.
2469 """
2470 self = ValueRecord()
2471 for k, v in value.items():
2472 setattr(self, k, v)
2473 return self
2476# GDEF
2479def buildAttachList(attachPoints, glyphMap):
2480 """Builds an AttachList subtable.
2482 A GDEF table may contain an Attachment Point List table (AttachList)
2483 which stores the contour indices of attachment points for glyphs with
2484 attachment points. This routine builds AttachList subtables.
2486 Args:
2487 attachPoints (dict): A mapping between glyph names and a list of
2488 contour indices.
2490 Returns:
2491 An ``otTables.AttachList`` object if attachment points are supplied,
2492 or ``None`` otherwise.
2493 """
2494 if not attachPoints:
2495 return None
2496 self = ot.AttachList()
2497 self.Coverage = buildCoverage(attachPoints.keys(), glyphMap)
2498 self.AttachPoint = [buildAttachPoint(attachPoints[g]) for g in self.Coverage.glyphs]
2499 self.GlyphCount = len(self.AttachPoint)
2500 return self
2503def buildAttachPoint(points):
2504 # [4, 23, 41] --> otTables.AttachPoint
2505 # Only used by above.
2506 if not points:
2507 return None
2508 self = ot.AttachPoint()
2509 self.PointIndex = sorted(set(points))
2510 self.PointCount = len(self.PointIndex)
2511 return self
2514def buildCaretValueForCoord(coord):
2515 # 500 --> otTables.CaretValue, format 1
2516 # (500, DeviceTable) --> otTables.CaretValue, format 3
2517 self = ot.CaretValue()
2518 if isinstance(coord, tuple):
2519 self.Format = 3
2520 self.Coordinate, self.DeviceTable = coord
2521 else:
2522 self.Format = 1
2523 self.Coordinate = coord
2524 return self
2527def buildCaretValueForPoint(point):
2528 # 4 --> otTables.CaretValue, format 2
2529 self = ot.CaretValue()
2530 self.Format = 2
2531 self.CaretValuePoint = point
2532 return self
2535def buildLigCaretList(coords, points, glyphMap):
2536 """Builds a ligature caret list table.
2538 Ligatures appear as a single glyph representing multiple characters; however
2539 when, for example, editing text containing a ``f_i`` ligature, the user may
2540 want to place the cursor between the ``f`` and the ``i``. The ligature caret
2541 list in the GDEF table specifies the position to display the "caret" (the
2542 character insertion indicator, typically a flashing vertical bar) "inside"
2543 the ligature to represent an insertion point. The insertion positions may
2544 be specified either by coordinate or by contour point.
2546 Example::
2548 coords = {
2549 "f_f_i": [300, 600] # f|fi cursor at 300 units, ff|i cursor at 600.
2550 }
2551 points = {
2552 "c_t": [28] # c|t cursor appears at coordinate of contour point 28.
2553 }
2554 ligcaretlist = buildLigCaretList(coords, points, font.getReverseGlyphMap())
2556 Args:
2557 coords: A mapping between glyph names and a list of coordinates for
2558 the insertion point of each ligature component after the first one.
2559 points: A mapping between glyph names and a list of contour points for
2560 the insertion point of each ligature component after the first one.
2561 glyphMap: a glyph name to ID map, typically returned from
2562 ``font.getReverseGlyphMap()``.
2564 Returns:
2565 A ``otTables.LigCaretList`` object if any carets are present, or
2566 ``None`` otherwise."""
2567 glyphs = set(coords.keys()) if coords else set()
2568 if points:
2569 glyphs.update(points.keys())
2570 carets = {g: buildLigGlyph(coords.get(g), points.get(g)) for g in glyphs}
2571 carets = {g: c for g, c in carets.items() if c is not None}
2572 if not carets:
2573 return None
2574 self = ot.LigCaretList()
2575 self.Coverage = buildCoverage(carets.keys(), glyphMap)
2576 self.LigGlyph = [carets[g] for g in self.Coverage.glyphs]
2577 self.LigGlyphCount = len(self.LigGlyph)
2578 return self
2581def buildLigGlyph(coords, points):
2582 # ([500], [4]) --> otTables.LigGlyph; None for empty coords/points
2583 carets = []
2584 if coords:
2585 coords = sorted(coords, key=lambda c: c[0] if isinstance(c, tuple) else c)
2586 carets.extend([buildCaretValueForCoord(c) for c in coords])
2587 if points:
2588 carets.extend([buildCaretValueForPoint(p) for p in sorted(points)])
2589 if not carets:
2590 return None
2591 self = ot.LigGlyph()
2592 self.CaretValue = carets
2593 self.CaretCount = len(self.CaretValue)
2594 return self
2597def buildMarkGlyphSetsDef(markSets, glyphMap):
2598 """Builds a mark glyph sets definition table.
2600 OpenType Layout lookups may choose to use mark filtering sets to consider
2601 or ignore particular combinations of marks. These sets are specified by
2602 setting a flag on the lookup, but the mark filtering sets are defined in
2603 the ``GDEF`` table. This routine builds the subtable containing the mark
2604 glyph set definitions.
2606 Example::
2608 set0 = set("acute", "grave")
2609 set1 = set("caron", "grave")
2611 markglyphsets = buildMarkGlyphSetsDef([set0, set1], font.getReverseGlyphMap())
2613 Args:
2615 markSets: A list of sets of glyphnames.
2616 glyphMap: a glyph name to ID map, typically returned from
2617 ``font.getReverseGlyphMap()``.
2619 Returns
2620 An ``otTables.MarkGlyphSetsDef`` object.
2621 """
2622 if not markSets:
2623 return None
2624 self = ot.MarkGlyphSetsDef()
2625 self.MarkSetTableFormat = 1
2626 self.Coverage = [buildCoverage(m, glyphMap) for m in markSets]
2627 self.MarkSetCount = len(self.Coverage)
2628 return self
2631class ClassDefBuilder(object):
2632 """Helper for building ClassDef tables."""
2634 def __init__(self, useClass0):
2635 self.classes_ = set()
2636 self.glyphs_ = {}
2637 self.useClass0_ = useClass0
2639 def canAdd(self, glyphs):
2640 if isinstance(glyphs, (set, frozenset)):
2641 glyphs = sorted(glyphs)
2642 glyphs = tuple(glyphs)
2643 if glyphs in self.classes_:
2644 return True
2645 for glyph in glyphs:
2646 if glyph in self.glyphs_:
2647 return False
2648 return True
2650 def add(self, glyphs):
2651 if isinstance(glyphs, (set, frozenset)):
2652 glyphs = sorted(glyphs)
2653 glyphs = tuple(glyphs)
2654 if glyphs in self.classes_:
2655 return
2656 self.classes_.add(glyphs)
2657 for glyph in glyphs:
2658 if glyph in self.glyphs_:
2659 raise OpenTypeLibError(
2660 f"Glyph {glyph} is already present in class.", None
2661 )
2662 self.glyphs_[glyph] = glyphs
2664 def classes(self):
2665 # In ClassDef1 tables, class id #0 does not need to be encoded
2666 # because zero is the default. Therefore, we use id #0 for the
2667 # glyph class that has the largest number of members. However,
2668 # in other tables than ClassDef1, 0 means "every other glyph"
2669 # so we should not use that ID for any real glyph classes;
2670 # we implement this by inserting an empty set at position 0.
2671 #
2672 # TODO: Instead of counting the number of glyphs in each class,
2673 # we should determine the encoded size. If the glyphs in a large
2674 # class form a contiguous range, the encoding is actually quite
2675 # compact, whereas a non-contiguous set might need a lot of bytes
2676 # in the output file. We don't get this right with the key below.
2677 result = sorted(self.classes_, key=lambda s: (-len(s), s))
2678 if not self.useClass0_:
2679 result.insert(0, frozenset())
2680 return result
2682 def build(self):
2683 glyphClasses = {}
2684 for classID, glyphs in enumerate(self.classes()):
2685 if classID == 0:
2686 continue
2687 for glyph in glyphs:
2688 glyphClasses[glyph] = classID
2689 classDef = ot.ClassDef()
2690 classDef.classDefs = glyphClasses
2691 return classDef
2694AXIS_VALUE_NEGATIVE_INFINITY = fixedToFloat(-0x80000000, 16)
2695AXIS_VALUE_POSITIVE_INFINITY = fixedToFloat(0x7FFFFFFF, 16)
2698def buildStatTable(
2699 ttFont, axes, locations=None, elidedFallbackName=2, windowsNames=True, macNames=True
2700):
2701 """Add a 'STAT' table to 'ttFont'.
2703 'axes' is a list of dictionaries describing axes and their
2704 values.
2706 Example::
2708 axes = [
2709 dict(
2710 tag="wght",
2711 name="Weight",
2712 ordering=0, # optional
2713 values=[
2714 dict(value=100, name='Thin'),
2715 dict(value=300, name='Light'),
2716 dict(value=400, name='Regular', flags=0x2),
2717 dict(value=900, name='Black'),
2718 ],
2719 )
2720 ]
2722 Each axis dict must have 'tag' and 'name' items. 'tag' maps
2723 to the 'AxisTag' field. 'name' can be a name ID (int), a string,
2724 or a dictionary containing multilingual names (see the
2725 addMultilingualName() name table method), and will translate to
2726 the AxisNameID field.
2728 An axis dict may contain an 'ordering' item that maps to the
2729 AxisOrdering field. If omitted, the order of the axes list is
2730 used to calculate AxisOrdering fields.
2732 The axis dict may contain a 'values' item, which is a list of
2733 dictionaries describing AxisValue records belonging to this axis.
2735 Each value dict must have a 'name' item, which can be a name ID
2736 (int), a string, or a dictionary containing multilingual names,
2737 like the axis name. It translates to the ValueNameID field.
2739 Optionally the value dict can contain a 'flags' item. It maps to
2740 the AxisValue Flags field, and will be 0 when omitted.
2742 The format of the AxisValue is determined by the remaining contents
2743 of the value dictionary:
2745 If the value dict contains a 'value' item, an AxisValue record
2746 Format 1 is created. If in addition to the 'value' item it contains
2747 a 'linkedValue' item, an AxisValue record Format 3 is built.
2749 If the value dict contains a 'nominalValue' item, an AxisValue
2750 record Format 2 is built. Optionally it may contain 'rangeMinValue'
2751 and 'rangeMaxValue' items. These map to -Infinity and +Infinity
2752 respectively if omitted.
2754 You cannot specify Format 4 AxisValue tables this way, as they are
2755 not tied to a single axis, and specify a name for a location that
2756 is defined by multiple axes values. Instead, you need to supply the
2757 'locations' argument.
2759 The optional 'locations' argument specifies AxisValue Format 4
2760 tables. It should be a list of dicts, where each dict has a 'name'
2761 item, which works just like the value dicts above, an optional
2762 'flags' item (defaulting to 0x0), and a 'location' dict. A
2763 location dict key is an axis tag, and the associated value is the
2764 location on the specified axis. They map to the AxisIndex and Value
2765 fields of the AxisValueRecord.
2767 Example::
2769 locations = [
2770 dict(name='Regular ABCD', location=dict(wght=300, ABCD=100)),
2771 dict(name='Bold ABCD XYZ', location=dict(wght=600, ABCD=200)),
2772 ]
2774 The optional 'elidedFallbackName' argument can be a name ID (int),
2775 a string, a dictionary containing multilingual names, or a list of
2776 STATNameStatements. It translates to the ElidedFallbackNameID field.
2778 The 'ttFont' argument must be a TTFont instance that already has a
2779 'name' table. If a 'STAT' table already exists, it will be
2780 overwritten by the newly created one.
2781 """
2782 ttFont["STAT"] = ttLib.newTable("STAT")
2783 statTable = ttFont["STAT"].table = ot.STAT()
2784 nameTable = ttFont["name"]
2785 statTable.ElidedFallbackNameID = _addName(
2786 nameTable, elidedFallbackName, windows=windowsNames, mac=macNames
2787 )
2789 # 'locations' contains data for AxisValue Format 4
2790 axisRecords, axisValues = _buildAxisRecords(
2791 axes, nameTable, windowsNames=windowsNames, macNames=macNames
2792 )
2793 if not locations:
2794 statTable.Version = 0x00010001
2795 else:
2796 # We'll be adding Format 4 AxisValue records, which
2797 # requires a higher table version
2798 statTable.Version = 0x00010002
2799 multiAxisValues = _buildAxisValuesFormat4(
2800 locations, axes, nameTable, windowsNames=windowsNames, macNames=macNames
2801 )
2802 axisValues = multiAxisValues + axisValues
2803 nameTable.names.sort()
2805 # Store AxisRecords
2806 axisRecordArray = ot.AxisRecordArray()
2807 axisRecordArray.Axis = axisRecords
2808 # XXX these should not be hard-coded but computed automatically
2809 statTable.DesignAxisRecordSize = 8
2810 statTable.DesignAxisRecord = axisRecordArray
2811 statTable.DesignAxisCount = len(axisRecords)
2813 statTable.AxisValueCount = 0
2814 statTable.AxisValueArray = None
2815 if axisValues:
2816 # Store AxisValueRecords
2817 axisValueArray = ot.AxisValueArray()
2818 axisValueArray.AxisValue = axisValues
2819 statTable.AxisValueArray = axisValueArray
2820 statTable.AxisValueCount = len(axisValues)
2823def _buildAxisRecords(axes, nameTable, windowsNames=True, macNames=True):
2824 axisRecords = []
2825 axisValues = []
2826 for axisRecordIndex, axisDict in enumerate(axes):
2827 axis = ot.AxisRecord()
2828 axis.AxisTag = axisDict["tag"]
2829 axis.AxisNameID = _addName(
2830 nameTable, axisDict["name"], 256, windows=windowsNames, mac=macNames
2831 )
2832 axis.AxisOrdering = axisDict.get("ordering", axisRecordIndex)
2833 axisRecords.append(axis)
2835 for axisVal in axisDict.get("values", ()):
2836 axisValRec = ot.AxisValue()
2837 axisValRec.AxisIndex = axisRecordIndex
2838 axisValRec.Flags = axisVal.get("flags", 0)
2839 axisValRec.ValueNameID = _addName(
2840 nameTable, axisVal["name"], windows=windowsNames, mac=macNames
2841 )
2843 if "value" in axisVal:
2844 axisValRec.Value = axisVal["value"]
2845 if "linkedValue" in axisVal:
2846 axisValRec.Format = 3
2847 axisValRec.LinkedValue = axisVal["linkedValue"]
2848 else:
2849 axisValRec.Format = 1
2850 elif "nominalValue" in axisVal:
2851 axisValRec.Format = 2
2852 axisValRec.NominalValue = axisVal["nominalValue"]
2853 axisValRec.RangeMinValue = axisVal.get(
2854 "rangeMinValue", AXIS_VALUE_NEGATIVE_INFINITY
2855 )
2856 axisValRec.RangeMaxValue = axisVal.get(
2857 "rangeMaxValue", AXIS_VALUE_POSITIVE_INFINITY
2858 )
2859 else:
2860 raise ValueError("Can't determine format for AxisValue")
2862 axisValues.append(axisValRec)
2863 return axisRecords, axisValues
2866def _buildAxisValuesFormat4(
2867 locations, axes, nameTable, windowsNames=True, macNames=True
2868):
2869 axisTagToIndex = {}
2870 for axisRecordIndex, axisDict in enumerate(axes):
2871 axisTagToIndex[axisDict["tag"]] = axisRecordIndex
2873 axisValues = []
2874 for axisLocationDict in locations:
2875 axisValRec = ot.AxisValue()
2876 axisValRec.Format = 4
2877 axisValRec.ValueNameID = _addName(
2878 nameTable, axisLocationDict["name"], windows=windowsNames, mac=macNames
2879 )
2880 axisValRec.Flags = axisLocationDict.get("flags", 0)
2881 axisValueRecords = []
2882 for tag, value in axisLocationDict["location"].items():
2883 avr = ot.AxisValueRecord()
2884 avr.AxisIndex = axisTagToIndex[tag]
2885 avr.Value = value
2886 axisValueRecords.append(avr)
2887 axisValueRecords.sort(key=lambda avr: avr.AxisIndex)
2888 axisValRec.AxisCount = len(axisValueRecords)
2889 axisValRec.AxisValueRecord = axisValueRecords
2890 axisValues.append(axisValRec)
2891 return axisValues
2894def _addName(nameTable, value, minNameID=0, windows=True, mac=True):
2895 if isinstance(value, int):
2896 # Already a nameID
2897 return value
2898 if isinstance(value, str):
2899 names = dict(en=value)
2900 elif isinstance(value, dict):
2901 names = value
2902 elif isinstance(value, list):
2903 nameID = nameTable._findUnusedNameID()
2904 for nameRecord in value:
2905 if isinstance(nameRecord, STATNameStatement):
2906 nameTable.setName(
2907 nameRecord.string,
2908 nameID,
2909 nameRecord.platformID,
2910 nameRecord.platEncID,
2911 nameRecord.langID,
2912 )
2913 else:
2914 raise TypeError("value must be a list of STATNameStatements")
2915 return nameID
2916 else:
2917 raise TypeError("value must be int, str, dict or list")
2918 return nameTable.addMultilingualName(
2919 names, windows=windows, mac=mac, minNameID=minNameID
2920 )