|
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 |
|
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"]) |
|
): |
|
@property |
|
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) |
|
|
|
@property |
|
def hasPrefixOrSuffix(self): |
|
|
|
|
|
for rule in self.rules: |
|
if len(rule.prefix) > 0 or len(rule.suffix) > 0: |
|
return True |
|
return False |
|
|
|
@property |
|
def hasAnyGlyphClasses(self): |
|
|
|
|
|
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): |
|
|
|
|
|
ruleset = [ChainContextualRuleset()] |
|
for rule in self.rules: |
|
if rule.is_subtable_break: |
|
ruleset.append(ChainContextualRuleset()) |
|
continue |
|
ruleset[-1].addRule(rule) |
|
|
|
return [x for x in ruleset if len(x.rules) > 0] |
|
|
|
def getCompiledSize_(self, subtables): |
|
if not subtables: |
|
return 0 |
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
write_gpos7 = self.font.cfg.get("fontTools.otlLib.builder:WRITE_GPOS7") |
|
|
|
if not write_gpos7 and self.subtable_type == "Pos": |
|
chaining = True |
|
|
|
for ruleset in rulesets: |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
candidates = [None, None, None, []] |
|
for rule in ruleset.rules: |
|
candidates[3].append(self.buildFormat3Subtable(rule, chaining)) |
|
|
|
|
|
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) |
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
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 = [ |
|
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, |
|
) |
|
for i, classSet in enumerate(classSets): |
|
if not getattr(classSet, classRuleAttr): |
|
|
|
classSets[i] = None |
|
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): |
|
|
|
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)() |
|
setattr(st, f"{self.subtable_type}Count", 0) |
|
setattr(st, f"{self.subtable_type}LookupRecord", []) |
|
return st |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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" |
|
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" |
|
if chaining: |
|
subtablename = "Chain" + subtablename |
|
return subtablename |
|
|
|
def newRuleSet_(self, format=1, chaining=True): |
|
st = getattr( |
|
ot, self.ruleSetAttr_(format, chaining) |
|
)() |
|
st.populateDefaults() |
|
return st |
|
|
|
def newRule_(self, format=1, chaining=True): |
|
st = getattr( |
|
ot, self.ruleAttr_(format, chaining) |
|
)() |
|
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: |
|
|
|
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, |
|
) |
|
|
|
|
|
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 = [] |
|
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_subst(self, mapping, builder_class): |
|
"""Helper for add_{single,multi}_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, builder_class) 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() |
|
|
|
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 = {} |
|
self.bases = {} |
|
|
|
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 = {} |
|
self.ligatures = {} |
|
|
|
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 = {} |
|
self.baseMarks = {} |
|
|
|
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 = [] |
|
|
|
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): |
|
|
|
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_ = {} |
|
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 = [] |
|
self.glyphPairs = {} |
|
self.locations = {} |
|
|
|
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: |
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
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 = {} |
|
self.mapping = {} |
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
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() |
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
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): |
|
|
|
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): |
|
|
|
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. |
|
""" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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. |
|
|
|
""" |
|
|
|
|
|
|
|
|
|
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): |
|
|
|
self = ot.Mark2Record() |
|
self.Mark2Anchor = anchors |
|
return self |
|
|
|
|
|
def _getValueFormat(f, values, i): |
|
|
|
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 = {} |
|
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() |
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
for key, glyphs in coverages.items(): |
|
|
|
if len(glyphs) * _getSinglePosValueSize(key) > 5: |
|
format1Mapping = {g: values[key] for g in glyphs} |
|
result.append(buildSinglePosSubtable(format1Mapping, glyphMap)) |
|
handled.add(key) |
|
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
for key, glyphs in coverages.items(): |
|
if key not in handled: |
|
for g in glyphs: |
|
st = buildSinglePosSubtable({g: values[key]}, glyphMap) |
|
result.append(st) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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): |
|
|
|
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): |
|
|
|
return _DeviceTuple( |
|
device.DeltaFormat, |
|
device.StartSize, |
|
device.EndSize, |
|
() if device.DeltaFormat & 0x8000 else tuple(device.DeltaValue), |
|
) |
|
|
|
|
|
def _getSinglePosValueSize(valueKey): |
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
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): |
|
|
|
|
|
if not points: |
|
return None |
|
self = ot.AttachPoint() |
|
self.PointIndex = sorted(set(points)) |
|
self.PointCount = len(self.PointIndex) |
|
return self |
|
|
|
|
|
def buildCaretValueForCoord(coord): |
|
|
|
|
|
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): |
|
|
|
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): |
|
|
|
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): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
) |
|
|
|
|
|
axisRecords, axisValues = _buildAxisRecords( |
|
axes, ttFont, windowsNames=windowsNames, macNames=macNames |
|
) |
|
if not locations: |
|
statTable.Version = 0x00010001 |
|
else: |
|
|
|
|
|
statTable.Version = 0x00010002 |
|
multiAxisValues = _buildAxisValuesFormat4( |
|
locations, axes, ttFont, windowsNames=windowsNames, macNames=macNames |
|
) |
|
axisValues = multiAxisValues + axisValues |
|
ttFont["name"].names.sort() |
|
|
|
|
|
axisRecordArray = ot.AxisRecordArray() |
|
axisRecordArray.Axis = axisRecords |
|
|
|
statTable.DesignAxisRecordSize = 8 |
|
statTable.DesignAxisRecord = axisRecordArray |
|
statTable.DesignAxisCount = len(axisRecords) |
|
|
|
statTable.AxisValueCount = 0 |
|
statTable.AxisValueArray = None |
|
if axisValues: |
|
|
|
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): |
|
|
|
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 |
|
|