2020-07-02 14:09:10 +01:00
|
|
|
from collections import namedtuple, OrderedDict
|
2020-05-09 16:08:11 +02:00
|
|
|
from fontTools.misc.fixedTools import fixedToFloat
|
2016-01-13 17:52:30 +00:00
|
|
|
from fontTools import ttLib
|
|
|
|
from fontTools.ttLib.tables import otTables as ot
|
|
|
|
from fontTools.ttLib.tables.otBase import ValueRecord, valueRecordFormatDict
|
2020-07-02 14:09:10 +01:00
|
|
|
from fontTools.ttLib.tables import otBase
|
|
|
|
from fontTools.otlLib.error import OpenTypeLibError
|
|
|
|
import logging
|
|
|
|
|
|
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
2016-01-13 17:52:30 +00:00
|
|
|
|
2016-01-14 17:10:45 +01:00
|
|
|
|
|
|
|
def buildCoverage(glyphs, glyphMap):
|
2016-01-22 19:27:33 +01:00
|
|
|
if not glyphs:
|
|
|
|
return None
|
2016-01-14 17:10:45 +01:00
|
|
|
self = ot.Coverage()
|
|
|
|
self.glyphs = sorted(glyphs, key=glyphMap.__getitem__)
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
2016-01-20 19:24:23 +01:00
|
|
|
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):
|
2016-01-22 19:38:20 +01:00
|
|
|
if subtables is None:
|
|
|
|
return None
|
|
|
|
subtables = [st for st in subtables if st is not None]
|
2016-01-20 19:24:23 +01:00
|
|
|
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
|
2016-01-22 19:57:22 +01:00
|
|
|
self.SubTableCount = len(self.SubTable)
|
2016-01-20 19:24:23 +01:00
|
|
|
if markFilterSet is not None:
|
|
|
|
assert self.LookupFlag & LOOKUP_FLAG_USE_MARK_FILTERING_SET, \
|
|
|
|
("if markFilterSet is not None, flags must set "
|
|
|
|
"LOOKUP_FLAG_USE_MARK_FILTERING_SET; flags=0x%04x" % flags)
|
|
|
|
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
|
|
|
|
|
|
|
|
|
2020-07-02 14:09:10 +01:00
|
|
|
class LookupBuilder(object):
|
|
|
|
SUBTABLE_BREAK_ = "SUBTABLE_BREAK"
|
|
|
|
|
|
|
|
def __init__(self, font, location, table, lookup_type):
|
|
|
|
self.font = font
|
|
|
|
self.glyphMap = font.getReverseGlyphMap()
|
|
|
|
self.location = location
|
|
|
|
self.table, self.lookup_type = table, lookup_type
|
|
|
|
self.lookupflag = 0
|
|
|
|
self.markFilterSet = None
|
|
|
|
self.lookup_index = None # assigned when making final tables
|
|
|
|
assert table in ('GPOS', 'GSUB')
|
|
|
|
|
|
|
|
def equals(self, other):
|
|
|
|
return (isinstance(other, self.__class__) and
|
|
|
|
self.table == other.table and
|
|
|
|
self.lookupflag == other.lookupflag and
|
|
|
|
self.markFilterSet == other.markFilterSet)
|
|
|
|
|
|
|
|
def inferGlyphClasses(self):
|
|
|
|
"""Infers glyph glasses for the GDEF table, such as {"cedilla":3}."""
|
|
|
|
return {}
|
|
|
|
|
|
|
|
def getAlternateGlyphs(self):
|
|
|
|
"""Helper for building 'aalt' features."""
|
|
|
|
return {}
|
|
|
|
|
|
|
|
def buildLookup_(self, subtables):
|
|
|
|
return buildLookup(subtables, self.lookupflag, self.markFilterSet)
|
|
|
|
|
|
|
|
def buildMarkClasses_(self, marks):
|
|
|
|
"""{"cedilla": ("BOTTOM", ast.Anchor), ...} --> {"BOTTOM":0, "TOP":1}
|
|
|
|
|
|
|
|
Helper for MarkBasePostBuilder, MarkLigPosBuilder, and
|
|
|
|
MarkMarkPosBuilder. Seems to return the same numeric IDs
|
|
|
|
for mark classes as the AFDKO makeotf tool.
|
|
|
|
"""
|
|
|
|
ids = {}
|
|
|
|
for mark in sorted(marks.keys(), key=self.font.getGlyphID):
|
|
|
|
markClassName, _markAnchor = marks[mark]
|
|
|
|
if markClassName not in ids:
|
|
|
|
ids[markClassName] = len(ids)
|
|
|
|
return ids
|
|
|
|
|
|
|
|
def setBacktrackCoverage_(self, prefix, subtable):
|
|
|
|
subtable.BacktrackGlyphCount = len(prefix)
|
|
|
|
subtable.BacktrackCoverage = []
|
|
|
|
for p in reversed(prefix):
|
|
|
|
coverage = buildCoverage(p, self.glyphMap)
|
|
|
|
subtable.BacktrackCoverage.append(coverage)
|
|
|
|
|
|
|
|
def setLookAheadCoverage_(self, suffix, subtable):
|
|
|
|
subtable.LookAheadGlyphCount = len(suffix)
|
|
|
|
subtable.LookAheadCoverage = []
|
|
|
|
for s in suffix:
|
|
|
|
coverage = buildCoverage(s, self.glyphMap)
|
|
|
|
subtable.LookAheadCoverage.append(coverage)
|
|
|
|
|
|
|
|
def setInputCoverage_(self, glyphs, subtable):
|
|
|
|
subtable.InputGlyphCount = len(glyphs)
|
|
|
|
subtable.InputCoverage = []
|
|
|
|
for g in glyphs:
|
|
|
|
coverage = buildCoverage(g, self.glyphMap)
|
|
|
|
subtable.InputCoverage.append(coverage)
|
|
|
|
|
|
|
|
def 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):
|
|
|
|
log.warning(OpenTypeLibError(
|
|
|
|
'unsupported "subtable" statement for lookup type',
|
|
|
|
location
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
class AlternateSubstBuilder(LookupBuilder):
|
|
|
|
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):
|
|
|
|
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 ChainContextPosBuilder(LookupBuilder):
|
|
|
|
def __init__(self, font, location):
|
|
|
|
LookupBuilder.__init__(self, font, location, 'GPOS', 8)
|
|
|
|
self.rules = [] # (prefix, input, suffix, lookups)
|
|
|
|
|
|
|
|
def equals(self, other):
|
|
|
|
return (LookupBuilder.equals(self, other) and
|
|
|
|
self.rules == other.rules)
|
|
|
|
|
|
|
|
def build(self):
|
|
|
|
subtables = []
|
|
|
|
for (prefix, glyphs, suffix, lookups) in self.rules:
|
|
|
|
if prefix == self.SUBTABLE_BREAK_:
|
|
|
|
continue
|
|
|
|
st = ot.ChainContextPos()
|
|
|
|
subtables.append(st)
|
|
|
|
st.Format = 3
|
|
|
|
self.setBacktrackCoverage_(prefix, st)
|
|
|
|
self.setLookAheadCoverage_(suffix, st)
|
|
|
|
self.setInputCoverage_(glyphs, st)
|
|
|
|
|
|
|
|
st.PosCount = 0
|
|
|
|
st.PosLookupRecord = []
|
|
|
|
for sequenceIndex, lookupList in enumerate(lookups):
|
|
|
|
if lookupList is not None:
|
|
|
|
if not isinstance(lookupList, list):
|
|
|
|
# Can happen with synthesised lookups
|
|
|
|
lookupList = [ lookupList ]
|
|
|
|
for l in lookupList:
|
|
|
|
st.PosCount += 1
|
|
|
|
if l.lookup_index is None:
|
|
|
|
raise OpenTypeLibError('Missing index of the specified '
|
|
|
|
'lookup, might be a substitution lookup',
|
|
|
|
self.location)
|
|
|
|
rec = ot.PosLookupRecord()
|
|
|
|
rec.SequenceIndex = sequenceIndex
|
|
|
|
rec.LookupListIndex = l.lookup_index
|
|
|
|
st.PosLookupRecord.append(rec)
|
|
|
|
return self.buildLookup_(subtables)
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
def add_subtable_break(self, location):
|
|
|
|
self.rules.append((self.SUBTABLE_BREAK_, self.SUBTABLE_BREAK_,
|
|
|
|
self.SUBTABLE_BREAK_, [self.SUBTABLE_BREAK_]))
|
|
|
|
|
|
|
|
|
|
|
|
class ChainContextSubstBuilder(LookupBuilder):
|
|
|
|
def __init__(self, font, location):
|
|
|
|
LookupBuilder.__init__(self, font, location, 'GSUB', 6)
|
|
|
|
self.substitutions = [] # (prefix, input, suffix, lookups)
|
|
|
|
|
|
|
|
def equals(self, other):
|
|
|
|
return (LookupBuilder.equals(self, other) and
|
|
|
|
self.substitutions == other.substitutions)
|
|
|
|
|
|
|
|
def build(self):
|
|
|
|
subtables = []
|
|
|
|
for (prefix, input, suffix, lookups) in self.substitutions:
|
|
|
|
if prefix == self.SUBTABLE_BREAK_:
|
|
|
|
continue
|
|
|
|
st = ot.ChainContextSubst()
|
|
|
|
subtables.append(st)
|
|
|
|
st.Format = 3
|
|
|
|
self.setBacktrackCoverage_(prefix, st)
|
|
|
|
self.setLookAheadCoverage_(suffix, st)
|
|
|
|
self.setInputCoverage_(input, st)
|
|
|
|
|
|
|
|
st.SubstCount = 0
|
|
|
|
st.SubstLookupRecord = []
|
|
|
|
for sequenceIndex, lookupList in enumerate(lookups):
|
|
|
|
if lookupList is not None:
|
|
|
|
if not isinstance(lookupList, list):
|
|
|
|
# Can happen with synthesised lookups
|
|
|
|
lookupList = [ lookupList ]
|
|
|
|
for l in lookupList:
|
|
|
|
st.SubstCount += 1
|
|
|
|
if l.lookup_index is None:
|
|
|
|
raise OpenTypeLibError('Missing index of the specified '
|
|
|
|
'lookup, might be a positioning lookup',
|
|
|
|
self.location)
|
|
|
|
rec = ot.SubstLookupRecord()
|
|
|
|
rec.SequenceIndex = sequenceIndex
|
|
|
|
rec.LookupListIndex = l.lookup_index
|
|
|
|
st.SubstLookupRecord.append(rec)
|
|
|
|
return self.buildLookup_(subtables)
|
|
|
|
|
|
|
|
def getAlternateGlyphs(self):
|
|
|
|
result = {}
|
|
|
|
for (_, _, _, lookuplist) in self.substitutions:
|
|
|
|
if lookuplist == self.SUBTABLE_BREAK_:
|
|
|
|
continue
|
|
|
|
for lookups in lookuplist:
|
|
|
|
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():
|
|
|
|
result.setdefault(glyph, set()).update(replacements)
|
|
|
|
return result
|
|
|
|
|
|
|
|
def find_chainable_single_subst(self, glyphs):
|
|
|
|
"""Helper for add_single_subst_chained_()"""
|
|
|
|
res = None
|
|
|
|
for _, _, _, substitutions in self.substitutions[::-1]:
|
|
|
|
if substitutions == self.SUBTABLE_BREAK_:
|
|
|
|
return res
|
|
|
|
for sub in substitutions:
|
|
|
|
if (isinstance(sub, SingleSubstBuilder) and
|
|
|
|
not any(g in glyphs for g in sub.mapping.keys())):
|
|
|
|
res = sub
|
|
|
|
return res
|
|
|
|
|
|
|
|
def add_subtable_break(self, location):
|
|
|
|
self.substitutions.append((self.SUBTABLE_BREAK_, self.SUBTABLE_BREAK_,
|
|
|
|
self.SUBTABLE_BREAK_, self.SUBTABLE_BREAK_))
|
|
|
|
|
|
|
|
|
|
|
|
class LigatureSubstBuilder(LookupBuilder):
|
|
|
|
def __init__(self, font, location):
|
|
|
|
LookupBuilder.__init__(self, font, location, 'GSUB', 4)
|
|
|
|
self.ligatures = OrderedDict() # {('f','f','i'): 'f_f_i'}
|
|
|
|
|
|
|
|
def equals(self, other):
|
|
|
|
return (LookupBuilder.equals(self, other) and
|
|
|
|
self.ligatures == other.ligatures)
|
|
|
|
|
|
|
|
def build(self):
|
|
|
|
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):
|
|
|
|
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):
|
|
|
|
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):
|
|
|
|
for glyph in glyphs:
|
|
|
|
self.attachments[glyph] = (entryAnchor, exitAnchor)
|
|
|
|
|
|
|
|
def build(self):
|
|
|
|
st = buildCursivePosSubtable(self.attachments, self.glyphMap)
|
|
|
|
return self.buildLookup_([st])
|
|
|
|
|
|
|
|
|
|
|
|
class MarkBasePosBuilder(LookupBuilder):
|
|
|
|
def __init__(self, font, location):
|
|
|
|
LookupBuilder.__init__(self, font, location, 'GPOS', 4)
|
|
|
|
self.marks = {} # glyphName -> (markClassName, anchor)
|
|
|
|
self.bases = {} # glyphName -> {markClassName: anchor}
|
|
|
|
|
|
|
|
def equals(self, other):
|
|
|
|
return (LookupBuilder.equals(self, other) and
|
|
|
|
self.marks == other.marks and
|
|
|
|
self.bases == other.bases)
|
|
|
|
|
|
|
|
def inferGlyphClasses(self):
|
|
|
|
result = {glyph: 1 for glyph in self.bases}
|
|
|
|
result.update({glyph: 3 for glyph in self.marks})
|
|
|
|
return result
|
|
|
|
|
|
|
|
def build(self):
|
|
|
|
markClasses = self.buildMarkClasses_(self.marks)
|
|
|
|
marks = {mark: (markClasses[mc], anchor)
|
|
|
|
for mark, (mc, anchor) in self.marks.items()}
|
|
|
|
bases = {}
|
|
|
|
for glyph, anchors in self.bases.items():
|
|
|
|
bases[glyph] = {markClasses[mc]: anchor
|
|
|
|
for (mc, anchor) in anchors.items()}
|
|
|
|
subtables = buildMarkBasePos(marks, bases, self.glyphMap)
|
|
|
|
return self.buildLookup_(subtables)
|
|
|
|
|
|
|
|
|
|
|
|
class MarkLigPosBuilder(LookupBuilder):
|
|
|
|
def __init__(self, font, location):
|
|
|
|
LookupBuilder.__init__(self, font, location, 'GPOS', 5)
|
|
|
|
self.marks = {} # glyphName -> (markClassName, anchor)
|
|
|
|
self.ligatures = {} # glyphName -> [{markClassName: anchor}, ...]
|
|
|
|
|
|
|
|
def equals(self, other):
|
|
|
|
return (LookupBuilder.equals(self, other) and
|
|
|
|
self.marks == other.marks and
|
|
|
|
self.ligatures == other.ligatures)
|
|
|
|
|
|
|
|
def inferGlyphClasses(self):
|
|
|
|
result = {glyph: 2 for glyph in self.ligatures}
|
|
|
|
result.update({glyph: 3 for glyph in self.marks})
|
|
|
|
return result
|
|
|
|
|
|
|
|
def build(self):
|
|
|
|
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):
|
|
|
|
def __init__(self, font, location):
|
|
|
|
LookupBuilder.__init__(self, font, location, 'GPOS', 6)
|
|
|
|
self.marks = {} # glyphName -> (markClassName, anchor)
|
|
|
|
self.baseMarks = {} # glyphName -> {markClassName: anchor}
|
|
|
|
|
|
|
|
def equals(self, other):
|
|
|
|
return (LookupBuilder.equals(self, other) and
|
|
|
|
self.marks == other.marks and
|
|
|
|
self.baseMarks == other.baseMarks)
|
|
|
|
|
|
|
|
def inferGlyphClasses(self):
|
|
|
|
result = {glyph: 3 for glyph in self.baseMarks}
|
|
|
|
result.update({glyph: 3 for glyph in self.marks})
|
|
|
|
return result
|
|
|
|
|
|
|
|
def build(self):
|
|
|
|
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):
|
|
|
|
def __init__(self, font, location):
|
|
|
|
LookupBuilder.__init__(self, font, location, 'GSUB', 8)
|
|
|
|
self.substitutions = [] # (prefix, suffix, mapping)
|
|
|
|
|
|
|
|
def equals(self, other):
|
|
|
|
return (LookupBuilder.equals(self, other) and
|
|
|
|
self.substitutions == other.substitutions)
|
|
|
|
|
|
|
|
def build(self):
|
|
|
|
subtables = []
|
|
|
|
for prefix, suffix, mapping in self.substitutions:
|
|
|
|
st = ot.ReverseChainSingleSubst()
|
|
|
|
st.Format = 1
|
|
|
|
self.setBacktrackCoverage_(prefix, st)
|
|
|
|
self.setLookAheadCoverage_(suffix, st)
|
|
|
|
st.Coverage = buildCoverage(mapping.keys(), self.glyphMap)
|
|
|
|
st.GlyphCount = len(mapping)
|
|
|
|
st.Substitute = [mapping[g] for g in st.Coverage.glyphs]
|
|
|
|
subtables.append(st)
|
|
|
|
return self.buildLookup_(subtables)
|
|
|
|
|
|
|
|
def add_subtable_break(self, location):
|
|
|
|
# Nothing to do here, each substitution is in its own subtable.
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class SingleSubstBuilder(LookupBuilder):
|
|
|
|
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):
|
|
|
|
subtables = self.build_subst_subtables(self.mapping,
|
|
|
|
buildSingleSubstSubtable)
|
|
|
|
return self.buildLookup_(subtables)
|
|
|
|
|
|
|
|
def getAlternateGlyphs(self):
|
|
|
|
return {glyph: set([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):
|
|
|
|
def __init__(self, builder, valueFormat1, valueFormat2):
|
|
|
|
self.builder_ = builder
|
|
|
|
self.classDef1_, self.classDef2_ = None, None
|
|
|
|
self.values_ = {} # (glyphclass1, glyphclass2) --> (value1, value2)
|
|
|
|
self.valueFormat1_, self.valueFormat2_ = valueFormat1, valueFormat2
|
|
|
|
self.forceSubtableBreak_ = False
|
|
|
|
self.subtables_ = []
|
|
|
|
|
|
|
|
def addPair(self, gc1, value1, gc2, value2):
|
|
|
|
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):
|
|
|
|
self.forceSubtableBreak_ = True
|
|
|
|
|
|
|
|
def subtables(self):
|
|
|
|
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):
|
|
|
|
def __init__(self, font, location):
|
|
|
|
LookupBuilder.__init__(self, font, location, 'GPOS', 2)
|
|
|
|
self.pairs = [] # [(gc1, value1, gc2, value2)*]
|
|
|
|
self.glyphPairs = {} # (glyph1, glyph2) --> (value1, value2)
|
|
|
|
self.locations = {} # (gc1, gc2) --> (filepath, line, column)
|
|
|
|
|
|
|
|
def addClassPair(self, location, glyphclass1, value1, glyphclass2, value2):
|
|
|
|
self.pairs.append((glyphclass1, value1, glyphclass2, value2))
|
|
|
|
|
|
|
|
def addGlyphPair(self, location, glyph1, value1, glyph2, value2):
|
|
|
|
key = (glyph1, glyph2)
|
|
|
|
oldValue = self.glyphPairs.get(key, None)
|
|
|
|
if oldValue is not None:
|
|
|
|
# the Feature File spec explicitly allows specific pairs generated
|
|
|
|
# by an 'enum' rule to be overridden by preceding single pairs
|
|
|
|
otherLoc = self.locations[key]
|
|
|
|
log.debug(
|
|
|
|
'Already defined position for pair %s %s at %s; '
|
|
|
|
'choosing the first value',
|
|
|
|
glyph1, glyph2, otherLoc)
|
|
|
|
else:
|
|
|
|
self.glyphPairs[key] = (value1, value2)
|
|
|
|
self.locations[key] = location
|
|
|
|
|
|
|
|
def add_subtable_break(self, location):
|
|
|
|
self.pairs.append((self.SUBTABLE_BREAK_, self.SUBTABLE_BREAK_,
|
|
|
|
self.SUBTABLE_BREAK_, self.SUBTABLE_BREAK_))
|
|
|
|
|
|
|
|
def equals(self, other):
|
|
|
|
return (LookupBuilder.equals(self, other) and
|
|
|
|
self.glyphPairs == other.glyphPairs and
|
|
|
|
self.pairs == other.pairs)
|
|
|
|
|
|
|
|
def build(self):
|
|
|
|
builders = {}
|
|
|
|
builder = None
|
|
|
|
for glyphclass1, value1, glyphclass2, value2 in self.pairs:
|
|
|
|
if glyphclass1 is self.SUBTABLE_BREAK_:
|
|
|
|
if builder is not None:
|
|
|
|
builder.addSubtableBreak()
|
|
|
|
continue
|
|
|
|
valFormat1, valFormat2 = 0, 0
|
|
|
|
if value1:
|
|
|
|
valFormat1 = value1.getFormat()
|
|
|
|
if value2:
|
|
|
|
valFormat2 = value2.getFormat()
|
|
|
|
builder = builders.get((valFormat1, valFormat2))
|
|
|
|
if builder is None:
|
|
|
|
builder = ClassPairPosSubtableBuilder(
|
|
|
|
self, valFormat1, valFormat2)
|
|
|
|
builders[(valFormat1, valFormat2)] = builder
|
|
|
|
builder.addPair(glyphclass1, value1, glyphclass2, value2)
|
|
|
|
subtables = []
|
|
|
|
if self.glyphPairs:
|
|
|
|
subtables.extend(
|
|
|
|
buildPairPosGlyphs(self.glyphPairs, self.glyphMap))
|
|
|
|
for key in sorted(builders.keys()):
|
|
|
|
subtables.extend(builders[key].subtables())
|
|
|
|
return self.buildLookup_(subtables)
|
|
|
|
|
|
|
|
|
|
|
|
class SinglePosBuilder(LookupBuilder):
|
|
|
|
def __init__(self, font, location):
|
|
|
|
LookupBuilder.__init__(self, font, location, 'GPOS', 1)
|
|
|
|
self.locations = {} # glyph -> (filename, line, column)
|
|
|
|
self.mapping = {} # glyph -> ot.ValueRecord
|
|
|
|
|
|
|
|
def add_pos(self, location, glyph, otValueRecord):
|
|
|
|
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):
|
|
|
|
subtables = buildSinglePos(self.mapping, self.glyphMap)
|
|
|
|
return self.buildLookup_(subtables)
|
|
|
|
|
|
|
|
|
2016-01-13 17:52:30 +00:00
|
|
|
# GSUB
|
|
|
|
|
2016-01-14 11:46:25 +01:00
|
|
|
|
2016-01-22 19:32:45 +01:00
|
|
|
def buildSingleSubstSubtable(mapping):
|
2016-01-22 19:38:20 +01:00
|
|
|
if not mapping:
|
|
|
|
return None
|
2016-01-14 11:46:25 +01:00
|
|
|
self = ot.SingleSubst()
|
|
|
|
self.mapping = dict(mapping)
|
|
|
|
return self
|
|
|
|
|
2016-01-13 17:52:30 +00:00
|
|
|
|
2016-01-22 19:32:45 +01:00
|
|
|
def buildMultipleSubstSubtable(mapping):
|
2016-01-22 19:38:20 +01:00
|
|
|
if not mapping:
|
|
|
|
return None
|
2016-01-14 11:46:25 +01:00
|
|
|
self = ot.MultipleSubst()
|
|
|
|
self.mapping = dict(mapping)
|
|
|
|
return self
|
|
|
|
|
2016-01-13 17:52:30 +00:00
|
|
|
|
2016-01-22 19:32:45 +01:00
|
|
|
def buildAlternateSubstSubtable(mapping):
|
2016-01-22 19:38:20 +01:00
|
|
|
if not mapping:
|
|
|
|
return None
|
2016-01-14 11:46:25 +01:00
|
|
|
self = ot.AlternateSubst()
|
|
|
|
self.alternates = dict(mapping)
|
|
|
|
return self
|
|
|
|
|
2016-01-13 17:52:30 +00:00
|
|
|
|
2016-01-14 12:27:39 +00:00
|
|
|
def _getLigatureKey(components):
|
2016-01-14 11:46:25 +01:00
|
|
|
"""Computes a key for ordering ligatures in a GSUB Type-4 lookup.
|
2016-01-14 10:27:54 +01:00
|
|
|
|
2016-01-14 11:46:25 +01:00
|
|
|
When building the OpenType lookup, we need to make sure that
|
|
|
|
the longest sequence of components is listed first, so we
|
|
|
|
use the negative length as the primary key for sorting.
|
2016-01-22 19:32:45 +01:00
|
|
|
To make buildLigatureSubstSubtable() deterministic, we use the
|
2016-01-14 11:46:25 +01:00
|
|
|
component sequence as the secondary key.
|
|
|
|
|
|
|
|
For example, this will sort (f,f,f) < (f,f,i) < (f,f) < (f,i) < (f,l).
|
|
|
|
"""
|
|
|
|
return (-len(components), components)
|
2016-01-14 10:27:54 +01:00
|
|
|
|
2016-01-14 16:25:28 +01:00
|
|
|
|
2016-01-22 19:32:45 +01:00
|
|
|
def buildLigatureSubstSubtable(mapping):
|
2016-01-22 19:38:20 +01:00
|
|
|
if not mapping:
|
|
|
|
return None
|
2016-01-14 11:46:25 +01:00
|
|
|
self = ot.LigatureSubst()
|
|
|
|
# The following single line can replace the rest of this function
|
|
|
|
# with fontTools >= 3.1:
|
|
|
|
# self.ligatures = dict(mapping)
|
|
|
|
self.ligatures = {}
|
2016-01-14 12:27:39 +00:00
|
|
|
for components in sorted(mapping.keys(), key=_getLigatureKey):
|
2016-01-14 11:46:25 +01:00
|
|
|
ligature = ot.Ligature()
|
|
|
|
ligature.Component = components[1:]
|
2016-01-22 19:57:22 +01:00
|
|
|
ligature.CompCount = len(ligature.Component) + 1
|
2016-01-14 11:46:25 +01:00
|
|
|
ligature.LigGlyph = mapping[components]
|
|
|
|
firstGlyph = components[0]
|
|
|
|
self.ligatures.setdefault(firstGlyph, []).append(ligature)
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
2016-01-14 12:28:32 +00:00
|
|
|
# GPOS
|
|
|
|
|
|
|
|
|
2016-01-14 13:08:26 +01:00
|
|
|
def buildAnchor(x, y, point=None, deviceX=None, deviceY=None):
|
|
|
|
self = ot.Anchor()
|
|
|
|
self.XCoordinate, self.YCoordinate = x, y
|
|
|
|
self.Format = 1
|
|
|
|
if point is not None:
|
|
|
|
self.AnchorPoint = point
|
|
|
|
self.Format = 2
|
2016-01-14 14:59:10 +00:00
|
|
|
if deviceX is not None or deviceY is not None:
|
2016-01-21 15:23:41 +01:00
|
|
|
assert self.Format == 1, \
|
|
|
|
"Either point, or both of deviceX/deviceY, must be None."
|
2016-01-14 13:08:26 +01:00
|
|
|
self.XDeviceTable = deviceX
|
|
|
|
self.YDeviceTable = deviceY
|
|
|
|
self.Format = 3
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
2016-01-22 10:57:03 +01:00
|
|
|
def buildBaseArray(bases, numMarkClasses, glyphMap):
|
|
|
|
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))
|
2016-01-22 19:57:22 +01:00
|
|
|
self.BaseCount = len(self.BaseRecord)
|
2016-01-22 10:57:03 +01:00
|
|
|
return self
|
|
|
|
|
|
|
|
|
2016-01-21 17:23:36 +01:00
|
|
|
def buildBaseRecord(anchors):
|
2020-07-02 14:09:10 +01:00
|
|
|
"""[ot.Anchor, ot.Anchor, ...] --> ot.BaseRecord"""
|
2016-01-21 17:23:36 +01:00
|
|
|
self = ot.BaseRecord()
|
|
|
|
self.BaseAnchor = anchors
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
2016-01-21 15:16:55 +01:00
|
|
|
def buildComponentRecord(anchors):
|
2020-07-02 14:09:10 +01:00
|
|
|
"""[ot.Anchor, ot.Anchor, ...] --> ot.ComponentRecord"""
|
2016-01-22 12:45:29 +01:00
|
|
|
if not anchors:
|
|
|
|
return None
|
2016-01-21 15:16:55 +01:00
|
|
|
self = ot.ComponentRecord()
|
|
|
|
self.LigatureAnchor = anchors
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
2016-01-22 19:38:20 +01:00
|
|
|
def buildCursivePosSubtable(attach, glyphMap):
|
2020-07-02 14:09:10 +01:00
|
|
|
"""{"alef": (entry, exit)} --> ot.CursivePos"""
|
2016-01-22 19:39:54 +01:00
|
|
|
if not attach:
|
|
|
|
return None
|
2016-01-14 17:54:47 +01:00
|
|
|
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)
|
2016-01-18 12:32:01 +01:00
|
|
|
self.EntryExitCount = len(self.EntryExitRecord)
|
2016-01-14 17:54:47 +01:00
|
|
|
return self
|
|
|
|
|
|
|
|
|
2016-01-22 19:45:56 +01:00
|
|
|
def buildDevice(deltas):
|
2020-07-02 14:09:10 +01:00
|
|
|
"""{8:+1, 10:-3, ...} --> ot.Device"""
|
2016-01-22 19:45:56 +01:00
|
|
|
if not deltas:
|
2016-02-02 08:58:28 +01:00
|
|
|
return None
|
2016-01-14 11:46:25 +01:00
|
|
|
self = ot.Device()
|
2016-01-22 19:45:56 +01:00
|
|
|
keys = deltas.keys()
|
|
|
|
self.StartSize = startSize = min(keys)
|
|
|
|
self.EndSize = endSize = max(keys)
|
2016-01-22 19:49:22 +01:00
|
|
|
assert 0 <= startSize <= endSize
|
2016-01-14 11:46:25 +01:00
|
|
|
self.DeltaValue = deltaValues = [
|
2016-01-22 19:45:56 +01:00
|
|
|
deltas.get(size, 0)
|
2016-01-14 11:46:25 +01:00
|
|
|
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
|
2016-01-14 16:25:28 +01:00
|
|
|
|
|
|
|
|
2016-01-22 14:12:53 +01:00
|
|
|
def buildLigatureArray(ligs, numMarkClasses, glyphMap):
|
|
|
|
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))
|
2016-01-22 19:57:22 +01:00
|
|
|
self.LigatureCount = len(self.LigatureAttach)
|
2016-01-22 14:12:53 +01:00
|
|
|
return self
|
|
|
|
|
|
|
|
|
2016-01-22 12:45:29 +01:00
|
|
|
def buildLigatureAttach(components):
|
|
|
|
"""[[Anchor, Anchor], [Anchor, Anchor, Anchor]] --> LigatureAttach"""
|
|
|
|
self = ot.LigatureAttach()
|
|
|
|
self.ComponentRecord = [buildComponentRecord(c) for c in components]
|
2016-01-22 19:57:22 +01:00
|
|
|
self.ComponentCount = len(self.ComponentRecord)
|
2016-01-22 12:45:29 +01:00
|
|
|
return self
|
|
|
|
|
|
|
|
|
2016-01-21 16:23:55 +01:00
|
|
|
def buildMarkArray(marks, glyphMap):
|
2020-07-02 14:09:10 +01:00
|
|
|
"""{"acute": (markClass, ot.Anchor)} --> ot.MarkArray"""
|
2016-01-21 16:23:55 +01:00
|
|
|
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)
|
2016-01-22 19:57:22 +01:00
|
|
|
self.MarkCount = len(self.MarkRecord)
|
2016-01-21 16:23:55 +01:00
|
|
|
return self
|
|
|
|
|
|
|
|
|
2016-01-22 11:53:34 +01:00
|
|
|
def buildMarkBasePos(marks, bases, glyphMap):
|
|
|
|
"""Build a list of MarkBasePos subtables.
|
|
|
|
|
2016-01-22 14:50:17 +01:00
|
|
|
a1, a2, a3, a4, a5 = buildAnchor(500, 100), ...
|
2016-01-22 11:53:34 +01:00
|
|
|
marks = {"acute": (0, a1), "grave": (0, a1), "cedilla": (1, a2)}
|
|
|
|
bases = {"a": {0: a3, 1: a5}, "b": {0: a4, 1: a5}}
|
|
|
|
"""
|
|
|
|
# TODO: Consider emitting multiple subtables to save space.
|
|
|
|
# Partition the marks and bases into disjoint subsets, so that
|
|
|
|
# MarkBasePos rules would only access glyphs from a single
|
|
|
|
# subset. This would likely lead to smaller mark/base
|
|
|
|
# matrices, so we might be able to omit many of the empty
|
|
|
|
# anchor tables that we currently produce. Of course, this
|
|
|
|
# would only work if the MarkBasePos rules of real-world fonts
|
|
|
|
# allow partitioning into multiple subsets. We should find out
|
|
|
|
# whether this is the case; if so, implement the optimization.
|
|
|
|
# On the other hand, a very large number of subtables could
|
|
|
|
# slow down layout engines; so this would need profiling.
|
|
|
|
return [buildMarkBasePosSubtable(marks, bases, glyphMap)]
|
|
|
|
|
|
|
|
|
|
|
|
def buildMarkBasePosSubtable(marks, bases, glyphMap):
|
|
|
|
"""Build a single MarkBasePos subtable.
|
|
|
|
|
2016-01-22 14:50:17 +01:00
|
|
|
a1, a2, a3, a4, a5 = buildAnchor(500, 100), ...
|
2016-01-22 11:53:34 +01:00
|
|
|
marks = {"acute": (0, a1), "grave": (0, a1), "cedilla": (1, a2)}
|
|
|
|
bases = {"a": {0: a3, 1: a5}, "b": {0: a4, 1: a5}}
|
|
|
|
"""
|
|
|
|
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
|
|
|
|
|
|
|
|
|
2016-01-22 14:50:17 +01:00
|
|
|
def buildMarkLigPos(marks, ligs, glyphMap):
|
|
|
|
"""Build a list of MarkLigPos subtables.
|
|
|
|
|
|
|
|
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}, {0: a4, 1: a5}], "c_t": [{...}, {...}]}
|
|
|
|
"""
|
|
|
|
# TODO: Consider splitting into multiple subtables to save space,
|
|
|
|
# as with MarkBasePos, this would be a trade-off that would need
|
|
|
|
# profiling. And, depending on how typical fonts are structured,
|
|
|
|
# it might not be worth doing at all.
|
|
|
|
return [buildMarkLigPosSubtable(marks, ligs, glyphMap)]
|
|
|
|
|
|
|
|
|
|
|
|
def buildMarkLigPosSubtable(marks, ligs, glyphMap):
|
|
|
|
"""Build a single MarkLigPos subtable.
|
|
|
|
|
|
|
|
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}, {0: a4, 1: a5}], "c_t": [{...}, {...}]}
|
|
|
|
"""
|
|
|
|
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
|
|
|
|
|
|
|
|
|
2016-01-21 11:29:32 +01:00
|
|
|
def buildMarkRecord(classID, anchor):
|
|
|
|
assert isinstance(classID, int)
|
|
|
|
assert isinstance(anchor, ot.Anchor)
|
|
|
|
self = ot.MarkRecord()
|
|
|
|
self.Class = classID
|
|
|
|
self.MarkAnchor = anchor
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
2016-01-21 13:05:36 +01:00
|
|
|
def buildMark2Record(anchors):
|
2020-07-02 14:09:10 +01:00
|
|
|
"""[ot.Anchor, ot.Anchor, ...] --> ot.Mark2Record"""
|
2016-01-21 13:05:36 +01:00
|
|
|
self = ot.Mark2Record()
|
|
|
|
self.Mark2Anchor = anchors
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
2016-02-02 15:17:01 +01:00
|
|
|
def _getValueFormat(f, values, i):
|
|
|
|
"""Helper for buildPairPos{Glyphs|Classes}Subtable."""
|
|
|
|
if f is not None:
|
|
|
|
return f
|
|
|
|
mask = 0
|
|
|
|
for value in values:
|
|
|
|
if value is not None and value[i] is not None:
|
|
|
|
mask |= value[i].getFormat()
|
|
|
|
return mask
|
|
|
|
|
|
|
|
|
|
|
|
def buildPairPosClassesSubtable(pairs, glyphMap,
|
|
|
|
valueFormat1=None, valueFormat2=None):
|
|
|
|
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
|
|
|
|
self.ValueFormat1 = _getValueFormat(valueFormat1, pairs.values(), 0)
|
|
|
|
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()
|
|
|
|
rec2.Value1, rec2.Value2 = pairs.get((c1, c2), (None, None))
|
|
|
|
rec1.Class2Record.append(rec2)
|
|
|
|
self.Class1Count = len(self.Class1Record)
|
|
|
|
self.Class2Count = len(classes2)
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
2016-02-02 10:30:33 +01:00
|
|
|
def buildPairPosGlyphs(pairs, glyphMap):
|
|
|
|
p = {} # (formatA, formatB) --> {(glyphA, glyphB): (valA, valB)}
|
|
|
|
for (glyphA, glyphB), (valA, valB) in pairs.items():
|
|
|
|
formatA = valA.getFormat() if valA is not None else 0
|
|
|
|
formatB = valB.getFormat() if valB is not None else 0
|
|
|
|
pos = p.setdefault((formatA, formatB), {})
|
|
|
|
pos[(glyphA, glyphB)] = (valA, valB)
|
|
|
|
return [
|
|
|
|
buildPairPosGlyphsSubtable(pos, glyphMap, formatA, formatB)
|
|
|
|
for ((formatA, formatB), pos) in sorted(p.items())]
|
|
|
|
|
|
|
|
|
2016-02-02 08:58:28 +01:00
|
|
|
def buildPairPosGlyphsSubtable(pairs, glyphMap,
|
|
|
|
valueFormat1=None, valueFormat2=None):
|
|
|
|
self = ot.PairPos()
|
|
|
|
self.Format = 1
|
2016-02-02 15:17:01 +01:00
|
|
|
self.ValueFormat1 = _getValueFormat(valueFormat1, pairs.values(), 0)
|
|
|
|
self.ValueFormat2 = _getValueFormat(valueFormat2, pairs.values(), 1)
|
2016-02-02 08:58:28 +01:00
|
|
|
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 = val1 if val1 and val1.getFormat() != 0 else None
|
|
|
|
pvr.Value2 = val2 if val2 and val2.getFormat() != 0 else None
|
|
|
|
ps.PairValueRecord.append(pvr)
|
|
|
|
ps.PairValueCount = len(ps.PairValueRecord)
|
|
|
|
self.PairSetCount = len(self.PairSet)
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
2016-01-19 14:22:28 +01:00
|
|
|
def buildSinglePos(mapping, glyphMap):
|
2020-07-02 14:09:10 +01:00
|
|
|
"""{"glyph": ValueRecord} --> [ot.SinglePos*]"""
|
2016-01-19 14:22:28 +01:00
|
|
|
result, handled = [], set()
|
|
|
|
# In SinglePos format 1, the covered glyphs all share the same ValueRecord.
|
|
|
|
# In format 2, each glyph has its own ValueRecord, but these records
|
|
|
|
# all have the same properties (eg., all have an X but no Y placement).
|
|
|
|
coverages, masks, values = {}, {}, {}
|
|
|
|
for glyph, value in mapping.items():
|
|
|
|
key = _getSinglePosValueKey(value)
|
|
|
|
coverages.setdefault(key, []).append(glyph)
|
|
|
|
masks.setdefault(key[0], []).append(key)
|
|
|
|
values[key] = value
|
|
|
|
|
|
|
|
# If a ValueRecord is shared between multiple glyphs, we generate
|
|
|
|
# a SinglePos format 1 subtable; that is the most compact form.
|
|
|
|
for key, glyphs in coverages.items():
|
2019-04-15 14:24:25 +07:00
|
|
|
# 5 ushorts is the length of introducing another sublookup
|
2019-03-15 10:50:13 +07:00
|
|
|
if len(glyphs) * _getSinglePosValueSize(key) > 5:
|
2016-01-19 15:38:33 +01:00
|
|
|
format1Mapping = {g: values[key] for g in glyphs}
|
|
|
|
result.append(buildSinglePosSubtable(format1Mapping, glyphMap))
|
2016-01-19 14:22:28 +01:00
|
|
|
handled.add(key)
|
|
|
|
|
|
|
|
# In the remaining ValueRecords, look for those whose valueFormat
|
|
|
|
# (the set of used properties) is shared between multiple records.
|
|
|
|
# These will get encoded in format 2.
|
|
|
|
for valueFormat, keys in masks.items():
|
|
|
|
f2 = [k for k in keys if k not in handled]
|
|
|
|
if len(f2) > 1:
|
2019-03-15 10:50:13 +07:00
|
|
|
format2Mapping = {}
|
|
|
|
for k in f2:
|
|
|
|
format2Mapping.update((g, values[k]) for g in coverages[k])
|
2016-01-19 15:38:33 +01:00
|
|
|
result.append(buildSinglePosSubtable(format2Mapping, glyphMap))
|
2016-01-19 14:22:28 +01:00
|
|
|
handled.update(f2)
|
|
|
|
|
2019-04-15 14:24:25 +07:00
|
|
|
# The remaining ValueRecords are only used by a few glyphs, normally
|
|
|
|
# one. We encode these in format 1 again.
|
2016-01-19 14:22:28 +01:00
|
|
|
for key, glyphs in coverages.items():
|
|
|
|
if key not in handled:
|
2019-03-15 10:50:13 +07:00
|
|
|
for g in glyphs:
|
|
|
|
st = buildSinglePosSubtable({g: values[key]}, glyphMap)
|
2016-01-19 15:38:33 +01:00
|
|
|
result.append(st)
|
2016-01-19 14:22:28 +01:00
|
|
|
|
|
|
|
# When the OpenType layout engine traverses the subtables, it will
|
|
|
|
# stop after the first matching subtable. Therefore, we sort the
|
|
|
|
# resulting subtables by decreasing coverage size; this increases
|
|
|
|
# the chance that the layout engine can do an early exit. (Of course,
|
|
|
|
# this would only be true if all glyphs were equally frequent, which
|
|
|
|
# is not really the case; but we do not know their distribution).
|
|
|
|
# If two subtables cover the same number of glyphs, we sort them
|
|
|
|
# by glyph ID so that our output is deterministic.
|
|
|
|
result.sort(key=lambda t: _getSinglePosTableKey(t, glyphMap))
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
2016-01-19 15:38:33 +01:00
|
|
|
def buildSinglePosSubtable(values, glyphMap):
|
2020-07-02 14:09:10 +01:00
|
|
|
"""{glyphName: otBase.ValueRecord} --> ot.SinglePos"""
|
2016-01-19 15:38:33 +01:00
|
|
|
self = ot.SinglePos()
|
|
|
|
self.Coverage = buildCoverage(values.keys(), glyphMap)
|
|
|
|
valueRecords = [values[g] for g in self.Coverage.glyphs]
|
|
|
|
self.ValueFormat = 0
|
|
|
|
for v in valueRecords:
|
|
|
|
self.ValueFormat |= v.getFormat()
|
|
|
|
if all(v == valueRecords[0] for v in valueRecords):
|
|
|
|
self.Format = 1
|
2016-01-19 21:59:37 +01:00
|
|
|
if self.ValueFormat != 0:
|
|
|
|
self.Value = valueRecords[0]
|
2016-01-22 13:43:05 +01:00
|
|
|
else:
|
|
|
|
self.Value = None
|
2016-01-19 15:38:33 +01:00
|
|
|
else:
|
|
|
|
self.Format = 2
|
|
|
|
self.Value = valueRecords
|
|
|
|
self.ValueCount = len(self.Value)
|
|
|
|
return self
|
2016-01-19 14:22:28 +01:00
|
|
|
|
|
|
|
|
|
|
|
def _getSinglePosTableKey(subtable, glyphMap):
|
|
|
|
assert isinstance(subtable, ot.SinglePos), subtable
|
|
|
|
glyphs = subtable.Coverage.glyphs
|
|
|
|
return (-len(glyphs), glyphMap[glyphs[0]])
|
|
|
|
|
|
|
|
|
|
|
|
def _getSinglePosValueKey(valueRecord):
|
|
|
|
"""otBase.ValueRecord --> (2, ("YPlacement": 12))"""
|
|
|
|
assert isinstance(valueRecord, ValueRecord), valueRecord
|
|
|
|
valueFormat, result = 0, []
|
|
|
|
for name, value in valueRecord.__dict__.items():
|
|
|
|
if isinstance(value, ot.Device):
|
2019-06-18 16:00:39 +01:00
|
|
|
result.append((name, _makeDeviceTuple(value)))
|
2016-01-19 14:22:28 +01:00
|
|
|
else:
|
|
|
|
result.append((name, value))
|
|
|
|
valueFormat |= valueRecordFormatDict[name][0]
|
|
|
|
result.sort()
|
|
|
|
result.insert(0, valueFormat)
|
|
|
|
return tuple(result)
|
|
|
|
|
|
|
|
|
2019-06-18 16:00:39 +01:00
|
|
|
_DeviceTuple = namedtuple("_DeviceTuple", "DeltaFormat StartSize EndSize DeltaValue")
|
|
|
|
|
|
|
|
|
2016-01-19 14:22:28 +01:00
|
|
|
def _makeDeviceTuple(device):
|
2020-07-02 14:09:10 +01:00
|
|
|
"""ot.Device --> tuple, for making device tables unique"""
|
2019-06-18 16:00:39 +01:00
|
|
|
return _DeviceTuple(
|
|
|
|
device.DeltaFormat,
|
|
|
|
device.StartSize,
|
|
|
|
device.EndSize,
|
|
|
|
() if device.DeltaFormat & 0x8000 else tuple(device.DeltaValue)
|
|
|
|
)
|
2016-01-19 14:22:28 +01:00
|
|
|
|
2019-06-12 14:31:38 -07:00
|
|
|
|
2019-03-15 10:50:13 +07:00
|
|
|
def _getSinglePosValueSize(valueKey):
|
2019-04-15 14:24:25 +07:00
|
|
|
"""Returns how many ushorts this valueKey (short form of ValueRecord) takes up"""
|
2019-03-15 10:50:13 +07:00
|
|
|
count = 0
|
2019-06-18 16:00:39 +01:00
|
|
|
for _, v in valueKey[1:]:
|
|
|
|
if isinstance(v, _DeviceTuple):
|
|
|
|
count += len(v.DeltaValue) + 3
|
2019-03-15 10:50:13 +07:00
|
|
|
else:
|
|
|
|
count += 1
|
|
|
|
return count
|
2016-01-19 14:22:28 +01:00
|
|
|
|
2016-01-14 16:25:28 +01:00
|
|
|
def buildValue(value):
|
|
|
|
self = ValueRecord()
|
|
|
|
for k, v in value.items():
|
|
|
|
setattr(self, k, v)
|
|
|
|
return self
|
2016-01-19 22:38:23 +01:00
|
|
|
|
|
|
|
|
|
|
|
# GDEF
|
|
|
|
|
|
|
|
def buildAttachList(attachPoints, glyphMap):
|
2020-07-02 14:09:10 +01:00
|
|
|
"""{"glyphName": [4, 23]} --> ot.AttachList, or None"""
|
2016-01-19 23:25:47 +01:00
|
|
|
if not attachPoints:
|
|
|
|
return None
|
2016-01-19 22:38:23 +01:00
|
|
|
self = ot.AttachList()
|
|
|
|
self.Coverage = buildCoverage(attachPoints.keys(), glyphMap)
|
2016-01-19 23:25:47 +01:00
|
|
|
self.AttachPoint = [buildAttachPoint(attachPoints[g])
|
2016-01-19 22:38:23 +01:00
|
|
|
for g in self.Coverage.glyphs]
|
|
|
|
self.GlyphCount = len(self.AttachPoint)
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
2016-01-19 23:25:47 +01:00
|
|
|
def buildAttachPoint(points):
|
2020-07-02 14:09:10 +01:00
|
|
|
"""[4, 23, 41] --> ot.AttachPoint"""
|
2016-01-22 19:51:02 +01:00
|
|
|
if not points:
|
|
|
|
return None
|
2016-01-19 22:38:23 +01:00
|
|
|
self = ot.AttachPoint()
|
2016-01-22 19:54:20 +01:00
|
|
|
self.PointIndex = sorted(set(points))
|
|
|
|
self.PointCount = len(self.PointIndex)
|
2016-01-19 22:38:23 +01:00
|
|
|
return self
|
2016-01-20 08:23:42 +01:00
|
|
|
|
|
|
|
|
|
|
|
def buildCaretValueForCoord(coord):
|
2020-07-02 14:09:10 +01:00
|
|
|
"""500 --> ot.CaretValue, format 1"""
|
2016-01-20 08:23:42 +01:00
|
|
|
self = ot.CaretValue()
|
|
|
|
self.Format = 1
|
|
|
|
self.Coordinate = coord
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
|
|
def buildCaretValueForPoint(point):
|
2020-07-02 14:09:10 +01:00
|
|
|
"""4 --> ot.CaretValue, format 2"""
|
2016-01-20 08:23:42 +01:00
|
|
|
self = ot.CaretValue()
|
|
|
|
self.Format = 2
|
|
|
|
self.CaretValuePoint = point
|
|
|
|
return self
|
2016-01-20 09:16:31 +01:00
|
|
|
|
|
|
|
|
2016-01-20 09:49:09 +01:00
|
|
|
def buildLigCaretList(coords, points, glyphMap):
|
2020-07-02 14:09:10 +01:00
|
|
|
"""{"f_f_i":[300,600]}, {"c_t":[28]} --> ot.LigCaretList, or None"""
|
2016-01-20 09:49:09 +01:00
|
|
|
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
|
|
|
|
|
|
|
|
|
2016-01-20 09:16:31 +01:00
|
|
|
def buildLigGlyph(coords, points):
|
2020-07-02 14:09:10 +01:00
|
|
|
"""([500], [4]) --> ot.LigGlyph; None for empty coords/points"""
|
2016-01-20 09:16:31 +01:00
|
|
|
carets = []
|
|
|
|
if coords:
|
|
|
|
carets.extend([buildCaretValueForCoord(c) for c in sorted(coords)])
|
|
|
|
if points:
|
|
|
|
carets.extend([buildCaretValueForPoint(p) for p in sorted(points)])
|
|
|
|
if not carets:
|
|
|
|
return None
|
|
|
|
self = ot.LigGlyph()
|
|
|
|
self.CaretValue = carets
|
2016-01-22 19:57:22 +01:00
|
|
|
self.CaretCount = len(self.CaretValue)
|
2016-01-20 09:16:31 +01:00
|
|
|
return self
|
2016-01-20 11:28:33 +01:00
|
|
|
|
|
|
|
|
|
|
|
def buildMarkGlyphSetsDef(markSets, glyphMap):
|
2020-07-02 14:09:10 +01:00
|
|
|
"""[{"acute","grave"}, {"caron","grave"}] --> ot.MarkGlyphSetsDef"""
|
2016-01-20 11:28:33 +01:00
|
|
|
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
|
2016-02-02 10:48:47 +01:00
|
|
|
|
|
|
|
|
|
|
|
class ClassDefBuilder(object):
|
|
|
|
"""Helper for building ClassDef tables."""
|
|
|
|
def __init__(self, useClass0):
|
|
|
|
self.classes_ = set()
|
|
|
|
self.glyphs_ = {}
|
|
|
|
self.useClass0_ = useClass0
|
|
|
|
|
|
|
|
def canAdd(self, glyphs):
|
2016-12-31 15:51:53 +01:00
|
|
|
if isinstance(glyphs, (set, frozenset)):
|
2017-01-04 13:13:36 +01:00
|
|
|
glyphs = sorted(glyphs)
|
|
|
|
glyphs = tuple(glyphs)
|
2016-02-02 10:48:47 +01:00
|
|
|
if glyphs in self.classes_:
|
|
|
|
return True
|
|
|
|
for glyph in glyphs:
|
|
|
|
if glyph in self.glyphs_:
|
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
|
|
def add(self, glyphs):
|
2016-12-31 15:51:53 +01:00
|
|
|
if isinstance(glyphs, (set, frozenset)):
|
2017-01-04 13:13:36 +01:00
|
|
|
glyphs = sorted(glyphs)
|
|
|
|
glyphs = tuple(glyphs)
|
2016-02-02 10:48:47 +01:00
|
|
|
if glyphs in self.classes_:
|
|
|
|
return
|
|
|
|
self.classes_.add(glyphs)
|
|
|
|
for glyph in glyphs:
|
|
|
|
assert glyph not in self.glyphs_
|
|
|
|
self.glyphs_[glyph] = glyphs
|
|
|
|
|
|
|
|
def classes(self):
|
|
|
|
# In ClassDef1 tables, class id #0 does not need to be encoded
|
|
|
|
# because zero is the default. Therefore, we use id #0 for the
|
|
|
|
# glyph class that has the largest number of members. However,
|
|
|
|
# in other tables than ClassDef1, 0 means "every other glyph"
|
|
|
|
# so we should not use that ID for any real glyph classes;
|
|
|
|
# we implement this by inserting an empty set at position 0.
|
|
|
|
#
|
|
|
|
# TODO: Instead of counting the number of glyphs in each class,
|
|
|
|
# we should determine the encoded size. If the glyphs in a large
|
|
|
|
# class form a contiguous range, the encoding is actually quite
|
|
|
|
# compact, whereas a non-contiguous set might need a lot of bytes
|
2016-12-30 19:18:15 -05:00
|
|
|
# in the output file. We don't get this right with the key below.
|
2016-12-28 15:44:19 -05:00
|
|
|
result = sorted(self.classes_, key=lambda s: (len(s), s), reverse=True)
|
2016-02-02 10:48:47 +01:00
|
|
|
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
|
2020-05-09 16:08:11 +02:00
|
|
|
|
|
|
|
|
|
|
|
AXIS_VALUE_NEGATIVE_INFINITY = fixedToFloat(-0x80000000, 16)
|
|
|
|
AXIS_VALUE_POSITIVE_INFINITY = fixedToFloat(0x7FFFFFFF, 16)
|
|
|
|
|
|
|
|
|
|
|
|
def buildStatTable(ttFont, axes, locations=None, elidedFallbackName=2):
|
|
|
|
"""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, or a dictionary containing multilingual names. 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()
|
|
|
|
nameTable = ttFont["name"]
|
|
|
|
statTable.ElidedFallbackNameID = _addName(nameTable, elidedFallbackName)
|
|
|
|
|
|
|
|
# 'locations' contains data for AxisValue Format 4
|
|
|
|
axisRecords, axisValues = _buildAxisRecords(axes, nameTable)
|
|
|
|
if not locations:
|
|
|
|
statTable.Version = 0x00010001
|
|
|
|
else:
|
|
|
|
# We'll be adding Format 4 AxisValue records, which
|
|
|
|
# requires a higher table version
|
|
|
|
statTable.Version = 0x00010002
|
|
|
|
multiAxisValues = _buildAxisValuesFormat4(locations, axes, nameTable)
|
|
|
|
axisValues = multiAxisValues + axisValues
|
|
|
|
|
|
|
|
# Store AxisRecords
|
|
|
|
axisRecordArray = ot.AxisRecordArray()
|
|
|
|
axisRecordArray.Axis = axisRecords
|
|
|
|
# XXX these should not be hard-coded but computed automatically
|
|
|
|
statTable.DesignAxisRecordSize = 8
|
|
|
|
statTable.DesignAxisRecord = axisRecordArray
|
|
|
|
statTable.DesignAxisCount = len(axisRecords)
|
|
|
|
|
|
|
|
if axisValues:
|
|
|
|
# Store AxisValueRecords
|
|
|
|
axisValueArray = ot.AxisValueArray()
|
|
|
|
axisValueArray.AxisValue = axisValues
|
|
|
|
statTable.AxisValueArray = axisValueArray
|
|
|
|
statTable.AxisValueCount = len(axisValues)
|
|
|
|
|
|
|
|
|
|
|
|
def _buildAxisRecords(axes, nameTable):
|
|
|
|
axisRecords = []
|
|
|
|
axisValues = []
|
|
|
|
for axisRecordIndex, axisDict in enumerate(axes):
|
|
|
|
axis = ot.AxisRecord()
|
|
|
|
axis.AxisTag = axisDict["tag"]
|
2020-06-08 19:39:28 +02:00
|
|
|
axis.AxisNameID = _addName(nameTable, axisDict["name"], 256)
|
2020-05-09 16:08:11 +02:00
|
|
|
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(nameTable, axisVal['name'])
|
|
|
|
|
|
|
|
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, nameTable):
|
|
|
|
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(nameTable, axisLocationDict['name'])
|
|
|
|
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
|
|
|
|
|
|
|
|
|
2020-06-08 19:39:28 +02:00
|
|
|
def _addName(nameTable, value, minNameID=0):
|
2020-05-09 16:08:11 +02:00
|
|
|
if isinstance(value, int):
|
|
|
|
# Already a nameID
|
|
|
|
return value
|
|
|
|
if isinstance(value, str):
|
|
|
|
names = dict(en=value)
|
|
|
|
elif isinstance(value, dict):
|
|
|
|
names = value
|
|
|
|
else:
|
|
|
|
raise TypeError("value must be int, str or dict")
|
2020-06-08 19:39:28 +02:00
|
|
|
return nameTable.addMultilingualName(names, minNameID=minNameID)
|