Spaces:
Running
Running
from collections import namedtuple, OrderedDict | |
import os | |
from fontTools.misc.fixedTools import fixedToFloat | |
from fontTools.misc.roundTools import otRound | |
from fontTools import ttLib | |
from fontTools.ttLib.tables import otTables as ot | |
from fontTools.ttLib.tables.otBase import ( | |
ValueRecord, | |
valueRecordFormatDict, | |
OTLOffsetOverflowError, | |
OTTableWriter, | |
CountReference, | |
) | |
from fontTools.ttLib.tables import otBase | |
from fontTools.feaLib.ast import STATNameStatement | |
from fontTools.otlLib.optimize.gpos import ( | |
_compression_level_from_env, | |
compact_lookup, | |
) | |
from fontTools.otlLib.error import OpenTypeLibError | |
from functools import reduce | |
import logging | |
import copy | |
log = logging.getLogger(__name__) | |
def buildCoverage(glyphs, glyphMap): | |
"""Builds a coverage table. | |
Coverage tables (as defined in the `OpenType spec <https://docs.microsoft.com/en-gb/typography/opentype/spec/chapter2#coverage-table>`__) | |
are used in all OpenType Layout lookups apart from the Extension type, and | |
define the glyphs involved in a layout subtable. This allows shaping engines | |
to compare the glyph stream with the coverage table and quickly determine | |
whether a subtable should be involved in a shaping operation. | |
This function takes a list of glyphs and a glyphname-to-ID map, and | |
returns a ``Coverage`` object representing the coverage table. | |
Example:: | |
glyphMap = font.getReverseGlyphMap() | |
glyphs = [ "A", "B", "C" ] | |
coverage = buildCoverage(glyphs, glyphMap) | |
Args: | |
glyphs: a sequence of glyph names. | |
glyphMap: a glyph name to ID map, typically returned from | |
``font.getReverseGlyphMap()``. | |
Returns: | |
An ``otTables.Coverage`` object or ``None`` if there are no glyphs | |
supplied. | |
""" | |
if not glyphs: | |
return None | |
self = ot.Coverage() | |
try: | |
self.glyphs = sorted(set(glyphs), key=glyphMap.__getitem__) | |
except KeyError as e: | |
raise ValueError(f"Could not find glyph {e} in font") from e | |
return self | |
LOOKUP_FLAG_RIGHT_TO_LEFT = 0x0001 | |
LOOKUP_FLAG_IGNORE_BASE_GLYPHS = 0x0002 | |
LOOKUP_FLAG_IGNORE_LIGATURES = 0x0004 | |
LOOKUP_FLAG_IGNORE_MARKS = 0x0008 | |
LOOKUP_FLAG_USE_MARK_FILTERING_SET = 0x0010 | |
def buildLookup(subtables, flags=0, markFilterSet=None): | |
"""Turns a collection of rules into a lookup. | |
A Lookup (as defined in the `OpenType Spec <https://docs.microsoft.com/en-gb/typography/opentype/spec/chapter2#lookupTbl>`__) | |
wraps the individual rules in a layout operation (substitution or | |
positioning) in a data structure expressing their overall lookup type - | |
for example, single substitution, mark-to-base attachment, and so on - | |
as well as the lookup flags and any mark filtering sets. You may import | |
the following constants to express lookup flags: | |
- ``LOOKUP_FLAG_RIGHT_TO_LEFT`` | |
- ``LOOKUP_FLAG_IGNORE_BASE_GLYPHS`` | |
- ``LOOKUP_FLAG_IGNORE_LIGATURES`` | |
- ``LOOKUP_FLAG_IGNORE_MARKS`` | |
- ``LOOKUP_FLAG_USE_MARK_FILTERING_SET`` | |
Args: | |
subtables: A list of layout subtable objects (e.g. | |
``MultipleSubst``, ``PairPos``, etc.) or ``None``. | |
flags (int): This lookup's flags. | |
markFilterSet: Either ``None`` if no mark filtering set is used, or | |
an integer representing the filtering set to be used for this | |
lookup. If a mark filtering set is provided, | |
`LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's | |
flags. | |
Returns: | |
An ``otTables.Lookup`` object or ``None`` if there are no subtables | |
supplied. | |
""" | |
if subtables is None: | |
return None | |
subtables = [st for st in subtables if st is not None] | |
if not subtables: | |
return None | |
assert all( | |
t.LookupType == subtables[0].LookupType for t in subtables | |
), "all subtables must have the same LookupType; got %s" % repr( | |
[t.LookupType for t in subtables] | |
) | |
self = ot.Lookup() | |
self.LookupType = subtables[0].LookupType | |
self.LookupFlag = flags | |
self.SubTable = subtables | |
self.SubTableCount = len(self.SubTable) | |
if markFilterSet is not None: | |
self.LookupFlag |= LOOKUP_FLAG_USE_MARK_FILTERING_SET | |
assert isinstance(markFilterSet, int), markFilterSet | |
self.MarkFilteringSet = markFilterSet | |
else: | |
assert (self.LookupFlag & LOOKUP_FLAG_USE_MARK_FILTERING_SET) == 0, ( | |
"if markFilterSet is None, flags must not set " | |
"LOOKUP_FLAG_USE_MARK_FILTERING_SET; flags=0x%04x" % flags | |
) | |
return self | |
class LookupBuilder(object): | |
SUBTABLE_BREAK_ = "SUBTABLE_BREAK" | |
def __init__(self, font, location, table, lookup_type): | |
self.font = font | |
self.glyphMap = font.getReverseGlyphMap() | |
self.location = location | |
self.table, self.lookup_type = table, lookup_type | |
self.lookupflag = 0 | |
self.markFilterSet = None | |
self.lookup_index = None # assigned when making final tables | |
assert table in ("GPOS", "GSUB") | |
def equals(self, other): | |
return ( | |
isinstance(other, self.__class__) | |
and self.table == other.table | |
and self.lookupflag == other.lookupflag | |
and self.markFilterSet == other.markFilterSet | |
) | |
def inferGlyphClasses(self): | |
"""Infers glyph glasses for the GDEF table, such as {"cedilla":3}.""" | |
return {} | |
def getAlternateGlyphs(self): | |
"""Helper for building 'aalt' features.""" | |
return {} | |
def buildLookup_(self, subtables): | |
return buildLookup(subtables, self.lookupflag, self.markFilterSet) | |
def buildMarkClasses_(self, marks): | |
"""{"cedilla": ("BOTTOM", ast.Anchor), ...} --> {"BOTTOM":0, "TOP":1} | |
Helper for MarkBasePostBuilder, MarkLigPosBuilder, and | |
MarkMarkPosBuilder. Seems to return the same numeric IDs | |
for mark classes as the AFDKO makeotf tool. | |
""" | |
ids = {} | |
for mark in sorted(marks.keys(), key=self.font.getGlyphID): | |
markClassName, _markAnchor = marks[mark] | |
if markClassName not in ids: | |
ids[markClassName] = len(ids) | |
return ids | |
def setBacktrackCoverage_(self, prefix, subtable): | |
subtable.BacktrackGlyphCount = len(prefix) | |
subtable.BacktrackCoverage = [] | |
for p in reversed(prefix): | |
coverage = buildCoverage(p, self.glyphMap) | |
subtable.BacktrackCoverage.append(coverage) | |
def setLookAheadCoverage_(self, suffix, subtable): | |
subtable.LookAheadGlyphCount = len(suffix) | |
subtable.LookAheadCoverage = [] | |
for s in suffix: | |
coverage = buildCoverage(s, self.glyphMap) | |
subtable.LookAheadCoverage.append(coverage) | |
def setInputCoverage_(self, glyphs, subtable): | |
subtable.InputGlyphCount = len(glyphs) | |
subtable.InputCoverage = [] | |
for g in glyphs: | |
coverage = buildCoverage(g, self.glyphMap) | |
subtable.InputCoverage.append(coverage) | |
def setCoverage_(self, glyphs, subtable): | |
subtable.GlyphCount = len(glyphs) | |
subtable.Coverage = [] | |
for g in glyphs: | |
coverage = buildCoverage(g, self.glyphMap) | |
subtable.Coverage.append(coverage) | |
def build_subst_subtables(self, mapping, klass): | |
substitutions = [{}] | |
for key in mapping: | |
if key[0] == self.SUBTABLE_BREAK_: | |
substitutions.append({}) | |
else: | |
substitutions[-1][key] = mapping[key] | |
subtables = [klass(s) for s in substitutions] | |
return subtables | |
def add_subtable_break(self, location): | |
"""Add an explicit subtable break. | |
Args: | |
location: A string or tuple representing the location in the | |
original source which produced this break, or ``None`` if | |
no location is provided. | |
""" | |
log.warning( | |
OpenTypeLibError( | |
'unsupported "subtable" statement for lookup type', location | |
) | |
) | |
class AlternateSubstBuilder(LookupBuilder): | |
"""Builds an Alternate Substitution (GSUB3) lookup. | |
Users are expected to manually add alternate glyph substitutions to | |
the ``alternates`` attribute after the object has been initialized, | |
e.g.:: | |
builder.alternates["A"] = ["A.alt1", "A.alt2"] | |
Attributes: | |
font (``fontTools.TTLib.TTFont``): A font object. | |
location: A string or tuple representing the location in the original | |
source which produced this lookup. | |
alternates: An ordered dictionary of alternates, mapping glyph names | |
to a list of names of alternates. | |
lookupflag (int): The lookup's flag | |
markFilterSet: Either ``None`` if no mark filtering set is used, or | |
an integer representing the filtering set to be used for this | |
lookup. If a mark filtering set is provided, | |
`LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's | |
flags. | |
""" | |
def __init__(self, font, location): | |
LookupBuilder.__init__(self, font, location, "GSUB", 3) | |
self.alternates = OrderedDict() | |
def equals(self, other): | |
return LookupBuilder.equals(self, other) and self.alternates == other.alternates | |
def build(self): | |
"""Build the lookup. | |
Returns: | |
An ``otTables.Lookup`` object representing the alternate | |
substitution lookup. | |
""" | |
subtables = self.build_subst_subtables( | |
self.alternates, buildAlternateSubstSubtable | |
) | |
return self.buildLookup_(subtables) | |
def getAlternateGlyphs(self): | |
return self.alternates | |
def add_subtable_break(self, location): | |
self.alternates[(self.SUBTABLE_BREAK_, location)] = self.SUBTABLE_BREAK_ | |
class ChainContextualRule( | |
namedtuple("ChainContextualRule", ["prefix", "glyphs", "suffix", "lookups"]) | |
): | |
def is_subtable_break(self): | |
return self.prefix == LookupBuilder.SUBTABLE_BREAK_ | |
class ChainContextualRuleset: | |
def __init__(self): | |
self.rules = [] | |
def addRule(self, rule): | |
self.rules.append(rule) | |
def hasPrefixOrSuffix(self): | |
# Do we have any prefixes/suffixes? If this is False for all | |
# rulesets, we can express the whole lookup as GPOS5/GSUB7. | |
for rule in self.rules: | |
if len(rule.prefix) > 0 or len(rule.suffix) > 0: | |
return True | |
return False | |
def hasAnyGlyphClasses(self): | |
# Do we use glyph classes anywhere in the rules? If this is False | |
# we can express this subtable as a Format 1. | |
for rule in self.rules: | |
for coverage in (rule.prefix, rule.glyphs, rule.suffix): | |
if any(len(x) > 1 for x in coverage): | |
return True | |
return False | |
def format2ClassDefs(self): | |
PREFIX, GLYPHS, SUFFIX = 0, 1, 2 | |
classDefBuilders = [] | |
for ix in [PREFIX, GLYPHS, SUFFIX]: | |
context = [] | |
for r in self.rules: | |
context.append(r[ix]) | |
classes = self._classBuilderForContext(context) | |
if not classes: | |
return None | |
classDefBuilders.append(classes) | |
return classDefBuilders | |
def _classBuilderForContext(self, context): | |
classdefbuilder = ClassDefBuilder(useClass0=False) | |
for position in context: | |
for glyphset in position: | |
glyphs = set(glyphset) | |
if not classdefbuilder.canAdd(glyphs): | |
return None | |
classdefbuilder.add(glyphs) | |
return classdefbuilder | |
class ChainContextualBuilder(LookupBuilder): | |
def equals(self, other): | |
return LookupBuilder.equals(self, other) and self.rules == other.rules | |
def rulesets(self): | |
# Return a list of ChainContextRuleset objects, taking explicit | |
# subtable breaks into account | |
ruleset = [ChainContextualRuleset()] | |
for rule in self.rules: | |
if rule.is_subtable_break: | |
ruleset.append(ChainContextualRuleset()) | |
continue | |
ruleset[-1].addRule(rule) | |
# Squish any empty subtables | |
return [x for x in ruleset if len(x.rules) > 0] | |
def getCompiledSize_(self, subtables): | |
if not subtables: | |
return 0 | |
# We need to make a copy here because compiling | |
# modifies the subtable (finalizing formats etc.) | |
table = self.buildLookup_(copy.deepcopy(subtables)) | |
w = OTTableWriter() | |
table.compile(w, self.font) | |
size = len(w.getAllData()) | |
return size | |
def build(self): | |
"""Build the lookup. | |
Returns: | |
An ``otTables.Lookup`` object representing the chained | |
contextual positioning lookup. | |
""" | |
subtables = [] | |
rulesets = self.rulesets() | |
chaining = any(ruleset.hasPrefixOrSuffix for ruleset in rulesets) | |
# https://github.com/fonttools/fonttools/issues/2539 | |
# | |
# Unfortunately, as of 2022-03-07, Apple's CoreText renderer does not | |
# correctly process GPOS7 lookups, so for now we force contextual | |
# positioning lookups to be chaining (GPOS8). | |
# | |
# This seems to be fixed as of macOS 13.2, but we keep disabling this | |
# for now until we are no longer concerned about old macOS versions. | |
# But we allow people to opt-out of this with the config key below. | |
write_gpos7 = self.font.cfg.get("fontTools.otlLib.builder:WRITE_GPOS7") | |
# horrible separation of concerns breach | |
if not write_gpos7 and self.subtable_type == "Pos": | |
chaining = True | |
for ruleset in rulesets: | |
# Determine format strategy. We try to build formats 1, 2 and 3 | |
# subtables and then work out which is best. candidates list holds | |
# the subtables in each format for this ruleset (including a dummy | |
# "format 0" to make the addressing match the format numbers). | |
# We can always build a format 3 lookup by accumulating each of | |
# the rules into a list, so start with that. | |
candidates = [None, None, None, []] | |
for rule in ruleset.rules: | |
candidates[3].append(self.buildFormat3Subtable(rule, chaining)) | |
# Can we express the whole ruleset as a format 2 subtable? | |
classdefs = ruleset.format2ClassDefs() | |
if classdefs: | |
candidates[2] = [ | |
self.buildFormat2Subtable(ruleset, classdefs, chaining) | |
] | |
if not ruleset.hasAnyGlyphClasses: | |
candidates[1] = [self.buildFormat1Subtable(ruleset, chaining)] | |
candidates_by_size = [] | |
for i in [1, 2, 3]: | |
if candidates[i]: | |
try: | |
size = self.getCompiledSize_(candidates[i]) | |
except OTLOffsetOverflowError as e: | |
log.warning( | |
"Contextual format %i at %s overflowed (%s)" | |
% (i, str(self.location), e) | |
) | |
else: | |
candidates_by_size.append((size, candidates[i])) | |
if not candidates_by_size: | |
raise OpenTypeLibError("All candidates overflowed", self.location) | |
_min_size, winner = min(candidates_by_size, key=lambda x: x[0]) | |
subtables.extend(winner) | |
# If we are not chaining, lookup type will be automatically fixed by | |
# buildLookup_ | |
return self.buildLookup_(subtables) | |
def buildFormat1Subtable(self, ruleset, chaining=True): | |
st = self.newSubtable_(chaining=chaining) | |
st.Format = 1 | |
st.populateDefaults() | |
coverage = set() | |
rulesetsByFirstGlyph = {} | |
ruleAttr = self.ruleAttr_(format=1, chaining=chaining) | |
for rule in ruleset.rules: | |
ruleAsSubtable = self.newRule_(format=1, chaining=chaining) | |
if chaining: | |
ruleAsSubtable.BacktrackGlyphCount = len(rule.prefix) | |
ruleAsSubtable.LookAheadGlyphCount = len(rule.suffix) | |
ruleAsSubtable.Backtrack = [list(x)[0] for x in reversed(rule.prefix)] | |
ruleAsSubtable.LookAhead = [list(x)[0] for x in rule.suffix] | |
ruleAsSubtable.InputGlyphCount = len(rule.glyphs) | |
else: | |
ruleAsSubtable.GlyphCount = len(rule.glyphs) | |
ruleAsSubtable.Input = [list(x)[0] for x in rule.glyphs[1:]] | |
self.buildLookupList(rule, ruleAsSubtable) | |
firstGlyph = list(rule.glyphs[0])[0] | |
if firstGlyph not in rulesetsByFirstGlyph: | |
coverage.add(firstGlyph) | |
rulesetsByFirstGlyph[firstGlyph] = [] | |
rulesetsByFirstGlyph[firstGlyph].append(ruleAsSubtable) | |
st.Coverage = buildCoverage(coverage, self.glyphMap) | |
ruleSets = [] | |
for g in st.Coverage.glyphs: | |
ruleSet = self.newRuleSet_(format=1, chaining=chaining) | |
setattr(ruleSet, ruleAttr, rulesetsByFirstGlyph[g]) | |
setattr(ruleSet, f"{ruleAttr}Count", len(rulesetsByFirstGlyph[g])) | |
ruleSets.append(ruleSet) | |
setattr(st, self.ruleSetAttr_(format=1, chaining=chaining), ruleSets) | |
setattr( | |
st, self.ruleSetAttr_(format=1, chaining=chaining) + "Count", len(ruleSets) | |
) | |
return st | |
def buildFormat2Subtable(self, ruleset, classdefs, chaining=True): | |
st = self.newSubtable_(chaining=chaining) | |
st.Format = 2 | |
st.populateDefaults() | |
if chaining: | |
( | |
st.BacktrackClassDef, | |
st.InputClassDef, | |
st.LookAheadClassDef, | |
) = [c.build() for c in classdefs] | |
else: | |
st.ClassDef = classdefs[1].build() | |
inClasses = classdefs[1].classes() | |
classSets = [] | |
for _ in inClasses: | |
classSet = self.newRuleSet_(format=2, chaining=chaining) | |
classSets.append(classSet) | |
coverage = set() | |
classRuleAttr = self.ruleAttr_(format=2, chaining=chaining) | |
for rule in ruleset.rules: | |
ruleAsSubtable = self.newRule_(format=2, chaining=chaining) | |
if chaining: | |
ruleAsSubtable.BacktrackGlyphCount = len(rule.prefix) | |
ruleAsSubtable.LookAheadGlyphCount = len(rule.suffix) | |
# The glyphs in the rule may be list, tuple, odict_keys... | |
# Order is not important anyway because they are guaranteed | |
# to be members of the same class. | |
ruleAsSubtable.Backtrack = [ | |
st.BacktrackClassDef.classDefs[list(x)[0]] | |
for x in reversed(rule.prefix) | |
] | |
ruleAsSubtable.LookAhead = [ | |
st.LookAheadClassDef.classDefs[list(x)[0]] for x in rule.suffix | |
] | |
ruleAsSubtable.InputGlyphCount = len(rule.glyphs) | |
ruleAsSubtable.Input = [ | |
st.InputClassDef.classDefs[list(x)[0]] for x in rule.glyphs[1:] | |
] | |
setForThisRule = classSets[ | |
st.InputClassDef.classDefs[list(rule.glyphs[0])[0]] | |
] | |
else: | |
ruleAsSubtable.GlyphCount = len(rule.glyphs) | |
ruleAsSubtable.Class = [ # The spec calls this InputSequence | |
st.ClassDef.classDefs[list(x)[0]] for x in rule.glyphs[1:] | |
] | |
setForThisRule = classSets[ | |
st.ClassDef.classDefs[list(rule.glyphs[0])[0]] | |
] | |
self.buildLookupList(rule, ruleAsSubtable) | |
coverage |= set(rule.glyphs[0]) | |
getattr(setForThisRule, classRuleAttr).append(ruleAsSubtable) | |
setattr( | |
setForThisRule, | |
f"{classRuleAttr}Count", | |
getattr(setForThisRule, f"{classRuleAttr}Count") + 1, | |
) | |
setattr(st, self.ruleSetAttr_(format=2, chaining=chaining), classSets) | |
setattr( | |
st, self.ruleSetAttr_(format=2, chaining=chaining) + "Count", len(classSets) | |
) | |
st.Coverage = buildCoverage(coverage, self.glyphMap) | |
return st | |
def buildFormat3Subtable(self, rule, chaining=True): | |
st = self.newSubtable_(chaining=chaining) | |
st.Format = 3 | |
if chaining: | |
self.setBacktrackCoverage_(rule.prefix, st) | |
self.setLookAheadCoverage_(rule.suffix, st) | |
self.setInputCoverage_(rule.glyphs, st) | |
else: | |
self.setCoverage_(rule.glyphs, st) | |
self.buildLookupList(rule, st) | |
return st | |
def buildLookupList(self, rule, st): | |
for sequenceIndex, lookupList in enumerate(rule.lookups): | |
if lookupList is not None: | |
if not isinstance(lookupList, list): | |
# Can happen with synthesised lookups | |
lookupList = [lookupList] | |
for l in lookupList: | |
if l.lookup_index is None: | |
if isinstance(self, ChainContextPosBuilder): | |
other = "substitution" | |
else: | |
other = "positioning" | |
raise OpenTypeLibError( | |
"Missing index of the specified " | |
f"lookup, might be a {other} lookup", | |
self.location, | |
) | |
rec = self.newLookupRecord_(st) | |
rec.SequenceIndex = sequenceIndex | |
rec.LookupListIndex = l.lookup_index | |
def add_subtable_break(self, location): | |
self.rules.append( | |
ChainContextualRule( | |
self.SUBTABLE_BREAK_, | |
self.SUBTABLE_BREAK_, | |
self.SUBTABLE_BREAK_, | |
[self.SUBTABLE_BREAK_], | |
) | |
) | |
def newSubtable_(self, chaining=True): | |
subtablename = f"Context{self.subtable_type}" | |
if chaining: | |
subtablename = "Chain" + subtablename | |
st = getattr(ot, subtablename)() # ot.ChainContextPos()/ot.ChainSubst()/etc. | |
setattr(st, f"{self.subtable_type}Count", 0) | |
setattr(st, f"{self.subtable_type}LookupRecord", []) | |
return st | |
# Format 1 and format 2 GSUB5/GSUB6/GPOS7/GPOS8 rulesets and rules form a family: | |
# | |
# format 1 ruleset format 1 rule format 2 ruleset format 2 rule | |
# GSUB5 SubRuleSet SubRule SubClassSet SubClassRule | |
# GSUB6 ChainSubRuleSet ChainSubRule ChainSubClassSet ChainSubClassRule | |
# GPOS7 PosRuleSet PosRule PosClassSet PosClassRule | |
# GPOS8 ChainPosRuleSet ChainPosRule ChainPosClassSet ChainPosClassRule | |
# | |
# The following functions generate the attribute names and subtables according | |
# to this naming convention. | |
def ruleSetAttr_(self, format=1, chaining=True): | |
if format == 1: | |
formatType = "Rule" | |
elif format == 2: | |
formatType = "Class" | |
else: | |
raise AssertionError(formatType) | |
subtablename = f"{self.subtable_type[0:3]}{formatType}Set" # Sub, not Subst. | |
if chaining: | |
subtablename = "Chain" + subtablename | |
return subtablename | |
def ruleAttr_(self, format=1, chaining=True): | |
if format == 1: | |
formatType = "" | |
elif format == 2: | |
formatType = "Class" | |
else: | |
raise AssertionError(formatType) | |
subtablename = f"{self.subtable_type[0:3]}{formatType}Rule" # Sub, not Subst. | |
if chaining: | |
subtablename = "Chain" + subtablename | |
return subtablename | |
def newRuleSet_(self, format=1, chaining=True): | |
st = getattr( | |
ot, self.ruleSetAttr_(format, chaining) | |
)() # ot.ChainPosRuleSet()/ot.SubRuleSet()/etc. | |
st.populateDefaults() | |
return st | |
def newRule_(self, format=1, chaining=True): | |
st = getattr( | |
ot, self.ruleAttr_(format, chaining) | |
)() # ot.ChainPosClassRule()/ot.SubClassRule()/etc. | |
st.populateDefaults() | |
return st | |
def attachSubtableWithCount_( | |
self, st, subtable_name, count_name, existing=None, index=None, chaining=False | |
): | |
if chaining: | |
subtable_name = "Chain" + subtable_name | |
count_name = "Chain" + count_name | |
if not hasattr(st, count_name): | |
setattr(st, count_name, 0) | |
setattr(st, subtable_name, []) | |
if existing: | |
new_subtable = existing | |
else: | |
# Create a new, empty subtable from otTables | |
new_subtable = getattr(ot, subtable_name)() | |
setattr(st, count_name, getattr(st, count_name) + 1) | |
if index: | |
getattr(st, subtable_name).insert(index, new_subtable) | |
else: | |
getattr(st, subtable_name).append(new_subtable) | |
return new_subtable | |
def newLookupRecord_(self, st): | |
return self.attachSubtableWithCount_( | |
st, | |
f"{self.subtable_type}LookupRecord", | |
f"{self.subtable_type}Count", | |
chaining=False, | |
) # Oddly, it isn't ChainSubstLookupRecord | |
class ChainContextPosBuilder(ChainContextualBuilder): | |
"""Builds a Chained Contextual Positioning (GPOS8) lookup. | |
Users are expected to manually add rules to the ``rules`` attribute after | |
the object has been initialized, e.g.:: | |
# pos [A B] [C D] x' lookup lu1 y' z' lookup lu2 E; | |
prefix = [ ["A", "B"], ["C", "D"] ] | |
suffix = [ ["E"] ] | |
glyphs = [ ["x"], ["y"], ["z"] ] | |
lookups = [ [lu1], None, [lu2] ] | |
builder.rules.append( (prefix, glyphs, suffix, lookups) ) | |
Attributes: | |
font (``fontTools.TTLib.TTFont``): A font object. | |
location: A string or tuple representing the location in the original | |
source which produced this lookup. | |
rules: A list of tuples representing the rules in this lookup. | |
lookupflag (int): The lookup's flag | |
markFilterSet: Either ``None`` if no mark filtering set is used, or | |
an integer representing the filtering set to be used for this | |
lookup. If a mark filtering set is provided, | |
`LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's | |
flags. | |
""" | |
def __init__(self, font, location): | |
LookupBuilder.__init__(self, font, location, "GPOS", 8) | |
self.rules = [] | |
self.subtable_type = "Pos" | |
def find_chainable_single_pos(self, lookups, glyphs, value): | |
"""Helper for add_single_pos_chained_()""" | |
res = None | |
for lookup in lookups[::-1]: | |
if lookup == self.SUBTABLE_BREAK_: | |
return res | |
if isinstance(lookup, SinglePosBuilder) and all( | |
lookup.can_add(glyph, value) for glyph in glyphs | |
): | |
res = lookup | |
return res | |
class ChainContextSubstBuilder(ChainContextualBuilder): | |
"""Builds a Chained Contextual Substitution (GSUB6) lookup. | |
Users are expected to manually add rules to the ``rules`` attribute after | |
the object has been initialized, e.g.:: | |
# sub [A B] [C D] x' lookup lu1 y' z' lookup lu2 E; | |
prefix = [ ["A", "B"], ["C", "D"] ] | |
suffix = [ ["E"] ] | |
glyphs = [ ["x"], ["y"], ["z"] ] | |
lookups = [ [lu1], None, [lu2] ] | |
builder.rules.append( (prefix, glyphs, suffix, lookups) ) | |
Attributes: | |
font (``fontTools.TTLib.TTFont``): A font object. | |
location: A string or tuple representing the location in the original | |
source which produced this lookup. | |
rules: A list of tuples representing the rules in this lookup. | |
lookupflag (int): The lookup's flag | |
markFilterSet: Either ``None`` if no mark filtering set is used, or | |
an integer representing the filtering set to be used for this | |
lookup. If a mark filtering set is provided, | |
`LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's | |
flags. | |
""" | |
def __init__(self, font, location): | |
LookupBuilder.__init__(self, font, location, "GSUB", 6) | |
self.rules = [] # (prefix, input, suffix, lookups) | |
self.subtable_type = "Subst" | |
def getAlternateGlyphs(self): | |
result = {} | |
for rule in self.rules: | |
if rule.is_subtable_break: | |
continue | |
for lookups in rule.lookups: | |
if not isinstance(lookups, list): | |
lookups = [lookups] | |
for lookup in lookups: | |
if lookup is not None: | |
alts = lookup.getAlternateGlyphs() | |
for glyph, replacements in alts.items(): | |
alts_for_glyph = result.setdefault(glyph, []) | |
alts_for_glyph.extend( | |
g for g in replacements if g not in alts_for_glyph | |
) | |
return result | |
def find_chainable_single_subst(self, mapping): | |
"""Helper for add_single_subst_chained_()""" | |
res = None | |
for rule in self.rules[::-1]: | |
if rule.is_subtable_break: | |
return res | |
for sub in rule.lookups: | |
if isinstance(sub, SingleSubstBuilder) and not any( | |
g in mapping and mapping[g] != sub.mapping[g] for g in sub.mapping | |
): | |
res = sub | |
return res | |
class LigatureSubstBuilder(LookupBuilder): | |
"""Builds a Ligature Substitution (GSUB4) lookup. | |
Users are expected to manually add ligatures to the ``ligatures`` | |
attribute after the object has been initialized, e.g.:: | |
# sub f i by f_i; | |
builder.ligatures[("f","f","i")] = "f_f_i" | |
Attributes: | |
font (``fontTools.TTLib.TTFont``): A font object. | |
location: A string or tuple representing the location in the original | |
source which produced this lookup. | |
ligatures: An ordered dictionary mapping a tuple of glyph names to the | |
ligature glyphname. | |
lookupflag (int): The lookup's flag | |
markFilterSet: Either ``None`` if no mark filtering set is used, or | |
an integer representing the filtering set to be used for this | |
lookup. If a mark filtering set is provided, | |
`LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's | |
flags. | |
""" | |
def __init__(self, font, location): | |
LookupBuilder.__init__(self, font, location, "GSUB", 4) | |
self.ligatures = OrderedDict() # {('f','f','i'): 'f_f_i'} | |
def equals(self, other): | |
return LookupBuilder.equals(self, other) and self.ligatures == other.ligatures | |
def build(self): | |
"""Build the lookup. | |
Returns: | |
An ``otTables.Lookup`` object representing the ligature | |
substitution lookup. | |
""" | |
subtables = self.build_subst_subtables( | |
self.ligatures, buildLigatureSubstSubtable | |
) | |
return self.buildLookup_(subtables) | |
def add_subtable_break(self, location): | |
self.ligatures[(self.SUBTABLE_BREAK_, location)] = self.SUBTABLE_BREAK_ | |
class MultipleSubstBuilder(LookupBuilder): | |
"""Builds a Multiple Substitution (GSUB2) lookup. | |
Users are expected to manually add substitutions to the ``mapping`` | |
attribute after the object has been initialized, e.g.:: | |
# sub uni06C0 by uni06D5.fina hamza.above; | |
builder.mapping["uni06C0"] = [ "uni06D5.fina", "hamza.above"] | |
Attributes: | |
font (``fontTools.TTLib.TTFont``): A font object. | |
location: A string or tuple representing the location in the original | |
source which produced this lookup. | |
mapping: An ordered dictionary mapping a glyph name to a list of | |
substituted glyph names. | |
lookupflag (int): The lookup's flag | |
markFilterSet: Either ``None`` if no mark filtering set is used, or | |
an integer representing the filtering set to be used for this | |
lookup. If a mark filtering set is provided, | |
`LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's | |
flags. | |
""" | |
def __init__(self, font, location): | |
LookupBuilder.__init__(self, font, location, "GSUB", 2) | |
self.mapping = OrderedDict() | |
def equals(self, other): | |
return LookupBuilder.equals(self, other) and self.mapping == other.mapping | |
def build(self): | |
subtables = self.build_subst_subtables(self.mapping, buildMultipleSubstSubtable) | |
return self.buildLookup_(subtables) | |
def add_subtable_break(self, location): | |
self.mapping[(self.SUBTABLE_BREAK_, location)] = self.SUBTABLE_BREAK_ | |
class CursivePosBuilder(LookupBuilder): | |
"""Builds a Cursive Positioning (GPOS3) lookup. | |
Attributes: | |
font (``fontTools.TTLib.TTFont``): A font object. | |
location: A string or tuple representing the location in the original | |
source which produced this lookup. | |
attachments: An ordered dictionary mapping a glyph name to a two-element | |
tuple of ``otTables.Anchor`` objects. | |
lookupflag (int): The lookup's flag | |
markFilterSet: Either ``None`` if no mark filtering set is used, or | |
an integer representing the filtering set to be used for this | |
lookup. If a mark filtering set is provided, | |
`LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's | |
flags. | |
""" | |
def __init__(self, font, location): | |
LookupBuilder.__init__(self, font, location, "GPOS", 3) | |
self.attachments = {} | |
def equals(self, other): | |
return ( | |
LookupBuilder.equals(self, other) and self.attachments == other.attachments | |
) | |
def add_attachment(self, location, glyphs, entryAnchor, exitAnchor): | |
"""Adds attachment information to the cursive positioning lookup. | |
Args: | |
location: A string or tuple representing the location in the | |
original source which produced this lookup. (Unused.) | |
glyphs: A list of glyph names sharing these entry and exit | |
anchor locations. | |
entryAnchor: A ``otTables.Anchor`` object representing the | |
entry anchor, or ``None`` if no entry anchor is present. | |
exitAnchor: A ``otTables.Anchor`` object representing the | |
exit anchor, or ``None`` if no exit anchor is present. | |
""" | |
for glyph in glyphs: | |
self.attachments[glyph] = (entryAnchor, exitAnchor) | |
def build(self): | |
"""Build the lookup. | |
Returns: | |
An ``otTables.Lookup`` object representing the cursive | |
positioning lookup. | |
""" | |
st = buildCursivePosSubtable(self.attachments, self.glyphMap) | |
return self.buildLookup_([st]) | |
class MarkBasePosBuilder(LookupBuilder): | |
"""Builds a Mark-To-Base Positioning (GPOS4) lookup. | |
Users are expected to manually add marks and bases to the ``marks`` | |
and ``bases`` attributes after the object has been initialized, e.g.:: | |
builder.marks["acute"] = (0, a1) | |
builder.marks["grave"] = (0, a1) | |
builder.marks["cedilla"] = (1, a2) | |
builder.bases["a"] = {0: a3, 1: a5} | |
builder.bases["b"] = {0: a4, 1: a5} | |
Attributes: | |
font (``fontTools.TTLib.TTFont``): A font object. | |
location: A string or tuple representing the location in the original | |
source which produced this lookup. | |
marks: An dictionary mapping a glyph name to a two-element | |
tuple containing a mark class ID and ``otTables.Anchor`` object. | |
bases: An dictionary mapping a glyph name to a dictionary of | |
mark class IDs and ``otTables.Anchor`` object. | |
lookupflag (int): The lookup's flag | |
markFilterSet: Either ``None`` if no mark filtering set is used, or | |
an integer representing the filtering set to be used for this | |
lookup. If a mark filtering set is provided, | |
`LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's | |
flags. | |
""" | |
def __init__(self, font, location): | |
LookupBuilder.__init__(self, font, location, "GPOS", 4) | |
self.marks = {} # glyphName -> (markClassName, anchor) | |
self.bases = {} # glyphName -> {markClassName: anchor} | |
def equals(self, other): | |
return ( | |
LookupBuilder.equals(self, other) | |
and self.marks == other.marks | |
and self.bases == other.bases | |
) | |
def inferGlyphClasses(self): | |
result = {glyph: 1 for glyph in self.bases} | |
result.update({glyph: 3 for glyph in self.marks}) | |
return result | |
def build(self): | |
"""Build the lookup. | |
Returns: | |
An ``otTables.Lookup`` object representing the mark-to-base | |
positioning lookup. | |
""" | |
markClasses = self.buildMarkClasses_(self.marks) | |
marks = {} | |
for mark, (mc, anchor) in self.marks.items(): | |
if mc not in markClasses: | |
raise ValueError( | |
"Mark class %s not found for mark glyph %s" % (mc, mark) | |
) | |
marks[mark] = (markClasses[mc], anchor) | |
bases = {} | |
for glyph, anchors in self.bases.items(): | |
bases[glyph] = {} | |
for mc, anchor in anchors.items(): | |
if mc not in markClasses: | |
raise ValueError( | |
"Mark class %s not found for base glyph %s" % (mc, glyph) | |
) | |
bases[glyph][markClasses[mc]] = anchor | |
subtables = buildMarkBasePos(marks, bases, self.glyphMap) | |
return self.buildLookup_(subtables) | |
class MarkLigPosBuilder(LookupBuilder): | |
"""Builds a Mark-To-Ligature Positioning (GPOS5) lookup. | |
Users are expected to manually add marks and bases to the ``marks`` | |
and ``ligatures`` attributes after the object has been initialized, e.g.:: | |
builder.marks["acute"] = (0, a1) | |
builder.marks["grave"] = (0, a1) | |
builder.marks["cedilla"] = (1, a2) | |
builder.ligatures["f_i"] = [ | |
{ 0: a3, 1: a5 }, # f | |
{ 0: a4, 1: a5 } # i | |
] | |
Attributes: | |
font (``fontTools.TTLib.TTFont``): A font object. | |
location: A string or tuple representing the location in the original | |
source which produced this lookup. | |
marks: An dictionary mapping a glyph name to a two-element | |
tuple containing a mark class ID and ``otTables.Anchor`` object. | |
ligatures: An dictionary mapping a glyph name to an array with one | |
element for each ligature component. Each array element should be | |
a dictionary mapping mark class IDs to ``otTables.Anchor`` objects. | |
lookupflag (int): The lookup's flag | |
markFilterSet: Either ``None`` if no mark filtering set is used, or | |
an integer representing the filtering set to be used for this | |
lookup. If a mark filtering set is provided, | |
`LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's | |
flags. | |
""" | |
def __init__(self, font, location): | |
LookupBuilder.__init__(self, font, location, "GPOS", 5) | |
self.marks = {} # glyphName -> (markClassName, anchor) | |
self.ligatures = {} # glyphName -> [{markClassName: anchor}, ...] | |
def equals(self, other): | |
return ( | |
LookupBuilder.equals(self, other) | |
and self.marks == other.marks | |
and self.ligatures == other.ligatures | |
) | |
def inferGlyphClasses(self): | |
result = {glyph: 2 for glyph in self.ligatures} | |
result.update({glyph: 3 for glyph in self.marks}) | |
return result | |
def build(self): | |
"""Build the lookup. | |
Returns: | |
An ``otTables.Lookup`` object representing the mark-to-ligature | |
positioning lookup. | |
""" | |
markClasses = self.buildMarkClasses_(self.marks) | |
marks = { | |
mark: (markClasses[mc], anchor) for mark, (mc, anchor) in self.marks.items() | |
} | |
ligs = {} | |
for lig, components in self.ligatures.items(): | |
ligs[lig] = [] | |
for c in components: | |
ligs[lig].append({markClasses[mc]: a for mc, a in c.items()}) | |
subtables = buildMarkLigPos(marks, ligs, self.glyphMap) | |
return self.buildLookup_(subtables) | |
class MarkMarkPosBuilder(LookupBuilder): | |
"""Builds a Mark-To-Mark Positioning (GPOS6) lookup. | |
Users are expected to manually add marks and bases to the ``marks`` | |
and ``baseMarks`` attributes after the object has been initialized, e.g.:: | |
builder.marks["acute"] = (0, a1) | |
builder.marks["grave"] = (0, a1) | |
builder.marks["cedilla"] = (1, a2) | |
builder.baseMarks["acute"] = {0: a3} | |
Attributes: | |
font (``fontTools.TTLib.TTFont``): A font object. | |
location: A string or tuple representing the location in the original | |
source which produced this lookup. | |
marks: An dictionary mapping a glyph name to a two-element | |
tuple containing a mark class ID and ``otTables.Anchor`` object. | |
baseMarks: An dictionary mapping a glyph name to a dictionary | |
containing one item: a mark class ID and a ``otTables.Anchor`` object. | |
lookupflag (int): The lookup's flag | |
markFilterSet: Either ``None`` if no mark filtering set is used, or | |
an integer representing the filtering set to be used for this | |
lookup. If a mark filtering set is provided, | |
`LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's | |
flags. | |
""" | |
def __init__(self, font, location): | |
LookupBuilder.__init__(self, font, location, "GPOS", 6) | |
self.marks = {} # glyphName -> (markClassName, anchor) | |
self.baseMarks = {} # glyphName -> {markClassName: anchor} | |
def equals(self, other): | |
return ( | |
LookupBuilder.equals(self, other) | |
and self.marks == other.marks | |
and self.baseMarks == other.baseMarks | |
) | |
def inferGlyphClasses(self): | |
result = {glyph: 3 for glyph in self.baseMarks} | |
result.update({glyph: 3 for glyph in self.marks}) | |
return result | |
def build(self): | |
"""Build the lookup. | |
Returns: | |
An ``otTables.Lookup`` object representing the mark-to-mark | |
positioning lookup. | |
""" | |
markClasses = self.buildMarkClasses_(self.marks) | |
markClassList = sorted(markClasses.keys(), key=markClasses.get) | |
marks = { | |
mark: (markClasses[mc], anchor) for mark, (mc, anchor) in self.marks.items() | |
} | |
st = ot.MarkMarkPos() | |
st.Format = 1 | |
st.ClassCount = len(markClasses) | |
st.Mark1Coverage = buildCoverage(marks, self.glyphMap) | |
st.Mark2Coverage = buildCoverage(self.baseMarks, self.glyphMap) | |
st.Mark1Array = buildMarkArray(marks, self.glyphMap) | |
st.Mark2Array = ot.Mark2Array() | |
st.Mark2Array.Mark2Count = len(st.Mark2Coverage.glyphs) | |
st.Mark2Array.Mark2Record = [] | |
for base in st.Mark2Coverage.glyphs: | |
anchors = [self.baseMarks[base].get(mc) for mc in markClassList] | |
st.Mark2Array.Mark2Record.append(buildMark2Record(anchors)) | |
return self.buildLookup_([st]) | |
class ReverseChainSingleSubstBuilder(LookupBuilder): | |
"""Builds a Reverse Chaining Contextual Single Substitution (GSUB8) lookup. | |
Users are expected to manually add substitutions to the ``substitutions`` | |
attribute after the object has been initialized, e.g.:: | |
# reversesub [a e n] d' by d.alt; | |
prefix = [ ["a", "e", "n"] ] | |
suffix = [] | |
mapping = { "d": "d.alt" } | |
builder.substitutions.append( (prefix, suffix, mapping) ) | |
Attributes: | |
font (``fontTools.TTLib.TTFont``): A font object. | |
location: A string or tuple representing the location in the original | |
source which produced this lookup. | |
substitutions: A three-element tuple consisting of a prefix sequence, | |
a suffix sequence, and a dictionary of single substitutions. | |
lookupflag (int): The lookup's flag | |
markFilterSet: Either ``None`` if no mark filtering set is used, or | |
an integer representing the filtering set to be used for this | |
lookup. If a mark filtering set is provided, | |
`LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's | |
flags. | |
""" | |
def __init__(self, font, location): | |
LookupBuilder.__init__(self, font, location, "GSUB", 8) | |
self.rules = [] # (prefix, suffix, mapping) | |
def equals(self, other): | |
return LookupBuilder.equals(self, other) and self.rules == other.rules | |
def build(self): | |
"""Build the lookup. | |
Returns: | |
An ``otTables.Lookup`` object representing the chained | |
contextual substitution lookup. | |
""" | |
subtables = [] | |
for prefix, suffix, mapping in self.rules: | |
st = ot.ReverseChainSingleSubst() | |
st.Format = 1 | |
self.setBacktrackCoverage_(prefix, st) | |
self.setLookAheadCoverage_(suffix, st) | |
st.Coverage = buildCoverage(mapping.keys(), self.glyphMap) | |
st.GlyphCount = len(mapping) | |
st.Substitute = [mapping[g] for g in st.Coverage.glyphs] | |
subtables.append(st) | |
return self.buildLookup_(subtables) | |
def add_subtable_break(self, location): | |
# Nothing to do here, each substitution is in its own subtable. | |
pass | |
class SingleSubstBuilder(LookupBuilder): | |
"""Builds a Single Substitution (GSUB1) lookup. | |
Users are expected to manually add substitutions to the ``mapping`` | |
attribute after the object has been initialized, e.g.:: | |
# sub x by y; | |
builder.mapping["x"] = "y" | |
Attributes: | |
font (``fontTools.TTLib.TTFont``): A font object. | |
location: A string or tuple representing the location in the original | |
source which produced this lookup. | |
mapping: A dictionary mapping a single glyph name to another glyph name. | |
lookupflag (int): The lookup's flag | |
markFilterSet: Either ``None`` if no mark filtering set is used, or | |
an integer representing the filtering set to be used for this | |
lookup. If a mark filtering set is provided, | |
`LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's | |
flags. | |
""" | |
def __init__(self, font, location): | |
LookupBuilder.__init__(self, font, location, "GSUB", 1) | |
self.mapping = OrderedDict() | |
def equals(self, other): | |
return LookupBuilder.equals(self, other) and self.mapping == other.mapping | |
def build(self): | |
"""Build the lookup. | |
Returns: | |
An ``otTables.Lookup`` object representing the multiple | |
substitution lookup. | |
""" | |
subtables = self.build_subst_subtables(self.mapping, buildSingleSubstSubtable) | |
return self.buildLookup_(subtables) | |
def getAlternateGlyphs(self): | |
return {glyph: [repl] for glyph, repl in self.mapping.items()} | |
def add_subtable_break(self, location): | |
self.mapping[(self.SUBTABLE_BREAK_, location)] = self.SUBTABLE_BREAK_ | |
class ClassPairPosSubtableBuilder(object): | |
"""Builds class-based Pair Positioning (GPOS2 format 2) subtables. | |
Note that this does *not* build a GPOS2 ``otTables.Lookup`` directly, | |
but builds a list of ``otTables.PairPos`` subtables. It is used by the | |
:class:`PairPosBuilder` below. | |
Attributes: | |
builder (PairPosBuilder): A pair positioning lookup builder. | |
""" | |
def __init__(self, builder): | |
self.builder_ = builder | |
self.classDef1_, self.classDef2_ = None, None | |
self.values_ = {} # (glyphclass1, glyphclass2) --> (value1, value2) | |
self.forceSubtableBreak_ = False | |
self.subtables_ = [] | |
def addPair(self, gc1, value1, gc2, value2): | |
"""Add a pair positioning rule. | |
Args: | |
gc1: A set of glyph names for the "left" glyph | |
value1: An ``otTables.ValueRecord`` object for the left glyph's | |
positioning. | |
gc2: A set of glyph names for the "right" glyph | |
value2: An ``otTables.ValueRecord`` object for the right glyph's | |
positioning. | |
""" | |
mergeable = ( | |
not self.forceSubtableBreak_ | |
and self.classDef1_ is not None | |
and self.classDef1_.canAdd(gc1) | |
and self.classDef2_ is not None | |
and self.classDef2_.canAdd(gc2) | |
) | |
if not mergeable: | |
self.flush_() | |
self.classDef1_ = ClassDefBuilder(useClass0=True) | |
self.classDef2_ = ClassDefBuilder(useClass0=False) | |
self.values_ = {} | |
self.classDef1_.add(gc1) | |
self.classDef2_.add(gc2) | |
self.values_[(gc1, gc2)] = (value1, value2) | |
def addSubtableBreak(self): | |
"""Add an explicit subtable break at this point.""" | |
self.forceSubtableBreak_ = True | |
def subtables(self): | |
"""Return the list of ``otTables.PairPos`` subtables constructed.""" | |
self.flush_() | |
return self.subtables_ | |
def flush_(self): | |
if self.classDef1_ is None or self.classDef2_ is None: | |
return | |
st = buildPairPosClassesSubtable(self.values_, self.builder_.glyphMap) | |
if st.Coverage is None: | |
return | |
self.subtables_.append(st) | |
self.forceSubtableBreak_ = False | |
class PairPosBuilder(LookupBuilder): | |
"""Builds a Pair Positioning (GPOS2) lookup. | |
Attributes: | |
font (``fontTools.TTLib.TTFont``): A font object. | |
location: A string or tuple representing the location in the original | |
source which produced this lookup. | |
pairs: An array of class-based pair positioning tuples. Usually | |
manipulated with the :meth:`addClassPair` method below. | |
glyphPairs: A dictionary mapping a tuple of glyph names to a tuple | |
of ``otTables.ValueRecord`` objects. Usually manipulated with the | |
:meth:`addGlyphPair` method below. | |
lookupflag (int): The lookup's flag | |
markFilterSet: Either ``None`` if no mark filtering set is used, or | |
an integer representing the filtering set to be used for this | |
lookup. If a mark filtering set is provided, | |
`LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's | |
flags. | |
""" | |
def __init__(self, font, location): | |
LookupBuilder.__init__(self, font, location, "GPOS", 2) | |
self.pairs = [] # [(gc1, value1, gc2, value2)*] | |
self.glyphPairs = {} # (glyph1, glyph2) --> (value1, value2) | |
self.locations = {} # (gc1, gc2) --> (filepath, line, column) | |
def addClassPair(self, location, glyphclass1, value1, glyphclass2, value2): | |
"""Add a class pair positioning rule to the current lookup. | |
Args: | |
location: A string or tuple representing the location in the | |
original source which produced this rule. Unused. | |
glyphclass1: A set of glyph names for the "left" glyph in the pair. | |
value1: A ``otTables.ValueRecord`` for positioning the left glyph. | |
glyphclass2: A set of glyph names for the "right" glyph in the pair. | |
value2: A ``otTables.ValueRecord`` for positioning the right glyph. | |
""" | |
self.pairs.append((glyphclass1, value1, glyphclass2, value2)) | |
def addGlyphPair(self, location, glyph1, value1, glyph2, value2): | |
"""Add a glyph pair positioning rule to the current lookup. | |
Args: | |
location: A string or tuple representing the location in the | |
original source which produced this rule. | |
glyph1: A glyph name for the "left" glyph in the pair. | |
value1: A ``otTables.ValueRecord`` for positioning the left glyph. | |
glyph2: A glyph name for the "right" glyph in the pair. | |
value2: A ``otTables.ValueRecord`` for positioning the right glyph. | |
""" | |
key = (glyph1, glyph2) | |
oldValue = self.glyphPairs.get(key, None) | |
if oldValue is not None: | |
# the Feature File spec explicitly allows specific pairs generated | |
# by an 'enum' rule to be overridden by preceding single pairs | |
otherLoc = self.locations[key] | |
log.debug( | |
"Already defined position for pair %s %s at %s; " | |
"choosing the first value", | |
glyph1, | |
glyph2, | |
otherLoc, | |
) | |
else: | |
self.glyphPairs[key] = (value1, value2) | |
self.locations[key] = location | |
def add_subtable_break(self, location): | |
self.pairs.append( | |
( | |
self.SUBTABLE_BREAK_, | |
self.SUBTABLE_BREAK_, | |
self.SUBTABLE_BREAK_, | |
self.SUBTABLE_BREAK_, | |
) | |
) | |
def equals(self, other): | |
return ( | |
LookupBuilder.equals(self, other) | |
and self.glyphPairs == other.glyphPairs | |
and self.pairs == other.pairs | |
) | |
def build(self): | |
"""Build the lookup. | |
Returns: | |
An ``otTables.Lookup`` object representing the pair positioning | |
lookup. | |
""" | |
builders = {} | |
builder = ClassPairPosSubtableBuilder(self) | |
for glyphclass1, value1, glyphclass2, value2 in self.pairs: | |
if glyphclass1 is self.SUBTABLE_BREAK_: | |
builder.addSubtableBreak() | |
continue | |
builder.addPair(glyphclass1, value1, glyphclass2, value2) | |
subtables = [] | |
if self.glyphPairs: | |
subtables.extend(buildPairPosGlyphs(self.glyphPairs, self.glyphMap)) | |
subtables.extend(builder.subtables()) | |
lookup = self.buildLookup_(subtables) | |
# Compact the lookup | |
# This is a good moment to do it because the compaction should create | |
# smaller subtables, which may prevent overflows from happening. | |
# Keep reading the value from the ENV until ufo2ft switches to the config system | |
level = self.font.cfg.get( | |
"fontTools.otlLib.optimize.gpos:COMPRESSION_LEVEL", | |
default=_compression_level_from_env(), | |
) | |
if level != 0: | |
log.info("Compacting GPOS...") | |
compact_lookup(self.font, level, lookup) | |
return lookup | |
class SinglePosBuilder(LookupBuilder): | |
"""Builds a Single Positioning (GPOS1) lookup. | |
Attributes: | |
font (``fontTools.TTLib.TTFont``): A font object. | |
location: A string or tuple representing the location in the original | |
source which produced this lookup. | |
mapping: A dictionary mapping a glyph name to a ``otTables.ValueRecord`` | |
objects. Usually manipulated with the :meth:`add_pos` method below. | |
lookupflag (int): The lookup's flag | |
markFilterSet: Either ``None`` if no mark filtering set is used, or | |
an integer representing the filtering set to be used for this | |
lookup. If a mark filtering set is provided, | |
`LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's | |
flags. | |
""" | |
def __init__(self, font, location): | |
LookupBuilder.__init__(self, font, location, "GPOS", 1) | |
self.locations = {} # glyph -> (filename, line, column) | |
self.mapping = {} # glyph -> ot.ValueRecord | |
def add_pos(self, location, glyph, otValueRecord): | |
"""Add a single positioning rule. | |
Args: | |
location: A string or tuple representing the location in the | |
original source which produced this lookup. | |
glyph: A glyph name. | |
otValueRection: A ``otTables.ValueRecord`` used to position the | |
glyph. | |
""" | |
if not self.can_add(glyph, otValueRecord): | |
otherLoc = self.locations[glyph] | |
raise OpenTypeLibError( | |
'Already defined different position for glyph "%s" at %s' | |
% (glyph, otherLoc), | |
location, | |
) | |
if otValueRecord: | |
self.mapping[glyph] = otValueRecord | |
self.locations[glyph] = location | |
def can_add(self, glyph, value): | |
assert isinstance(value, ValueRecord) | |
curValue = self.mapping.get(glyph) | |
return curValue is None or curValue == value | |
def equals(self, other): | |
return LookupBuilder.equals(self, other) and self.mapping == other.mapping | |
def build(self): | |
"""Build the lookup. | |
Returns: | |
An ``otTables.Lookup`` object representing the single positioning | |
lookup. | |
""" | |
subtables = buildSinglePos(self.mapping, self.glyphMap) | |
return self.buildLookup_(subtables) | |
# GSUB | |
def buildSingleSubstSubtable(mapping): | |
"""Builds a single substitution (GSUB1) subtable. | |
Note that if you are implementing a layout compiler, you may find it more | |
flexible to use | |
:py:class:`fontTools.otlLib.lookupBuilders.SingleSubstBuilder` instead. | |
Args: | |
mapping: A dictionary mapping input glyph names to output glyph names. | |
Returns: | |
An ``otTables.SingleSubst`` object, or ``None`` if the mapping dictionary | |
is empty. | |
""" | |
if not mapping: | |
return None | |
self = ot.SingleSubst() | |
self.mapping = dict(mapping) | |
return self | |
def buildMultipleSubstSubtable(mapping): | |
"""Builds a multiple substitution (GSUB2) subtable. | |
Note that if you are implementing a layout compiler, you may find it more | |
flexible to use | |
:py:class:`fontTools.otlLib.lookupBuilders.MultipleSubstBuilder` instead. | |
Example:: | |
# sub uni06C0 by uni06D5.fina hamza.above | |
# sub uni06C2 by uni06C1.fina hamza.above; | |
subtable = buildMultipleSubstSubtable({ | |
"uni06C0": [ "uni06D5.fina", "hamza.above"], | |
"uni06C2": [ "uni06D1.fina", "hamza.above"] | |
}) | |
Args: | |
mapping: A dictionary mapping input glyph names to a list of output | |
glyph names. | |
Returns: | |
An ``otTables.MultipleSubst`` object or ``None`` if the mapping dictionary | |
is empty. | |
""" | |
if not mapping: | |
return None | |
self = ot.MultipleSubst() | |
self.mapping = dict(mapping) | |
return self | |
def buildAlternateSubstSubtable(mapping): | |
"""Builds an alternate substitution (GSUB3) subtable. | |
Note that if you are implementing a layout compiler, you may find it more | |
flexible to use | |
:py:class:`fontTools.otlLib.lookupBuilders.AlternateSubstBuilder` instead. | |
Args: | |
mapping: A dictionary mapping input glyph names to a list of output | |
glyph names. | |
Returns: | |
An ``otTables.AlternateSubst`` object or ``None`` if the mapping dictionary | |
is empty. | |
""" | |
if not mapping: | |
return None | |
self = ot.AlternateSubst() | |
self.alternates = dict(mapping) | |
return self | |
def buildLigatureSubstSubtable(mapping): | |
"""Builds a ligature substitution (GSUB4) subtable. | |
Note that if you are implementing a layout compiler, you may find it more | |
flexible to use | |
:py:class:`fontTools.otlLib.lookupBuilders.LigatureSubstBuilder` instead. | |
Example:: | |
# sub f f i by f_f_i; | |
# sub f i by f_i; | |
subtable = buildLigatureSubstSubtable({ | |
("f", "f", "i"): "f_f_i", | |
("f", "i"): "f_i", | |
}) | |
Args: | |
mapping: A dictionary mapping tuples of glyph names to output | |
glyph names. | |
Returns: | |
An ``otTables.LigatureSubst`` object or ``None`` if the mapping dictionary | |
is empty. | |
""" | |
if not mapping: | |
return None | |
self = ot.LigatureSubst() | |
# The following single line can replace the rest of this function | |
# with fontTools >= 3.1: | |
# self.ligatures = dict(mapping) | |
self.ligatures = {} | |
for components in sorted(mapping.keys(), key=self._getLigatureSortKey): | |
ligature = ot.Ligature() | |
ligature.Component = components[1:] | |
ligature.CompCount = len(ligature.Component) + 1 | |
ligature.LigGlyph = mapping[components] | |
firstGlyph = components[0] | |
self.ligatures.setdefault(firstGlyph, []).append(ligature) | |
return self | |
# GPOS | |
def buildAnchor(x, y, point=None, deviceX=None, deviceY=None): | |
"""Builds an Anchor table. | |
This determines the appropriate anchor format based on the passed parameters. | |
Args: | |
x (int): X coordinate. | |
y (int): Y coordinate. | |
point (int): Index of glyph contour point, if provided. | |
deviceX (``otTables.Device``): X coordinate device table, if provided. | |
deviceY (``otTables.Device``): Y coordinate device table, if provided. | |
Returns: | |
An ``otTables.Anchor`` object. | |
""" | |
self = ot.Anchor() | |
self.XCoordinate, self.YCoordinate = x, y | |
self.Format = 1 | |
if point is not None: | |
self.AnchorPoint = point | |
self.Format = 2 | |
if deviceX is not None or deviceY is not None: | |
assert ( | |
self.Format == 1 | |
), "Either point, or both of deviceX/deviceY, must be None." | |
self.XDeviceTable = deviceX | |
self.YDeviceTable = deviceY | |
self.Format = 3 | |
return self | |
def buildBaseArray(bases, numMarkClasses, glyphMap): | |
"""Builds a base array record. | |
As part of building mark-to-base positioning rules, you will need to define | |
a ``BaseArray`` record, which "defines for each base glyph an array of | |
anchors, one for each mark class." This function builds the base array | |
subtable. | |
Example:: | |
bases = {"a": {0: a3, 1: a5}, "b": {0: a4, 1: a5}} | |
basearray = buildBaseArray(bases, 2, font.getReverseGlyphMap()) | |
Args: | |
bases (dict): A dictionary mapping anchors to glyphs; the keys being | |
glyph names, and the values being dictionaries mapping mark class ID | |
to the appropriate ``otTables.Anchor`` object used for attaching marks | |
of that class. | |
numMarkClasses (int): The total number of mark classes for which anchors | |
are defined. | |
glyphMap: a glyph name to ID map, typically returned from | |
``font.getReverseGlyphMap()``. | |
Returns: | |
An ``otTables.BaseArray`` object. | |
""" | |
self = ot.BaseArray() | |
self.BaseRecord = [] | |
for base in sorted(bases, key=glyphMap.__getitem__): | |
b = bases[base] | |
anchors = [b.get(markClass) for markClass in range(numMarkClasses)] | |
self.BaseRecord.append(buildBaseRecord(anchors)) | |
self.BaseCount = len(self.BaseRecord) | |
return self | |
def buildBaseRecord(anchors): | |
# [otTables.Anchor, otTables.Anchor, ...] --> otTables.BaseRecord | |
self = ot.BaseRecord() | |
self.BaseAnchor = anchors | |
return self | |
def buildComponentRecord(anchors): | |
"""Builds a component record. | |
As part of building mark-to-ligature positioning rules, you will need to | |
define ``ComponentRecord`` objects, which contain "an array of offsets... | |
to the Anchor tables that define all the attachment points used to attach | |
marks to the component." This function builds the component record. | |
Args: | |
anchors: A list of ``otTables.Anchor`` objects or ``None``. | |
Returns: | |
A ``otTables.ComponentRecord`` object or ``None`` if no anchors are | |
supplied. | |
""" | |
if not anchors: | |
return None | |
self = ot.ComponentRecord() | |
self.LigatureAnchor = anchors | |
return self | |
def buildCursivePosSubtable(attach, glyphMap): | |
"""Builds a cursive positioning (GPOS3) subtable. | |
Cursive positioning lookups are made up of a coverage table of glyphs, | |
and a set of ``EntryExitRecord`` records containing the anchors for | |
each glyph. This function builds the cursive positioning subtable. | |
Example:: | |
subtable = buildCursivePosSubtable({ | |
"AlifIni": (None, buildAnchor(0, 50)), | |
"BehMed": (buildAnchor(500,250), buildAnchor(0,50)), | |
# ... | |
}, font.getReverseGlyphMap()) | |
Args: | |
attach (dict): A mapping between glyph names and a tuple of two | |
``otTables.Anchor`` objects representing entry and exit anchors. | |
glyphMap: a glyph name to ID map, typically returned from | |
``font.getReverseGlyphMap()``. | |
Returns: | |
An ``otTables.CursivePos`` object, or ``None`` if the attachment | |
dictionary was empty. | |
""" | |
if not attach: | |
return None | |
self = ot.CursivePos() | |
self.Format = 1 | |
self.Coverage = buildCoverage(attach.keys(), glyphMap) | |
self.EntryExitRecord = [] | |
for glyph in self.Coverage.glyphs: | |
entryAnchor, exitAnchor = attach[glyph] | |
rec = ot.EntryExitRecord() | |
rec.EntryAnchor = entryAnchor | |
rec.ExitAnchor = exitAnchor | |
self.EntryExitRecord.append(rec) | |
self.EntryExitCount = len(self.EntryExitRecord) | |
return self | |
def buildDevice(deltas): | |
"""Builds a Device record as part of a ValueRecord or Anchor. | |
Device tables specify size-specific adjustments to value records | |
and anchors to reflect changes based on the resolution of the output. | |
For example, one could specify that an anchor's Y position should be | |
increased by 1 pixel when displayed at 8 pixels per em. This routine | |
builds device records. | |
Args: | |
deltas: A dictionary mapping pixels-per-em sizes to the delta | |
adjustment in pixels when the font is displayed at that size. | |
Returns: | |
An ``otTables.Device`` object if any deltas were supplied, or | |
``None`` otherwise. | |
""" | |
if not deltas: | |
return None | |
self = ot.Device() | |
keys = deltas.keys() | |
self.StartSize = startSize = min(keys) | |
self.EndSize = endSize = max(keys) | |
assert 0 <= startSize <= endSize | |
self.DeltaValue = deltaValues = [ | |
deltas.get(size, 0) for size in range(startSize, endSize + 1) | |
] | |
maxDelta = max(deltaValues) | |
minDelta = min(deltaValues) | |
assert minDelta > -129 and maxDelta < 128 | |
if minDelta > -3 and maxDelta < 2: | |
self.DeltaFormat = 1 | |
elif minDelta > -9 and maxDelta < 8: | |
self.DeltaFormat = 2 | |
else: | |
self.DeltaFormat = 3 | |
return self | |
def buildLigatureArray(ligs, numMarkClasses, glyphMap): | |
"""Builds a LigatureArray subtable. | |
As part of building a mark-to-ligature lookup, you will need to define | |
the set of anchors (for each mark class) on each component of the ligature | |
where marks can be attached. For example, for an Arabic divine name ligature | |
(lam lam heh), you may want to specify mark attachment positioning for | |
superior marks (fatha, etc.) and inferior marks (kasra, etc.) on each glyph | |
of the ligature. This routine builds the ligature array record. | |
Example:: | |
buildLigatureArray({ | |
"lam-lam-heh": [ | |
{ 0: superiorAnchor1, 1: inferiorAnchor1 }, # attach points for lam1 | |
{ 0: superiorAnchor2, 1: inferiorAnchor2 }, # attach points for lam2 | |
{ 0: superiorAnchor3, 1: inferiorAnchor3 }, # attach points for heh | |
] | |
}, 2, font.getReverseGlyphMap()) | |
Args: | |
ligs (dict): A mapping of ligature names to an array of dictionaries: | |
for each component glyph in the ligature, an dictionary mapping | |
mark class IDs to anchors. | |
numMarkClasses (int): The number of mark classes. | |
glyphMap: a glyph name to ID map, typically returned from | |
``font.getReverseGlyphMap()``. | |
Returns: | |
An ``otTables.LigatureArray`` object if deltas were supplied. | |
""" | |
self = ot.LigatureArray() | |
self.LigatureAttach = [] | |
for lig in sorted(ligs, key=glyphMap.__getitem__): | |
anchors = [] | |
for component in ligs[lig]: | |
anchors.append([component.get(mc) for mc in range(numMarkClasses)]) | |
self.LigatureAttach.append(buildLigatureAttach(anchors)) | |
self.LigatureCount = len(self.LigatureAttach) | |
return self | |
def buildLigatureAttach(components): | |
# [[Anchor, Anchor], [Anchor, Anchor, Anchor]] --> LigatureAttach | |
self = ot.LigatureAttach() | |
self.ComponentRecord = [buildComponentRecord(c) for c in components] | |
self.ComponentCount = len(self.ComponentRecord) | |
return self | |
def buildMarkArray(marks, glyphMap): | |
"""Builds a mark array subtable. | |
As part of building mark-to-* positioning rules, you will need to define | |
a MarkArray subtable, which "defines the class and the anchor point | |
for a mark glyph." This function builds the mark array subtable. | |
Example:: | |
mark = { | |
"acute": (0, buildAnchor(300,712)), | |
# ... | |
} | |
markarray = buildMarkArray(marks, font.getReverseGlyphMap()) | |
Args: | |
marks (dict): A dictionary mapping anchors to glyphs; the keys being | |
glyph names, and the values being a tuple of mark class number and | |
an ``otTables.Anchor`` object representing the mark's attachment | |
point. | |
glyphMap: a glyph name to ID map, typically returned from | |
``font.getReverseGlyphMap()``. | |
Returns: | |
An ``otTables.MarkArray`` object. | |
""" | |
self = ot.MarkArray() | |
self.MarkRecord = [] | |
for mark in sorted(marks.keys(), key=glyphMap.__getitem__): | |
markClass, anchor = marks[mark] | |
markrec = buildMarkRecord(markClass, anchor) | |
self.MarkRecord.append(markrec) | |
self.MarkCount = len(self.MarkRecord) | |
return self | |
def buildMarkBasePos(marks, bases, glyphMap): | |
"""Build a list of MarkBasePos (GPOS4) subtables. | |
This routine turns a set of marks and bases into a list of mark-to-base | |
positioning subtables. Currently the list will contain a single subtable | |
containing all marks and bases, although at a later date it may return the | |
optimal list of subtables subsetting the marks and bases into groups which | |
save space. See :func:`buildMarkBasePosSubtable` below. | |
Note that if you are implementing a layout compiler, you may find it more | |
flexible to use | |
:py:class:`fontTools.otlLib.lookupBuilders.MarkBasePosBuilder` instead. | |
Example:: | |
# a1, a2, a3, a4, a5 = buildAnchor(500, 100), ... | |
marks = {"acute": (0, a1), "grave": (0, a1), "cedilla": (1, a2)} | |
bases = {"a": {0: a3, 1: a5}, "b": {0: a4, 1: a5}} | |
markbaseposes = buildMarkBasePos(marks, bases, font.getReverseGlyphMap()) | |
Args: | |
marks (dict): A dictionary mapping anchors to glyphs; the keys being | |
glyph names, and the values being a tuple of mark class number and | |
an ``otTables.Anchor`` object representing the mark's attachment | |
point. (See :func:`buildMarkArray`.) | |
bases (dict): A dictionary mapping anchors to glyphs; the keys being | |
glyph names, and the values being dictionaries mapping mark class ID | |
to the appropriate ``otTables.Anchor`` object used for attaching marks | |
of that class. (See :func:`buildBaseArray`.) | |
glyphMap: a glyph name to ID map, typically returned from | |
``font.getReverseGlyphMap()``. | |
Returns: | |
A list of ``otTables.MarkBasePos`` objects. | |
""" | |
# TODO: Consider emitting multiple subtables to save space. | |
# Partition the marks and bases into disjoint subsets, so that | |
# MarkBasePos rules would only access glyphs from a single | |
# subset. This would likely lead to smaller mark/base | |
# matrices, so we might be able to omit many of the empty | |
# anchor tables that we currently produce. Of course, this | |
# would only work if the MarkBasePos rules of real-world fonts | |
# allow partitioning into multiple subsets. We should find out | |
# whether this is the case; if so, implement the optimization. | |
# On the other hand, a very large number of subtables could | |
# slow down layout engines; so this would need profiling. | |
return [buildMarkBasePosSubtable(marks, bases, glyphMap)] | |
def buildMarkBasePosSubtable(marks, bases, glyphMap): | |
"""Build a single MarkBasePos (GPOS4) subtable. | |
This builds a mark-to-base lookup subtable containing all of the referenced | |
marks and bases. See :func:`buildMarkBasePos`. | |
Args: | |
marks (dict): A dictionary mapping anchors to glyphs; the keys being | |
glyph names, and the values being a tuple of mark class number and | |
an ``otTables.Anchor`` object representing the mark's attachment | |
point. (See :func:`buildMarkArray`.) | |
bases (dict): A dictionary mapping anchors to glyphs; the keys being | |
glyph names, and the values being dictionaries mapping mark class ID | |
to the appropriate ``otTables.Anchor`` object used for attaching marks | |
of that class. (See :func:`buildBaseArray`.) | |
glyphMap: a glyph name to ID map, typically returned from | |
``font.getReverseGlyphMap()``. | |
Returns: | |
A ``otTables.MarkBasePos`` object. | |
""" | |
self = ot.MarkBasePos() | |
self.Format = 1 | |
self.MarkCoverage = buildCoverage(marks, glyphMap) | |
self.MarkArray = buildMarkArray(marks, glyphMap) | |
self.ClassCount = max([mc for mc, _ in marks.values()]) + 1 | |
self.BaseCoverage = buildCoverage(bases, glyphMap) | |
self.BaseArray = buildBaseArray(bases, self.ClassCount, glyphMap) | |
return self | |
def buildMarkLigPos(marks, ligs, glyphMap): | |
"""Build a list of MarkLigPos (GPOS5) subtables. | |
This routine turns a set of marks and ligatures into a list of mark-to-ligature | |
positioning subtables. Currently the list will contain a single subtable | |
containing all marks and ligatures, although at a later date it may return | |
the optimal list of subtables subsetting the marks and ligatures into groups | |
which save space. See :func:`buildMarkLigPosSubtable` below. | |
Note that if you are implementing a layout compiler, you may find it more | |
flexible to use | |
:py:class:`fontTools.otlLib.lookupBuilders.MarkLigPosBuilder` instead. | |
Example:: | |
# a1, a2, a3, a4, a5 = buildAnchor(500, 100), ... | |
marks = { | |
"acute": (0, a1), | |
"grave": (0, a1), | |
"cedilla": (1, a2) | |
} | |
ligs = { | |
"f_i": [ | |
{ 0: a3, 1: a5 }, # f | |
{ 0: a4, 1: a5 } # i | |
], | |
# "c_t": [{...}, {...}] | |
} | |
markligposes = buildMarkLigPos(marks, ligs, | |
font.getReverseGlyphMap()) | |
Args: | |
marks (dict): A dictionary mapping anchors to glyphs; the keys being | |
glyph names, and the values being a tuple of mark class number and | |
an ``otTables.Anchor`` object representing the mark's attachment | |
point. (See :func:`buildMarkArray`.) | |
ligs (dict): A mapping of ligature names to an array of dictionaries: | |
for each component glyph in the ligature, an dictionary mapping | |
mark class IDs to anchors. (See :func:`buildLigatureArray`.) | |
glyphMap: a glyph name to ID map, typically returned from | |
``font.getReverseGlyphMap()``. | |
Returns: | |
A list of ``otTables.MarkLigPos`` objects. | |
""" | |
# TODO: Consider splitting into multiple subtables to save space, | |
# as with MarkBasePos, this would be a trade-off that would need | |
# profiling. And, depending on how typical fonts are structured, | |
# it might not be worth doing at all. | |
return [buildMarkLigPosSubtable(marks, ligs, glyphMap)] | |
def buildMarkLigPosSubtable(marks, ligs, glyphMap): | |
"""Build a single MarkLigPos (GPOS5) subtable. | |
This builds a mark-to-base lookup subtable containing all of the referenced | |
marks and bases. See :func:`buildMarkLigPos`. | |
Args: | |
marks (dict): A dictionary mapping anchors to glyphs; the keys being | |
glyph names, and the values being a tuple of mark class number and | |
an ``otTables.Anchor`` object representing the mark's attachment | |
point. (See :func:`buildMarkArray`.) | |
ligs (dict): A mapping of ligature names to an array of dictionaries: | |
for each component glyph in the ligature, an dictionary mapping | |
mark class IDs to anchors. (See :func:`buildLigatureArray`.) | |
glyphMap: a glyph name to ID map, typically returned from | |
``font.getReverseGlyphMap()``. | |
Returns: | |
A ``otTables.MarkLigPos`` object. | |
""" | |
self = ot.MarkLigPos() | |
self.Format = 1 | |
self.MarkCoverage = buildCoverage(marks, glyphMap) | |
self.MarkArray = buildMarkArray(marks, glyphMap) | |
self.ClassCount = max([mc for mc, _ in marks.values()]) + 1 | |
self.LigatureCoverage = buildCoverage(ligs, glyphMap) | |
self.LigatureArray = buildLigatureArray(ligs, self.ClassCount, glyphMap) | |
return self | |
def buildMarkRecord(classID, anchor): | |
assert isinstance(classID, int) | |
assert isinstance(anchor, ot.Anchor) | |
self = ot.MarkRecord() | |
self.Class = classID | |
self.MarkAnchor = anchor | |
return self | |
def buildMark2Record(anchors): | |
# [otTables.Anchor, otTables.Anchor, ...] --> otTables.Mark2Record | |
self = ot.Mark2Record() | |
self.Mark2Anchor = anchors | |
return self | |
def _getValueFormat(f, values, i): | |
# Helper for buildPairPos{Glyphs|Classes}Subtable. | |
if f is not None: | |
return f | |
mask = 0 | |
for value in values: | |
if value is not None and value[i] is not None: | |
mask |= value[i].getFormat() | |
return mask | |
def buildPairPosClassesSubtable(pairs, glyphMap, valueFormat1=None, valueFormat2=None): | |
"""Builds a class pair adjustment (GPOS2 format 2) subtable. | |
Kerning tables are generally expressed as pair positioning tables using | |
class-based pair adjustments. This routine builds format 2 PairPos | |
subtables. | |
Note that if you are implementing a layout compiler, you may find it more | |
flexible to use | |
:py:class:`fontTools.otlLib.lookupBuilders.ClassPairPosSubtableBuilder` | |
instead, as this takes care of ensuring that the supplied pairs can be | |
formed into non-overlapping classes and emitting individual subtables | |
whenever the non-overlapping requirement means that a new subtable is | |
required. | |
Example:: | |
pairs = {} | |
pairs[( | |
[ "K", "X" ], | |
[ "W", "V" ] | |
)] = ( buildValue(xAdvance=+5), buildValue() ) | |
# pairs[(... , ...)] = (..., ...) | |
pairpos = buildPairPosClassesSubtable(pairs, font.getReverseGlyphMap()) | |
Args: | |
pairs (dict): Pair positioning data; the keys being a two-element | |
tuple of lists of glyphnames, and the values being a two-element | |
tuple of ``otTables.ValueRecord`` objects. | |
glyphMap: a glyph name to ID map, typically returned from | |
``font.getReverseGlyphMap()``. | |
valueFormat1: Force the "left" value records to the given format. | |
valueFormat2: Force the "right" value records to the given format. | |
Returns: | |
A ``otTables.PairPos`` object. | |
""" | |
coverage = set() | |
classDef1 = ClassDefBuilder(useClass0=True) | |
classDef2 = ClassDefBuilder(useClass0=False) | |
for gc1, gc2 in sorted(pairs): | |
coverage.update(gc1) | |
classDef1.add(gc1) | |
classDef2.add(gc2) | |
self = ot.PairPos() | |
self.Format = 2 | |
valueFormat1 = self.ValueFormat1 = _getValueFormat(valueFormat1, pairs.values(), 0) | |
valueFormat2 = self.ValueFormat2 = _getValueFormat(valueFormat2, pairs.values(), 1) | |
self.Coverage = buildCoverage(coverage, glyphMap) | |
self.ClassDef1 = classDef1.build() | |
self.ClassDef2 = classDef2.build() | |
classes1 = classDef1.classes() | |
classes2 = classDef2.classes() | |
self.Class1Record = [] | |
for c1 in classes1: | |
rec1 = ot.Class1Record() | |
rec1.Class2Record = [] | |
self.Class1Record.append(rec1) | |
for c2 in classes2: | |
rec2 = ot.Class2Record() | |
val1, val2 = pairs.get((c1, c2), (None, None)) | |
rec2.Value1 = ( | |
ValueRecord(src=val1, valueFormat=valueFormat1) | |
if valueFormat1 | |
else None | |
) | |
rec2.Value2 = ( | |
ValueRecord(src=val2, valueFormat=valueFormat2) | |
if valueFormat2 | |
else None | |
) | |
rec1.Class2Record.append(rec2) | |
self.Class1Count = len(self.Class1Record) | |
self.Class2Count = len(classes2) | |
return self | |
def buildPairPosGlyphs(pairs, glyphMap): | |
"""Builds a list of glyph-based pair adjustment (GPOS2 format 1) subtables. | |
This organises a list of pair positioning adjustments into subtables based | |
on common value record formats. | |
Note that if you are implementing a layout compiler, you may find it more | |
flexible to use | |
:py:class:`fontTools.otlLib.lookupBuilders.PairPosBuilder` | |
instead. | |
Example:: | |
pairs = { | |
("K", "W"): ( buildValue(xAdvance=+5), buildValue() ), | |
("K", "V"): ( buildValue(xAdvance=+5), buildValue() ), | |
# ... | |
} | |
subtables = buildPairPosGlyphs(pairs, font.getReverseGlyphMap()) | |
Args: | |
pairs (dict): Pair positioning data; the keys being a two-element | |
tuple of glyphnames, and the values being a two-element | |
tuple of ``otTables.ValueRecord`` objects. | |
glyphMap: a glyph name to ID map, typically returned from | |
``font.getReverseGlyphMap()``. | |
Returns: | |
A list of ``otTables.PairPos`` objects. | |
""" | |
p = {} # (formatA, formatB) --> {(glyphA, glyphB): (valA, valB)} | |
for (glyphA, glyphB), (valA, valB) in pairs.items(): | |
formatA = valA.getFormat() if valA is not None else 0 | |
formatB = valB.getFormat() if valB is not None else 0 | |
pos = p.setdefault((formatA, formatB), {}) | |
pos[(glyphA, glyphB)] = (valA, valB) | |
return [ | |
buildPairPosGlyphsSubtable(pos, glyphMap, formatA, formatB) | |
for ((formatA, formatB), pos) in sorted(p.items()) | |
] | |
def buildPairPosGlyphsSubtable(pairs, glyphMap, valueFormat1=None, valueFormat2=None): | |
"""Builds a single glyph-based pair adjustment (GPOS2 format 1) subtable. | |
This builds a PairPos subtable from a dictionary of glyph pairs and | |
their positioning adjustments. See also :func:`buildPairPosGlyphs`. | |
Note that if you are implementing a layout compiler, you may find it more | |
flexible to use | |
:py:class:`fontTools.otlLib.lookupBuilders.PairPosBuilder` instead. | |
Example:: | |
pairs = { | |
("K", "W"): ( buildValue(xAdvance=+5), buildValue() ), | |
("K", "V"): ( buildValue(xAdvance=+5), buildValue() ), | |
# ... | |
} | |
pairpos = buildPairPosGlyphsSubtable(pairs, font.getReverseGlyphMap()) | |
Args: | |
pairs (dict): Pair positioning data; the keys being a two-element | |
tuple of glyphnames, and the values being a two-element | |
tuple of ``otTables.ValueRecord`` objects. | |
glyphMap: a glyph name to ID map, typically returned from | |
``font.getReverseGlyphMap()``. | |
valueFormat1: Force the "left" value records to the given format. | |
valueFormat2: Force the "right" value records to the given format. | |
Returns: | |
A ``otTables.PairPos`` object. | |
""" | |
self = ot.PairPos() | |
self.Format = 1 | |
valueFormat1 = self.ValueFormat1 = _getValueFormat(valueFormat1, pairs.values(), 0) | |
valueFormat2 = self.ValueFormat2 = _getValueFormat(valueFormat2, pairs.values(), 1) | |
p = {} | |
for (glyphA, glyphB), (valA, valB) in pairs.items(): | |
p.setdefault(glyphA, []).append((glyphB, valA, valB)) | |
self.Coverage = buildCoverage({g for g, _ in pairs.keys()}, glyphMap) | |
self.PairSet = [] | |
for glyph in self.Coverage.glyphs: | |
ps = ot.PairSet() | |
ps.PairValueRecord = [] | |
self.PairSet.append(ps) | |
for glyph2, val1, val2 in sorted(p[glyph], key=lambda x: glyphMap[x[0]]): | |
pvr = ot.PairValueRecord() | |
pvr.SecondGlyph = glyph2 | |
pvr.Value1 = ( | |
ValueRecord(src=val1, valueFormat=valueFormat1) | |
if valueFormat1 | |
else None | |
) | |
pvr.Value2 = ( | |
ValueRecord(src=val2, valueFormat=valueFormat2) | |
if valueFormat2 | |
else None | |
) | |
ps.PairValueRecord.append(pvr) | |
ps.PairValueCount = len(ps.PairValueRecord) | |
self.PairSetCount = len(self.PairSet) | |
return self | |
def buildSinglePos(mapping, glyphMap): | |
"""Builds a list of single adjustment (GPOS1) subtables. | |
This builds a list of SinglePos subtables from a dictionary of glyph | |
names and their positioning adjustments. The format of the subtables are | |
determined to optimize the size of the resulting subtables. | |
See also :func:`buildSinglePosSubtable`. | |
Note that if you are implementing a layout compiler, you may find it more | |
flexible to use | |
:py:class:`fontTools.otlLib.lookupBuilders.SinglePosBuilder` instead. | |
Example:: | |
mapping = { | |
"V": buildValue({ "xAdvance" : +5 }), | |
# ... | |
} | |
subtables = buildSinglePos(pairs, font.getReverseGlyphMap()) | |
Args: | |
mapping (dict): A mapping between glyphnames and | |
``otTables.ValueRecord`` objects. | |
glyphMap: a glyph name to ID map, typically returned from | |
``font.getReverseGlyphMap()``. | |
Returns: | |
A list of ``otTables.SinglePos`` objects. | |
""" | |
result, handled = [], set() | |
# In SinglePos format 1, the covered glyphs all share the same ValueRecord. | |
# In format 2, each glyph has its own ValueRecord, but these records | |
# all have the same properties (eg., all have an X but no Y placement). | |
coverages, masks, values = {}, {}, {} | |
for glyph, value in mapping.items(): | |
key = _getSinglePosValueKey(value) | |
coverages.setdefault(key, []).append(glyph) | |
masks.setdefault(key[0], []).append(key) | |
values[key] = value | |
# If a ValueRecord is shared between multiple glyphs, we generate | |
# a SinglePos format 1 subtable; that is the most compact form. | |
for key, glyphs in coverages.items(): | |
# 5 ushorts is the length of introducing another sublookup | |
if len(glyphs) * _getSinglePosValueSize(key) > 5: | |
format1Mapping = {g: values[key] for g in glyphs} | |
result.append(buildSinglePosSubtable(format1Mapping, glyphMap)) | |
handled.add(key) | |
# In the remaining ValueRecords, look for those whose valueFormat | |
# (the set of used properties) is shared between multiple records. | |
# These will get encoded in format 2. | |
for valueFormat, keys in masks.items(): | |
f2 = [k for k in keys if k not in handled] | |
if len(f2) > 1: | |
format2Mapping = {} | |
for k in f2: | |
format2Mapping.update((g, values[k]) for g in coverages[k]) | |
result.append(buildSinglePosSubtable(format2Mapping, glyphMap)) | |
handled.update(f2) | |
# The remaining ValueRecords are only used by a few glyphs, normally | |
# one. We encode these in format 1 again. | |
for key, glyphs in coverages.items(): | |
if key not in handled: | |
for g in glyphs: | |
st = buildSinglePosSubtable({g: values[key]}, glyphMap) | |
result.append(st) | |
# When the OpenType layout engine traverses the subtables, it will | |
# stop after the first matching subtable. Therefore, we sort the | |
# resulting subtables by decreasing coverage size; this increases | |
# the chance that the layout engine can do an early exit. (Of course, | |
# this would only be true if all glyphs were equally frequent, which | |
# is not really the case; but we do not know their distribution). | |
# If two subtables cover the same number of glyphs, we sort them | |
# by glyph ID so that our output is deterministic. | |
result.sort(key=lambda t: _getSinglePosTableKey(t, glyphMap)) | |
return result | |
def buildSinglePosSubtable(values, glyphMap): | |
"""Builds a single adjustment (GPOS1) subtable. | |
This builds a list of SinglePos subtables from a dictionary of glyph | |
names and their positioning adjustments. The format of the subtable is | |
determined to optimize the size of the output. | |
See also :func:`buildSinglePos`. | |
Note that if you are implementing a layout compiler, you may find it more | |
flexible to use | |
:py:class:`fontTools.otlLib.lookupBuilders.SinglePosBuilder` instead. | |
Example:: | |
mapping = { | |
"V": buildValue({ "xAdvance" : +5 }), | |
# ... | |
} | |
subtable = buildSinglePos(pairs, font.getReverseGlyphMap()) | |
Args: | |
mapping (dict): A mapping between glyphnames and | |
``otTables.ValueRecord`` objects. | |
glyphMap: a glyph name to ID map, typically returned from | |
``font.getReverseGlyphMap()``. | |
Returns: | |
A ``otTables.SinglePos`` object. | |
""" | |
self = ot.SinglePos() | |
self.Coverage = buildCoverage(values.keys(), glyphMap) | |
valueFormat = self.ValueFormat = reduce( | |
int.__or__, [v.getFormat() for v in values.values()], 0 | |
) | |
valueRecords = [ | |
ValueRecord(src=values[g], valueFormat=valueFormat) | |
for g in self.Coverage.glyphs | |
] | |
if all(v == valueRecords[0] for v in valueRecords): | |
self.Format = 1 | |
if self.ValueFormat != 0: | |
self.Value = valueRecords[0] | |
else: | |
self.Value = None | |
else: | |
self.Format = 2 | |
self.Value = valueRecords | |
self.ValueCount = len(self.Value) | |
return self | |
def _getSinglePosTableKey(subtable, glyphMap): | |
assert isinstance(subtable, ot.SinglePos), subtable | |
glyphs = subtable.Coverage.glyphs | |
return (-len(glyphs), glyphMap[glyphs[0]]) | |
def _getSinglePosValueKey(valueRecord): | |
# otBase.ValueRecord --> (2, ("YPlacement": 12)) | |
assert isinstance(valueRecord, ValueRecord), valueRecord | |
valueFormat, result = 0, [] | |
for name, value in valueRecord.__dict__.items(): | |
if isinstance(value, ot.Device): | |
result.append((name, _makeDeviceTuple(value))) | |
else: | |
result.append((name, value)) | |
valueFormat |= valueRecordFormatDict[name][0] | |
result.sort() | |
result.insert(0, valueFormat) | |
return tuple(result) | |
_DeviceTuple = namedtuple("_DeviceTuple", "DeltaFormat StartSize EndSize DeltaValue") | |
def _makeDeviceTuple(device): | |
# otTables.Device --> tuple, for making device tables unique | |
return _DeviceTuple( | |
device.DeltaFormat, | |
device.StartSize, | |
device.EndSize, | |
() if device.DeltaFormat & 0x8000 else tuple(device.DeltaValue), | |
) | |
def _getSinglePosValueSize(valueKey): | |
# Returns how many ushorts this valueKey (short form of ValueRecord) takes up | |
count = 0 | |
for _, v in valueKey[1:]: | |
if isinstance(v, _DeviceTuple): | |
count += len(v.DeltaValue) + 3 | |
else: | |
count += 1 | |
return count | |
def buildValue(value): | |
"""Builds a positioning value record. | |
Value records are used to specify coordinates and adjustments for | |
positioning and attaching glyphs. Many of the positioning functions | |
in this library take ``otTables.ValueRecord`` objects as arguments. | |
This function builds value records from dictionaries. | |
Args: | |
value (dict): A dictionary with zero or more of the following keys: | |
- ``xPlacement`` | |
- ``yPlacement`` | |
- ``xAdvance`` | |
- ``yAdvance`` | |
- ``xPlaDevice`` | |
- ``yPlaDevice`` | |
- ``xAdvDevice`` | |
- ``yAdvDevice`` | |
Returns: | |
An ``otTables.ValueRecord`` object. | |
""" | |
self = ValueRecord() | |
for k, v in value.items(): | |
setattr(self, k, v) | |
return self | |
# GDEF | |
def buildAttachList(attachPoints, glyphMap): | |
"""Builds an AttachList subtable. | |
A GDEF table may contain an Attachment Point List table (AttachList) | |
which stores the contour indices of attachment points for glyphs with | |
attachment points. This routine builds AttachList subtables. | |
Args: | |
attachPoints (dict): A mapping between glyph names and a list of | |
contour indices. | |
Returns: | |
An ``otTables.AttachList`` object if attachment points are supplied, | |
or ``None`` otherwise. | |
""" | |
if not attachPoints: | |
return None | |
self = ot.AttachList() | |
self.Coverage = buildCoverage(attachPoints.keys(), glyphMap) | |
self.AttachPoint = [buildAttachPoint(attachPoints[g]) for g in self.Coverage.glyphs] | |
self.GlyphCount = len(self.AttachPoint) | |
return self | |
def buildAttachPoint(points): | |
# [4, 23, 41] --> otTables.AttachPoint | |
# Only used by above. | |
if not points: | |
return None | |
self = ot.AttachPoint() | |
self.PointIndex = sorted(set(points)) | |
self.PointCount = len(self.PointIndex) | |
return self | |
def buildCaretValueForCoord(coord): | |
# 500 --> otTables.CaretValue, format 1 | |
# (500, DeviceTable) --> otTables.CaretValue, format 3 | |
self = ot.CaretValue() | |
if isinstance(coord, tuple): | |
self.Format = 3 | |
self.Coordinate, self.DeviceTable = coord | |
else: | |
self.Format = 1 | |
self.Coordinate = coord | |
return self | |
def buildCaretValueForPoint(point): | |
# 4 --> otTables.CaretValue, format 2 | |
self = ot.CaretValue() | |
self.Format = 2 | |
self.CaretValuePoint = point | |
return self | |
def buildLigCaretList(coords, points, glyphMap): | |
"""Builds a ligature caret list table. | |
Ligatures appear as a single glyph representing multiple characters; however | |
when, for example, editing text containing a ``f_i`` ligature, the user may | |
want to place the cursor between the ``f`` and the ``i``. The ligature caret | |
list in the GDEF table specifies the position to display the "caret" (the | |
character insertion indicator, typically a flashing vertical bar) "inside" | |
the ligature to represent an insertion point. The insertion positions may | |
be specified either by coordinate or by contour point. | |
Example:: | |
coords = { | |
"f_f_i": [300, 600] # f|fi cursor at 300 units, ff|i cursor at 600. | |
} | |
points = { | |
"c_t": [28] # c|t cursor appears at coordinate of contour point 28. | |
} | |
ligcaretlist = buildLigCaretList(coords, points, font.getReverseGlyphMap()) | |
Args: | |
coords: A mapping between glyph names and a list of coordinates for | |
the insertion point of each ligature component after the first one. | |
points: A mapping between glyph names and a list of contour points for | |
the insertion point of each ligature component after the first one. | |
glyphMap: a glyph name to ID map, typically returned from | |
``font.getReverseGlyphMap()``. | |
Returns: | |
A ``otTables.LigCaretList`` object if any carets are present, or | |
``None`` otherwise.""" | |
glyphs = set(coords.keys()) if coords else set() | |
if points: | |
glyphs.update(points.keys()) | |
carets = {g: buildLigGlyph(coords.get(g), points.get(g)) for g in glyphs} | |
carets = {g: c for g, c in carets.items() if c is not None} | |
if not carets: | |
return None | |
self = ot.LigCaretList() | |
self.Coverage = buildCoverage(carets.keys(), glyphMap) | |
self.LigGlyph = [carets[g] for g in self.Coverage.glyphs] | |
self.LigGlyphCount = len(self.LigGlyph) | |
return self | |
def buildLigGlyph(coords, points): | |
# ([500], [4]) --> otTables.LigGlyph; None for empty coords/points | |
carets = [] | |
if coords: | |
coords = sorted(coords, key=lambda c: c[0] if isinstance(c, tuple) else c) | |
carets.extend([buildCaretValueForCoord(c) for c in coords]) | |
if points: | |
carets.extend([buildCaretValueForPoint(p) for p in sorted(points)]) | |
if not carets: | |
return None | |
self = ot.LigGlyph() | |
self.CaretValue = carets | |
self.CaretCount = len(self.CaretValue) | |
return self | |
def buildMarkGlyphSetsDef(markSets, glyphMap): | |
"""Builds a mark glyph sets definition table. | |
OpenType Layout lookups may choose to use mark filtering sets to consider | |
or ignore particular combinations of marks. These sets are specified by | |
setting a flag on the lookup, but the mark filtering sets are defined in | |
the ``GDEF`` table. This routine builds the subtable containing the mark | |
glyph set definitions. | |
Example:: | |
set0 = set("acute", "grave") | |
set1 = set("caron", "grave") | |
markglyphsets = buildMarkGlyphSetsDef([set0, set1], font.getReverseGlyphMap()) | |
Args: | |
markSets: A list of sets of glyphnames. | |
glyphMap: a glyph name to ID map, typically returned from | |
``font.getReverseGlyphMap()``. | |
Returns | |
An ``otTables.MarkGlyphSetsDef`` object. | |
""" | |
if not markSets: | |
return None | |
self = ot.MarkGlyphSetsDef() | |
self.MarkSetTableFormat = 1 | |
self.Coverage = [buildCoverage(m, glyphMap) for m in markSets] | |
self.MarkSetCount = len(self.Coverage) | |
return self | |
class ClassDefBuilder(object): | |
"""Helper for building ClassDef tables.""" | |
def __init__(self, useClass0): | |
self.classes_ = set() | |
self.glyphs_ = {} | |
self.useClass0_ = useClass0 | |
def canAdd(self, glyphs): | |
if isinstance(glyphs, (set, frozenset)): | |
glyphs = sorted(glyphs) | |
glyphs = tuple(glyphs) | |
if glyphs in self.classes_: | |
return True | |
for glyph in glyphs: | |
if glyph in self.glyphs_: | |
return False | |
return True | |
def add(self, glyphs): | |
if isinstance(glyphs, (set, frozenset)): | |
glyphs = sorted(glyphs) | |
glyphs = tuple(glyphs) | |
if glyphs in self.classes_: | |
return | |
self.classes_.add(glyphs) | |
for glyph in glyphs: | |
if glyph in self.glyphs_: | |
raise OpenTypeLibError( | |
f"Glyph {glyph} is already present in class.", None | |
) | |
self.glyphs_[glyph] = glyphs | |
def classes(self): | |
# In ClassDef1 tables, class id #0 does not need to be encoded | |
# because zero is the default. Therefore, we use id #0 for the | |
# glyph class that has the largest number of members. However, | |
# in other tables than ClassDef1, 0 means "every other glyph" | |
# so we should not use that ID for any real glyph classes; | |
# we implement this by inserting an empty set at position 0. | |
# | |
# TODO: Instead of counting the number of glyphs in each class, | |
# we should determine the encoded size. If the glyphs in a large | |
# class form a contiguous range, the encoding is actually quite | |
# compact, whereas a non-contiguous set might need a lot of bytes | |
# in the output file. We don't get this right with the key below. | |
result = sorted(self.classes_, key=lambda s: (-len(s), s)) | |
if not self.useClass0_: | |
result.insert(0, frozenset()) | |
return result | |
def build(self): | |
glyphClasses = {} | |
for classID, glyphs in enumerate(self.classes()): | |
if classID == 0: | |
continue | |
for glyph in glyphs: | |
glyphClasses[glyph] = classID | |
classDef = ot.ClassDef() | |
classDef.classDefs = glyphClasses | |
return classDef | |
AXIS_VALUE_NEGATIVE_INFINITY = fixedToFloat(-0x80000000, 16) | |
AXIS_VALUE_POSITIVE_INFINITY = fixedToFloat(0x7FFFFFFF, 16) | |
def buildStatTable( | |
ttFont, axes, locations=None, elidedFallbackName=2, windowsNames=True, macNames=True | |
): | |
"""Add a 'STAT' table to 'ttFont'. | |
'axes' is a list of dictionaries describing axes and their | |
values. | |
Example:: | |
axes = [ | |
dict( | |
tag="wght", | |
name="Weight", | |
ordering=0, # optional | |
values=[ | |
dict(value=100, name='Thin'), | |
dict(value=300, name='Light'), | |
dict(value=400, name='Regular', flags=0x2), | |
dict(value=900, name='Black'), | |
], | |
) | |
] | |
Each axis dict must have 'tag' and 'name' items. 'tag' maps | |
to the 'AxisTag' field. 'name' can be a name ID (int), a string, | |
or a dictionary containing multilingual names (see the | |
addMultilingualName() name table method), and will translate to | |
the AxisNameID field. | |
An axis dict may contain an 'ordering' item that maps to the | |
AxisOrdering field. If omitted, the order of the axes list is | |
used to calculate AxisOrdering fields. | |
The axis dict may contain a 'values' item, which is a list of | |
dictionaries describing AxisValue records belonging to this axis. | |
Each value dict must have a 'name' item, which can be a name ID | |
(int), a string, or a dictionary containing multilingual names, | |
like the axis name. It translates to the ValueNameID field. | |
Optionally the value dict can contain a 'flags' item. It maps to | |
the AxisValue Flags field, and will be 0 when omitted. | |
The format of the AxisValue is determined by the remaining contents | |
of the value dictionary: | |
If the value dict contains a 'value' item, an AxisValue record | |
Format 1 is created. If in addition to the 'value' item it contains | |
a 'linkedValue' item, an AxisValue record Format 3 is built. | |
If the value dict contains a 'nominalValue' item, an AxisValue | |
record Format 2 is built. Optionally it may contain 'rangeMinValue' | |
and 'rangeMaxValue' items. These map to -Infinity and +Infinity | |
respectively if omitted. | |
You cannot specify Format 4 AxisValue tables this way, as they are | |
not tied to a single axis, and specify a name for a location that | |
is defined by multiple axes values. Instead, you need to supply the | |
'locations' argument. | |
The optional 'locations' argument specifies AxisValue Format 4 | |
tables. It should be a list of dicts, where each dict has a 'name' | |
item, which works just like the value dicts above, an optional | |
'flags' item (defaulting to 0x0), and a 'location' dict. A | |
location dict key is an axis tag, and the associated value is the | |
location on the specified axis. They map to the AxisIndex and Value | |
fields of the AxisValueRecord. | |
Example:: | |
locations = [ | |
dict(name='Regular ABCD', location=dict(wght=300, ABCD=100)), | |
dict(name='Bold ABCD XYZ', location=dict(wght=600, ABCD=200)), | |
] | |
The optional 'elidedFallbackName' argument can be a name ID (int), | |
a string, a dictionary containing multilingual names, or a list of | |
STATNameStatements. It translates to the ElidedFallbackNameID field. | |
The 'ttFont' argument must be a TTFont instance that already has a | |
'name' table. If a 'STAT' table already exists, it will be | |
overwritten by the newly created one. | |
""" | |
ttFont["STAT"] = ttLib.newTable("STAT") | |
statTable = ttFont["STAT"].table = ot.STAT() | |
statTable.ElidedFallbackNameID = _addName( | |
ttFont, elidedFallbackName, windows=windowsNames, mac=macNames | |
) | |
# 'locations' contains data for AxisValue Format 4 | |
axisRecords, axisValues = _buildAxisRecords( | |
axes, ttFont, windowsNames=windowsNames, macNames=macNames | |
) | |
if not locations: | |
statTable.Version = 0x00010001 | |
else: | |
# We'll be adding Format 4 AxisValue records, which | |
# requires a higher table version | |
statTable.Version = 0x00010002 | |
multiAxisValues = _buildAxisValuesFormat4( | |
locations, axes, ttFont, windowsNames=windowsNames, macNames=macNames | |
) | |
axisValues = multiAxisValues + axisValues | |
ttFont["name"].names.sort() | |
# Store AxisRecords | |
axisRecordArray = ot.AxisRecordArray() | |
axisRecordArray.Axis = axisRecords | |
# XXX these should not be hard-coded but computed automatically | |
statTable.DesignAxisRecordSize = 8 | |
statTable.DesignAxisRecord = axisRecordArray | |
statTable.DesignAxisCount = len(axisRecords) | |
statTable.AxisValueCount = 0 | |
statTable.AxisValueArray = None | |
if axisValues: | |
# Store AxisValueRecords | |
axisValueArray = ot.AxisValueArray() | |
axisValueArray.AxisValue = axisValues | |
statTable.AxisValueArray = axisValueArray | |
statTable.AxisValueCount = len(axisValues) | |
def _buildAxisRecords(axes, ttFont, windowsNames=True, macNames=True): | |
axisRecords = [] | |
axisValues = [] | |
for axisRecordIndex, axisDict in enumerate(axes): | |
axis = ot.AxisRecord() | |
axis.AxisTag = axisDict["tag"] | |
axis.AxisNameID = _addName( | |
ttFont, axisDict["name"], 256, windows=windowsNames, mac=macNames | |
) | |
axis.AxisOrdering = axisDict.get("ordering", axisRecordIndex) | |
axisRecords.append(axis) | |
for axisVal in axisDict.get("values", ()): | |
axisValRec = ot.AxisValue() | |
axisValRec.AxisIndex = axisRecordIndex | |
axisValRec.Flags = axisVal.get("flags", 0) | |
axisValRec.ValueNameID = _addName( | |
ttFont, axisVal["name"], windows=windowsNames, mac=macNames | |
) | |
if "value" in axisVal: | |
axisValRec.Value = axisVal["value"] | |
if "linkedValue" in axisVal: | |
axisValRec.Format = 3 | |
axisValRec.LinkedValue = axisVal["linkedValue"] | |
else: | |
axisValRec.Format = 1 | |
elif "nominalValue" in axisVal: | |
axisValRec.Format = 2 | |
axisValRec.NominalValue = axisVal["nominalValue"] | |
axisValRec.RangeMinValue = axisVal.get( | |
"rangeMinValue", AXIS_VALUE_NEGATIVE_INFINITY | |
) | |
axisValRec.RangeMaxValue = axisVal.get( | |
"rangeMaxValue", AXIS_VALUE_POSITIVE_INFINITY | |
) | |
else: | |
raise ValueError("Can't determine format for AxisValue") | |
axisValues.append(axisValRec) | |
return axisRecords, axisValues | |
def _buildAxisValuesFormat4(locations, axes, ttFont, windowsNames=True, macNames=True): | |
axisTagToIndex = {} | |
for axisRecordIndex, axisDict in enumerate(axes): | |
axisTagToIndex[axisDict["tag"]] = axisRecordIndex | |
axisValues = [] | |
for axisLocationDict in locations: | |
axisValRec = ot.AxisValue() | |
axisValRec.Format = 4 | |
axisValRec.ValueNameID = _addName( | |
ttFont, axisLocationDict["name"], windows=windowsNames, mac=macNames | |
) | |
axisValRec.Flags = axisLocationDict.get("flags", 0) | |
axisValueRecords = [] | |
for tag, value in axisLocationDict["location"].items(): | |
avr = ot.AxisValueRecord() | |
avr.AxisIndex = axisTagToIndex[tag] | |
avr.Value = value | |
axisValueRecords.append(avr) | |
axisValueRecords.sort(key=lambda avr: avr.AxisIndex) | |
axisValRec.AxisCount = len(axisValueRecords) | |
axisValRec.AxisValueRecord = axisValueRecords | |
axisValues.append(axisValRec) | |
return axisValues | |
def _addName(ttFont, value, minNameID=0, windows=True, mac=True): | |
nameTable = ttFont["name"] | |
if isinstance(value, int): | |
# Already a nameID | |
return value | |
if isinstance(value, str): | |
names = dict(en=value) | |
elif isinstance(value, dict): | |
names = value | |
elif isinstance(value, list): | |
nameID = nameTable._findUnusedNameID() | |
for nameRecord in value: | |
if isinstance(nameRecord, STATNameStatement): | |
nameTable.setName( | |
nameRecord.string, | |
nameID, | |
nameRecord.platformID, | |
nameRecord.platEncID, | |
nameRecord.langID, | |
) | |
else: | |
raise TypeError("value must be a list of STATNameStatements") | |
return nameID | |
else: | |
raise TypeError("value must be int, str, dict or list") | |
return nameTable.addMultilingualName( | |
names, ttFont=ttFont, windows=windows, mac=mac, minNameID=minNameID | |
) | |
def buildMathTable( | |
ttFont, | |
constants=None, | |
italicsCorrections=None, | |
topAccentAttachments=None, | |
extendedShapes=None, | |
mathKerns=None, | |
minConnectorOverlap=0, | |
vertGlyphVariants=None, | |
horizGlyphVariants=None, | |
vertGlyphAssembly=None, | |
horizGlyphAssembly=None, | |
): | |
""" | |
Add a 'MATH' table to 'ttFont'. | |
'constants' is a dictionary of math constants. The keys are the constant | |
names from the MATH table specification (with capital first letter), and the | |
values are the constant values as numbers. | |
'italicsCorrections' is a dictionary of italic corrections. The keys are the | |
glyph names, and the values are the italic corrections as numbers. | |
'topAccentAttachments' is a dictionary of top accent attachments. The keys | |
are the glyph names, and the values are the top accent horizontal positions | |
as numbers. | |
'extendedShapes' is a set of extended shape glyphs. | |
'mathKerns' is a dictionary of math kerns. The keys are the glyph names, and | |
the values are dictionaries. The keys of these dictionaries are the side | |
names ('TopRight', 'TopLeft', 'BottomRight', 'BottomLeft'), and the values | |
are tuples of two lists. The first list contains the correction heights as | |
numbers, and the second list contains the kern values as numbers. | |
'minConnectorOverlap' is the minimum connector overlap as a number. | |
'vertGlyphVariants' is a dictionary of vertical glyph variants. The keys are | |
the glyph names, and the values are tuples of glyph name and full advance height. | |
'horizGlyphVariants' is a dictionary of horizontal glyph variants. The keys | |
are the glyph names, and the values are tuples of glyph name and full | |
advance width. | |
'vertGlyphAssembly' is a dictionary of vertical glyph assemblies. The keys | |
are the glyph names, and the values are tuples of assembly parts and italics | |
correction. The assembly parts are tuples of glyph name, flags, start | |
connector length, end connector length, and full advance height. | |
'horizGlyphAssembly' is a dictionary of horizontal glyph assemblies. The | |
keys are the glyph names, and the values are tuples of assembly parts | |
and italics correction. The assembly parts are tuples of glyph name, flags, | |
start connector length, end connector length, and full advance width. | |
Where a number is expected, an integer or a float can be used. The floats | |
will be rounded. | |
Example:: | |
constants = { | |
"ScriptPercentScaleDown": 70, | |
"ScriptScriptPercentScaleDown": 50, | |
"DelimitedSubFormulaMinHeight": 24, | |
"DisplayOperatorMinHeight": 60, | |
... | |
} | |
italicsCorrections = { | |
"fitalic-math": 100, | |
"fbolditalic-math": 120, | |
... | |
} | |
topAccentAttachments = { | |
"circumflexcomb": 500, | |
"acutecomb": 400, | |
"A": 300, | |
"B": 340, | |
... | |
} | |
extendedShapes = {"parenleft", "parenright", ...} | |
mathKerns = { | |
"A": { | |
"TopRight": ([-50, -100], [10, 20, 30]), | |
"TopLeft": ([50, 100], [10, 20, 30]), | |
... | |
}, | |
... | |
} | |
vertGlyphVariants = { | |
"parenleft": [("parenleft", 700), ("parenleft.size1", 1000), ...], | |
"parenright": [("parenright", 700), ("parenright.size1", 1000), ...], | |
... | |
} | |
vertGlyphAssembly = { | |
"braceleft": [ | |
( | |
("braceleft.bottom", 0, 0, 200, 500), | |
("braceleft.extender", 1, 200, 200, 200)), | |
("braceleft.middle", 0, 100, 100, 700), | |
("braceleft.extender", 1, 200, 200, 200), | |
("braceleft.top", 0, 200, 0, 500), | |
), | |
100, | |
], | |
... | |
} | |
""" | |
glyphMap = ttFont.getReverseGlyphMap() | |
ttFont["MATH"] = math = ttLib.newTable("MATH") | |
math.table = table = ot.MATH() | |
table.Version = 0x00010000 | |
table.populateDefaults() | |
table.MathConstants = _buildMathConstants(constants) | |
table.MathGlyphInfo = _buildMathGlyphInfo( | |
glyphMap, | |
italicsCorrections, | |
topAccentAttachments, | |
extendedShapes, | |
mathKerns, | |
) | |
table.MathVariants = _buildMathVariants( | |
glyphMap, | |
minConnectorOverlap, | |
vertGlyphVariants, | |
horizGlyphVariants, | |
vertGlyphAssembly, | |
horizGlyphAssembly, | |
) | |
def _buildMathConstants(constants): | |
if not constants: | |
return None | |
mathConstants = ot.MathConstants() | |
for conv in mathConstants.getConverters(): | |
value = otRound(constants.get(conv.name, 0)) | |
if conv.tableClass: | |
assert issubclass(conv.tableClass, ot.MathValueRecord) | |
value = _mathValueRecord(value) | |
setattr(mathConstants, conv.name, value) | |
return mathConstants | |
def _buildMathGlyphInfo( | |
glyphMap, | |
italicsCorrections, | |
topAccentAttachments, | |
extendedShapes, | |
mathKerns, | |
): | |
if not any([extendedShapes, italicsCorrections, topAccentAttachments, mathKerns]): | |
return None | |
info = ot.MathGlyphInfo() | |
info.populateDefaults() | |
if italicsCorrections: | |
coverage = buildCoverage(italicsCorrections.keys(), glyphMap) | |
info.MathItalicsCorrectionInfo = ot.MathItalicsCorrectionInfo() | |
info.MathItalicsCorrectionInfo.Coverage = coverage | |
info.MathItalicsCorrectionInfo.ItalicsCorrectionCount = len(coverage.glyphs) | |
info.MathItalicsCorrectionInfo.ItalicsCorrection = [ | |
_mathValueRecord(italicsCorrections[n]) for n in coverage.glyphs | |
] | |
if topAccentAttachments: | |
coverage = buildCoverage(topAccentAttachments.keys(), glyphMap) | |
info.MathTopAccentAttachment = ot.MathTopAccentAttachment() | |
info.MathTopAccentAttachment.TopAccentCoverage = coverage | |
info.MathTopAccentAttachment.TopAccentAttachmentCount = len(coverage.glyphs) | |
info.MathTopAccentAttachment.TopAccentAttachment = [ | |
_mathValueRecord(topAccentAttachments[n]) for n in coverage.glyphs | |
] | |
if extendedShapes: | |
info.ExtendedShapeCoverage = buildCoverage(extendedShapes, glyphMap) | |
if mathKerns: | |
coverage = buildCoverage(mathKerns.keys(), glyphMap) | |
info.MathKernInfo = ot.MathKernInfo() | |
info.MathKernInfo.MathKernCoverage = coverage | |
info.MathKernInfo.MathKernCount = len(coverage.glyphs) | |
info.MathKernInfo.MathKernInfoRecords = [] | |
for glyph in coverage.glyphs: | |
record = ot.MathKernInfoRecord() | |
for side in {"TopRight", "TopLeft", "BottomRight", "BottomLeft"}: | |
if side in mathKerns[glyph]: | |
correctionHeights, kernValues = mathKerns[glyph][side] | |
assert len(correctionHeights) == len(kernValues) - 1 | |
kern = ot.MathKern() | |
kern.HeightCount = len(correctionHeights) | |
kern.CorrectionHeight = [ | |
_mathValueRecord(h) for h in correctionHeights | |
] | |
kern.KernValue = [_mathValueRecord(v) for v in kernValues] | |
setattr(record, f"{side}MathKern", kern) | |
info.MathKernInfo.MathKernInfoRecords.append(record) | |
return info | |
def _buildMathVariants( | |
glyphMap, | |
minConnectorOverlap, | |
vertGlyphVariants, | |
horizGlyphVariants, | |
vertGlyphAssembly, | |
horizGlyphAssembly, | |
): | |
if not any( | |
[vertGlyphVariants, horizGlyphVariants, vertGlyphAssembly, horizGlyphAssembly] | |
): | |
return None | |
variants = ot.MathVariants() | |
variants.populateDefaults() | |
variants.MinConnectorOverlap = minConnectorOverlap | |
if vertGlyphVariants or vertGlyphAssembly: | |
variants.VertGlyphCoverage, variants.VertGlyphConstruction = ( | |
_buildMathGlyphConstruction( | |
glyphMap, | |
vertGlyphVariants, | |
vertGlyphAssembly, | |
) | |
) | |
if horizGlyphVariants or horizGlyphAssembly: | |
variants.HorizGlyphCoverage, variants.HorizGlyphConstruction = ( | |
_buildMathGlyphConstruction( | |
glyphMap, | |
horizGlyphVariants, | |
horizGlyphAssembly, | |
) | |
) | |
return variants | |
def _buildMathGlyphConstruction(glyphMap, variants, assemblies): | |
glyphs = set() | |
if variants: | |
glyphs.update(variants.keys()) | |
if assemblies: | |
glyphs.update(assemblies.keys()) | |
coverage = buildCoverage(glyphs, glyphMap) | |
constructions = [] | |
for glyphName in coverage.glyphs: | |
construction = ot.MathGlyphConstruction() | |
construction.populateDefaults() | |
if variants and glyphName in variants: | |
construction.VariantCount = len(variants[glyphName]) | |
construction.MathGlyphVariantRecord = [] | |
for variantName, advance in variants[glyphName]: | |
record = ot.MathGlyphVariantRecord() | |
record.VariantGlyph = variantName | |
record.AdvanceMeasurement = otRound(advance) | |
construction.MathGlyphVariantRecord.append(record) | |
if assemblies and glyphName in assemblies: | |
parts, ic = assemblies[glyphName] | |
construction.GlyphAssembly = ot.GlyphAssembly() | |
construction.GlyphAssembly.ItalicsCorrection = _mathValueRecord(ic) | |
construction.GlyphAssembly.PartCount = len(parts) | |
construction.GlyphAssembly.PartRecords = [] | |
for part in parts: | |
part_name, flags, start, end, advance = part | |
record = ot.GlyphPartRecord() | |
record.glyph = part_name | |
record.PartFlags = int(flags) | |
record.StartConnectorLength = otRound(start) | |
record.EndConnectorLength = otRound(end) | |
record.FullAdvance = otRound(advance) | |
construction.GlyphAssembly.PartRecords.append(record) | |
constructions.append(construction) | |
return coverage, constructions | |
def _mathValueRecord(value): | |
value_record = ot.MathValueRecord() | |
value_record.Value = otRound(value) | |
return value_record | |