2015-09-04 15:06:11 +02:00
|
|
|
from __future__ import print_function, division, absolute_import
|
|
|
|
from __future__ import unicode_literals
|
|
|
|
from fontTools.feaLib.error import FeatureLibError
|
|
|
|
from fontTools.feaLib.parser import Parser
|
2015-09-09 16:57:29 +02:00
|
|
|
from fontTools.ttLib import getTableClass
|
2015-12-04 11:16:43 +01:00
|
|
|
from fontTools.ttLib.tables import otBase, otTables
|
2016-01-07 10:31:13 +01:00
|
|
|
import itertools
|
2015-09-08 12:05:44 +02:00
|
|
|
import warnings
|
2015-09-04 15:06:11 +02:00
|
|
|
|
|
|
|
|
|
|
|
def addOpenTypeFeatures(featurefile_path, font):
|
|
|
|
builder = Builder(featurefile_path, font)
|
|
|
|
builder.build()
|
|
|
|
|
|
|
|
|
|
|
|
class Builder(object):
|
|
|
|
def __init__(self, featurefile_path, font):
|
|
|
|
self.featurefile_path = featurefile_path
|
|
|
|
self.font = font
|
2015-09-04 22:29:06 +02:00
|
|
|
self.default_language_systems_ = set()
|
|
|
|
self.script_ = None
|
2015-12-10 19:17:11 +01:00
|
|
|
self.lookupflag_ = 0
|
|
|
|
self.lookupflag_markFilterSet_ = None
|
2015-09-04 22:29:06 +02:00
|
|
|
self.language_systems = set()
|
2015-09-07 13:33:44 +02:00
|
|
|
self.named_lookups_ = {}
|
2015-09-07 11:14:03 +02:00
|
|
|
self.cur_lookup_ = None
|
2015-09-07 13:33:44 +02:00
|
|
|
self.cur_lookup_name_ = None
|
2015-09-07 17:22:37 +02:00
|
|
|
self.cur_feature_name_ = None
|
2015-09-07 11:14:03 +02:00
|
|
|
self.lookups_ = []
|
2015-09-08 15:55:54 +02:00
|
|
|
self.features_ = {} # ('latn', 'DEU ', 'smcp') --> [LookupBuilder*]
|
2016-01-08 08:32:47 +01:00
|
|
|
self.attachPoints_ = {} # "a" --> {3, 7}
|
2016-01-07 17:22:31 +01:00
|
|
|
self.ligatureCaretByIndex_ = {} # "f_f_i" --> {3, 7}
|
2016-01-07 16:39:35 +01:00
|
|
|
self.ligatureCaretByPos_ = {} # "f_f_i" --> {300, 600}
|
2015-12-08 17:04:21 +01:00
|
|
|
self.parseTree = None
|
2015-09-08 15:55:54 +02:00
|
|
|
self.required_features_ = {} # ('latn', 'DEU ') --> 'scmp'
|
2015-12-10 19:17:11 +01:00
|
|
|
self.markAttach_ = {} # "acute" --> (4, (file, line, column))
|
|
|
|
self.markAttachClassID_ = {} # frozenset({"acute", "grave"}) --> 4
|
|
|
|
self.markFilterSets_ = {} # frozenset({"acute", "grave"}) --> 4
|
2015-09-04 15:06:11 +02:00
|
|
|
|
|
|
|
def build(self):
|
2015-12-08 17:04:21 +01:00
|
|
|
self.parseTree = Parser(self.featurefile_path).parse()
|
|
|
|
self.parseTree.build(self)
|
2015-09-09 16:57:29 +02:00
|
|
|
for tag in ('GPOS', 'GSUB'):
|
2015-12-07 21:37:42 +01:00
|
|
|
table = self.makeTable(tag)
|
|
|
|
if (table.ScriptList.ScriptCount > 0 or
|
|
|
|
table.FeatureList.FeatureCount > 0 or
|
|
|
|
table.LookupList.LookupCount > 0):
|
|
|
|
fontTable = self.font[tag] = getTableClass(tag)()
|
|
|
|
fontTable.table = table
|
2015-12-07 22:49:20 +01:00
|
|
|
elif tag in self.font:
|
2015-12-07 21:37:42 +01:00
|
|
|
del self.font[tag]
|
2015-12-08 17:04:21 +01:00
|
|
|
gdef = self.makeGDEF()
|
|
|
|
if gdef:
|
|
|
|
self.font["GDEF"] = gdef
|
|
|
|
elif "GDEF" in self.font:
|
|
|
|
del self.font["GDEF"]
|
2015-09-07 11:14:03 +02:00
|
|
|
|
2016-01-06 16:15:26 +01:00
|
|
|
def get_chained_lookup_(self, location, builder_class):
|
|
|
|
result = builder_class(self.font, location)
|
|
|
|
result.lookupflag = self.lookupflag_
|
|
|
|
result.markFilterSet = self.lookupflag_markFilterSet_
|
|
|
|
self.lookups_.append(result)
|
|
|
|
return result
|
|
|
|
|
2016-01-07 08:57:34 +01:00
|
|
|
def add_lookup_to_feature_(self, lookup, feature_name):
|
|
|
|
for script, lang in self.language_systems:
|
|
|
|
key = (script, lang, feature_name)
|
|
|
|
self.features_.setdefault(key, []).append(lookup)
|
|
|
|
|
2015-09-07 11:14:03 +02:00
|
|
|
def get_lookup_(self, location, builder_class):
|
2015-09-07 13:33:44 +02:00
|
|
|
if (self.cur_lookup_ and
|
2015-12-10 19:17:11 +01:00
|
|
|
type(self.cur_lookup_) == builder_class and
|
|
|
|
self.cur_lookup_.lookupflag == self.lookupflag_ and
|
|
|
|
self.cur_lookup_.markFilterSet ==
|
|
|
|
self.lookupflag_markFilterSet_):
|
2015-09-07 11:14:03 +02:00
|
|
|
return self.cur_lookup_
|
2015-09-07 16:27:12 +02:00
|
|
|
if self.cur_lookup_name_ and self.cur_lookup_:
|
|
|
|
raise FeatureLibError(
|
|
|
|
"Within a named lookup block, all rules must be of "
|
|
|
|
"the same lookup type and flag", location)
|
2015-12-10 19:17:11 +01:00
|
|
|
self.cur_lookup_ = builder_class(self.font, location)
|
|
|
|
self.cur_lookup_.lookupflag = self.lookupflag_
|
|
|
|
self.cur_lookup_.markFilterSet = self.lookupflag_markFilterSet_
|
2015-09-07 11:14:03 +02:00
|
|
|
self.lookups_.append(self.cur_lookup_)
|
2015-09-07 13:33:44 +02:00
|
|
|
if self.cur_lookup_name_:
|
2015-09-07 17:22:37 +02:00
|
|
|
# We are starting a lookup rule inside a named lookup block.
|
2015-09-07 13:33:44 +02:00
|
|
|
self.named_lookups_[self.cur_lookup_name_] = self.cur_lookup_
|
2015-09-28 16:49:17 +02:00
|
|
|
if self.cur_feature_name_:
|
|
|
|
# We are starting a lookup rule inside a feature. This includes
|
|
|
|
# lookup rules inside named lookups inside features.
|
2016-01-07 08:57:34 +01:00
|
|
|
self.add_lookup_to_feature_(self.cur_lookup_,
|
|
|
|
self.cur_feature_name_)
|
2015-09-07 11:14:03 +02:00
|
|
|
return self.cur_lookup_
|
2015-09-04 15:06:11 +02:00
|
|
|
|
2015-12-08 17:04:21 +01:00
|
|
|
def makeGDEF(self):
|
|
|
|
gdef = otTables.GDEF()
|
|
|
|
gdef.Version = 1.0
|
2016-01-07 16:39:35 +01:00
|
|
|
gdef.GlyphClassDef = None
|
2016-01-08 08:32:47 +01:00
|
|
|
gdef.AttachList = self.makeGDEFAttachList_()
|
|
|
|
gdef.LigCaretList = self.makeGDEFLigCaretList_()
|
2015-12-08 17:04:21 +01:00
|
|
|
|
2015-12-08 22:28:02 +01:00
|
|
|
inferredGlyphClass = {}
|
|
|
|
for lookup in self.lookups_:
|
|
|
|
inferredGlyphClass.update(lookup.inferGlyphClasses())
|
|
|
|
|
2015-12-12 12:54:23 +01:00
|
|
|
marks = {} # glyph --> markClass
|
2015-12-08 17:04:21 +01:00
|
|
|
for markClass in self.parseTree.markClasses.values():
|
2015-12-12 12:54:23 +01:00
|
|
|
for markClassDef in markClass.definitions:
|
|
|
|
for glyph in markClassDef.glyphSet():
|
|
|
|
other = marks.get(glyph)
|
|
|
|
if other not in (None, markClass):
|
|
|
|
name1, name2 = sorted([markClass.name, other.name])
|
|
|
|
raise FeatureLibError(
|
|
|
|
'Glyph %s cannot be both in '
|
|
|
|
'markClass @%s and @%s' %
|
|
|
|
(glyph, name1, name2), markClassDef.location)
|
|
|
|
marks[glyph] = markClass
|
|
|
|
inferredGlyphClass[glyph] = 3
|
2015-12-08 22:28:02 +01:00
|
|
|
|
2016-01-07 16:39:35 +01:00
|
|
|
if inferredGlyphClass:
|
|
|
|
gdef.GlyphClassDef = otTables.GlyphClassDef()
|
|
|
|
gdef.GlyphClassDef.classDefs = inferredGlyphClass
|
2015-12-10 19:17:11 +01:00
|
|
|
|
|
|
|
markAttachClass = {g: c for g, (c, _) in self.markAttach_.items()}
|
|
|
|
if markAttachClass:
|
|
|
|
gdef.MarkAttachClassDef = otTables.MarkAttachClassDef()
|
|
|
|
gdef.MarkAttachClassDef.classDefs = markAttachClass
|
|
|
|
else:
|
|
|
|
gdef.MarkAttachClassDef = None
|
|
|
|
|
|
|
|
if self.markFilterSets_:
|
|
|
|
gdef.Version = 0x00010002
|
|
|
|
m = gdef.MarkGlyphSetsDef = otTables.MarkGlyphSetsDef()
|
|
|
|
m.MarkSetTableFormat = 1
|
|
|
|
m.MarkSetCount = len(self.markFilterSets_)
|
|
|
|
m.Coverage = []
|
|
|
|
filterSets = [(id, glyphs)
|
|
|
|
for (glyphs, id) in self.markFilterSets_.items()]
|
|
|
|
for i, glyphs in sorted(filterSets):
|
|
|
|
coverage = otTables.Coverage()
|
|
|
|
coverage.glyphs = sorted(glyphs, key=self.font.getGlyphID)
|
|
|
|
m.Coverage.append(coverage)
|
|
|
|
|
2016-01-08 08:32:47 +01:00
|
|
|
if any((gdef.GlyphClassDef, gdef.AttachList,
|
|
|
|
gdef.LigCaretList, gdef.MarkAttachClassDef)):
|
|
|
|
result = getTableClass("GDEF")()
|
|
|
|
result.table = gdef
|
|
|
|
return result
|
|
|
|
else:
|
2015-12-08 17:04:21 +01:00
|
|
|
return None
|
2016-01-08 08:32:47 +01:00
|
|
|
|
|
|
|
def makeGDEFAttachList_(self):
|
|
|
|
glyphs = sorted(self.attachPoints_.keys(), key=self.font.getGlyphID)
|
|
|
|
if not glyphs:
|
|
|
|
return None
|
|
|
|
result = otTables.AttachList()
|
|
|
|
result.Coverage = otTables.Coverage()
|
|
|
|
result.Coverage.glyphs = glyphs
|
|
|
|
result.GlyphCount = len(glyphs)
|
|
|
|
result.AttachPoint = []
|
|
|
|
for glyph in glyphs:
|
|
|
|
pt = otTables.AttachPoint()
|
|
|
|
pt.PointIndex = sorted(self.attachPoints_[glyph])
|
|
|
|
pt.PointCount = len(pt.PointIndex)
|
|
|
|
result.AttachPoint.append(pt)
|
2015-12-08 17:04:21 +01:00
|
|
|
return result
|
|
|
|
|
2016-01-08 08:32:47 +01:00
|
|
|
def makeGDEFLigCaretList_(self):
|
2016-01-07 17:22:31 +01:00
|
|
|
glyphs = set(self.ligatureCaretByPos_.keys())
|
|
|
|
glyphs.update(self.ligatureCaretByIndex_.keys())
|
|
|
|
glyphs = sorted(glyphs, key=self.font.getGlyphID)
|
|
|
|
if not glyphs:
|
2016-01-07 16:39:35 +01:00
|
|
|
return None
|
|
|
|
result = otTables.LigCaretList()
|
|
|
|
result.Coverage = otTables.Coverage()
|
2016-01-07 17:22:31 +01:00
|
|
|
result.Coverage.glyphs = glyphs
|
|
|
|
result.LigGlyphCount = len(glyphs)
|
2016-01-07 16:39:35 +01:00
|
|
|
result.LigGlyph = []
|
2016-01-07 17:22:31 +01:00
|
|
|
for glyph in glyphs:
|
2016-01-07 16:39:35 +01:00
|
|
|
ligGlyph = otTables.LigGlyph()
|
|
|
|
result.LigGlyph.append(ligGlyph)
|
|
|
|
ligGlyph.CaretValue = []
|
2016-01-07 17:22:31 +01:00
|
|
|
for caretPos in sorted(self.ligatureCaretByPos_.get(glyph, [])):
|
2016-01-07 16:39:35 +01:00
|
|
|
val = otTables.CaretValue()
|
|
|
|
val.Format = 1
|
|
|
|
val.Coordinate = caretPos
|
|
|
|
ligGlyph.CaretValue.append(val)
|
2016-01-07 17:22:31 +01:00
|
|
|
for point in sorted(self.ligatureCaretByIndex_.get(glyph, [])):
|
|
|
|
val = otTables.CaretValue()
|
|
|
|
val.Format = 2
|
|
|
|
val.CaretValuePoint = point
|
|
|
|
ligGlyph.CaretValue.append(val)
|
2016-01-07 16:39:35 +01:00
|
|
|
ligGlyph.CaretCount = len(ligGlyph.CaretValue)
|
|
|
|
return result
|
|
|
|
|
2015-09-04 15:06:11 +02:00
|
|
|
def makeTable(self, tag):
|
|
|
|
table = getattr(otTables, tag, None)()
|
|
|
|
table.Version = 1.0
|
|
|
|
table.ScriptList = otTables.ScriptList()
|
|
|
|
table.ScriptList.ScriptRecord = []
|
|
|
|
table.FeatureList = otTables.FeatureList()
|
|
|
|
table.FeatureList.FeatureRecord = []
|
2015-09-07 11:14:03 +02:00
|
|
|
|
2015-09-04 15:06:11 +02:00
|
|
|
table.LookupList = otTables.LookupList()
|
|
|
|
table.LookupList.Lookup = []
|
2015-09-07 17:22:37 +02:00
|
|
|
for lookup in self.lookups_:
|
|
|
|
lookup.lookup_index = None
|
2015-09-07 11:14:03 +02:00
|
|
|
for i, lookup_builder in enumerate(self.lookups_):
|
|
|
|
if lookup_builder.table != tag:
|
|
|
|
continue
|
|
|
|
# If multiple lookup builders would build equivalent lookups,
|
|
|
|
# emit them only once. This is quadratic in the number of lookups,
|
|
|
|
# but the checks are cheap. If performance ever becomes an issue,
|
|
|
|
# we could hash the lookup content and only compare those with
|
|
|
|
# the same hash value.
|
|
|
|
equivalent_builder = None
|
|
|
|
for other_builder in self.lookups_[:i]:
|
|
|
|
if lookup_builder.equals(other_builder):
|
|
|
|
equivalent_builder = other_builder
|
|
|
|
if equivalent_builder is not None:
|
|
|
|
lookup_builder.lookup_index = equivalent_builder.lookup_index
|
|
|
|
continue
|
|
|
|
lookup_builder.lookup_index = len(table.LookupList.Lookup)
|
|
|
|
table.LookupList.Lookup.append(lookup_builder.build())
|
2015-09-07 17:22:37 +02:00
|
|
|
|
|
|
|
# Build a table for mapping (tag, lookup_indices) to feature_index.
|
|
|
|
# For example, ('liga', (2,3,7)) --> 23.
|
|
|
|
feature_indices = {}
|
2015-09-08 15:55:54 +02:00
|
|
|
required_feature_indices = {} # ('latn', 'DEU') --> 23
|
|
|
|
scripts = {} # 'latn' --> {'DEU': [23, 24]} for feature #23,24
|
2015-09-07 17:22:37 +02:00
|
|
|
for key, lookups in sorted(self.features_.items()):
|
|
|
|
script, lang, feature_tag = key
|
|
|
|
# l.lookup_index will be None when a lookup is not needed
|
|
|
|
# for the table under construction. For example, substitution
|
|
|
|
# rules will have no lookup_index while building GPOS tables.
|
|
|
|
lookup_indices = tuple([l.lookup_index for l in lookups
|
|
|
|
if l.lookup_index is not None])
|
|
|
|
if len(lookup_indices) == 0:
|
|
|
|
continue
|
2015-09-07 21:34:10 +02:00
|
|
|
|
2015-09-07 17:22:37 +02:00
|
|
|
feature_key = (feature_tag, lookup_indices)
|
|
|
|
feature_index = feature_indices.get(feature_key)
|
|
|
|
if feature_index is None:
|
|
|
|
feature_index = len(table.FeatureList.FeatureRecord)
|
|
|
|
frec = otTables.FeatureRecord()
|
|
|
|
frec.FeatureTag = feature_tag
|
|
|
|
frec.Feature = otTables.Feature()
|
|
|
|
frec.Feature.FeatureParams = None
|
|
|
|
frec.Feature.LookupListIndex = lookup_indices
|
|
|
|
frec.Feature.LookupCount = len(lookup_indices)
|
|
|
|
table.FeatureList.FeatureRecord.append(frec)
|
|
|
|
feature_indices[feature_key] = feature_index
|
2015-09-07 21:34:10 +02:00
|
|
|
scripts.setdefault(script, {}).setdefault(lang, []).append(
|
|
|
|
feature_index)
|
2015-09-08 15:55:54 +02:00
|
|
|
if self.required_features_.get((script, lang)) == feature_tag:
|
|
|
|
required_feature_indices[(script, lang)] = feature_index
|
2015-09-07 21:34:10 +02:00
|
|
|
|
|
|
|
# Build ScriptList.
|
|
|
|
for script, lang_features in sorted(scripts.items()):
|
|
|
|
srec = otTables.ScriptRecord()
|
|
|
|
srec.ScriptTag = script
|
|
|
|
srec.Script = otTables.Script()
|
|
|
|
srec.Script.DefaultLangSys = None
|
|
|
|
srec.Script.LangSysRecord = []
|
|
|
|
for lang, feature_indices in sorted(lang_features.items()):
|
2015-09-07 22:03:50 +02:00
|
|
|
langrec = otTables.LangSysRecord()
|
|
|
|
langrec.LangSys = otTables.LangSys()
|
|
|
|
langrec.LangSys.LookupOrder = None
|
2015-09-08 15:55:54 +02:00
|
|
|
|
|
|
|
req_feature_index = \
|
|
|
|
required_feature_indices.get((script, lang))
|
|
|
|
if req_feature_index is None:
|
|
|
|
langrec.LangSys.ReqFeatureIndex = 0xFFFF
|
|
|
|
else:
|
|
|
|
langrec.LangSys.ReqFeatureIndex = req_feature_index
|
|
|
|
|
|
|
|
langrec.LangSys.FeatureIndex = [i for i in feature_indices
|
|
|
|
if i != req_feature_index]
|
|
|
|
langrec.LangSys.FeatureCount = \
|
|
|
|
len(langrec.LangSys.FeatureIndex)
|
|
|
|
|
2015-09-07 21:34:10 +02:00
|
|
|
if lang == "dflt":
|
2015-09-07 22:03:50 +02:00
|
|
|
srec.Script.DefaultLangSys = langrec.LangSys
|
2015-09-07 21:34:10 +02:00
|
|
|
else:
|
2015-09-07 22:03:50 +02:00
|
|
|
langrec.LangSysTag = lang
|
|
|
|
srec.Script.LangSysRecord.append(langrec)
|
2015-09-07 21:34:10 +02:00
|
|
|
srec.Script.LangSysCount = len(srec.Script.LangSysRecord)
|
|
|
|
table.ScriptList.ScriptRecord.append(srec)
|
2015-09-07 17:22:37 +02:00
|
|
|
|
|
|
|
table.ScriptList.ScriptCount = len(table.ScriptList.ScriptRecord)
|
|
|
|
table.FeatureList.FeatureCount = len(table.FeatureList.FeatureRecord)
|
2015-09-07 11:14:03 +02:00
|
|
|
table.LookupList.LookupCount = len(table.LookupList.Lookup)
|
2015-09-04 15:06:11 +02:00
|
|
|
return table
|
|
|
|
|
|
|
|
def add_language_system(self, location, script, language):
|
2015-09-04 22:29:06 +02:00
|
|
|
# OpenType Feature File Specification, section 4.b.i
|
|
|
|
if (script == "DFLT" and language == "dflt" and
|
|
|
|
self.default_language_systems_):
|
2015-09-04 15:06:11 +02:00
|
|
|
raise FeatureLibError(
|
|
|
|
'If "languagesystem DFLT dflt" is present, it must be '
|
|
|
|
'the first of the languagesystem statements', location)
|
2015-09-08 10:56:07 +02:00
|
|
|
if (script, language) in self.default_language_systems_:
|
|
|
|
raise FeatureLibError(
|
|
|
|
'"languagesystem %s %s" has already been specified' %
|
|
|
|
(script.strip(), language.strip()), location)
|
2015-09-04 22:29:06 +02:00
|
|
|
self.default_language_systems_.add((script, language))
|
|
|
|
|
|
|
|
def get_default_language_systems_(self):
|
|
|
|
# OpenType Feature File specification, 4.b.i. languagesystem:
|
|
|
|
# If no "languagesystem" statement is present, then the
|
|
|
|
# implementation must behave exactly as though the following
|
|
|
|
# statement were present at the beginning of the feature file:
|
|
|
|
# languagesystem DFLT dflt;
|
|
|
|
if self.default_language_systems_:
|
|
|
|
return frozenset(self.default_language_systems_)
|
|
|
|
else:
|
|
|
|
return frozenset({('DFLT', 'dflt')})
|
|
|
|
|
|
|
|
def start_feature(self, location, name):
|
|
|
|
self.language_systems = self.get_default_language_systems_()
|
2015-09-07 13:33:44 +02:00
|
|
|
self.cur_lookup_ = None
|
2015-09-07 17:22:37 +02:00
|
|
|
self.cur_feature_name_ = name
|
2015-09-04 22:29:06 +02:00
|
|
|
|
2015-09-07 11:14:03 +02:00
|
|
|
def end_feature(self):
|
2015-09-07 17:22:37 +02:00
|
|
|
assert self.cur_feature_name_ is not None
|
|
|
|
self.cur_feature_name_ = None
|
2015-09-07 11:14:03 +02:00
|
|
|
self.language_systems = None
|
|
|
|
self.cur_lookup_ = None
|
|
|
|
|
2015-09-07 13:33:44 +02:00
|
|
|
def start_lookup_block(self, location, name):
|
|
|
|
if name in self.named_lookups_:
|
|
|
|
raise FeatureLibError(
|
|
|
|
'Lookup "%s" has already been defined' % name, location)
|
|
|
|
self.cur_lookup_name_ = name
|
|
|
|
self.named_lookups_[name] = None
|
|
|
|
self.cur_lookup_ = None
|
|
|
|
|
|
|
|
def end_lookup_block(self):
|
|
|
|
assert self.cur_lookup_name_ is not None
|
|
|
|
self.cur_lookup_name_ = None
|
|
|
|
self.cur_lookup_ = None
|
|
|
|
|
2016-01-07 08:57:34 +01:00
|
|
|
def add_lookup_call(self, lookup_name):
|
|
|
|
assert lookup_name in self.named_lookups_, lookup_name
|
|
|
|
self.cur_lookup_ = None
|
|
|
|
lookup = self.named_lookups_[lookup_name]
|
|
|
|
self.add_lookup_to_feature_(lookup, self.cur_feature_name_)
|
|
|
|
|
2015-09-08 15:55:54 +02:00
|
|
|
def set_language(self, location, language, include_default, required):
|
2015-09-07 21:34:10 +02:00
|
|
|
assert(len(language) == 4)
|
2015-09-07 13:33:44 +02:00
|
|
|
if self.cur_lookup_name_:
|
|
|
|
raise FeatureLibError(
|
|
|
|
"Within a named lookup block, it is not allowed "
|
|
|
|
"to change the language", location)
|
2015-09-08 12:18:03 +02:00
|
|
|
if self.cur_feature_name_ in ('aalt', 'size'):
|
|
|
|
raise FeatureLibError(
|
|
|
|
"Language statements are not allowed "
|
|
|
|
"within \"feature %s\"" % self.cur_feature_name_, location)
|
2015-09-07 11:14:03 +02:00
|
|
|
self.cur_lookup_ = None
|
2015-09-04 22:29:06 +02:00
|
|
|
if include_default:
|
|
|
|
langsys = set(self.get_default_language_systems_())
|
|
|
|
else:
|
|
|
|
langsys = set()
|
|
|
|
langsys.add((self.script_, language))
|
|
|
|
self.language_systems = frozenset(langsys)
|
2015-09-08 15:55:54 +02:00
|
|
|
if required:
|
|
|
|
key = (self.script_, language)
|
|
|
|
if key in self.required_features_:
|
|
|
|
raise FeatureLibError(
|
|
|
|
"Language %s (script %s) has already "
|
|
|
|
"specified feature %s as its required feature" % (
|
|
|
|
language.strip(), self.script_.strip(),
|
|
|
|
self.required_features_[key].strip()),
|
|
|
|
location)
|
|
|
|
self.required_features_[key] = self.cur_feature_name_
|
2015-09-04 22:29:06 +02:00
|
|
|
|
2015-12-10 19:17:11 +01:00
|
|
|
def getMarkAttachClass_(self, location, glyphs):
|
|
|
|
id = self.markAttachClassID_.get(glyphs)
|
|
|
|
if id is not None:
|
|
|
|
return id
|
|
|
|
id = len(self.markAttachClassID_) + 1
|
|
|
|
self.markAttachClassID_[glyphs] = id
|
|
|
|
for glyph in glyphs:
|
|
|
|
if glyph in self.markAttach_:
|
|
|
|
_, loc = self.markAttach_[glyph]
|
|
|
|
raise FeatureLibError(
|
|
|
|
"Glyph %s already has been assigned "
|
|
|
|
"a MarkAttachmentType at %s:%d:%d" % (
|
|
|
|
glyph, loc[0], loc[1], loc[2]),
|
|
|
|
location)
|
|
|
|
self.markAttach_[glyph] = (id, location)
|
|
|
|
return id
|
|
|
|
|
|
|
|
def getMarkFilterSet_(self, location, glyphs):
|
|
|
|
id = self.markFilterSets_.get(glyphs)
|
|
|
|
if id is not None:
|
|
|
|
return id
|
|
|
|
id = len(self.markFilterSets_)
|
|
|
|
self.markFilterSets_[glyphs] = id
|
|
|
|
return id
|
|
|
|
|
|
|
|
def set_lookup_flag(self, location, value, markAttach, markFilter):
|
|
|
|
value = value & 0xFF
|
|
|
|
if markAttach:
|
|
|
|
markAttachClass = self.getMarkAttachClass_(location, markAttach)
|
|
|
|
value = value | (markAttachClass << 8)
|
|
|
|
if markFilter:
|
|
|
|
markFilterSet = self.getMarkFilterSet_(location, markFilter)
|
|
|
|
value = value | 0x10
|
|
|
|
self.lookupflag_markFilterSet_ = markFilterSet
|
|
|
|
else:
|
|
|
|
self.lookupflag_markFilterSet_ = None
|
|
|
|
self.lookupflag_ = value
|
|
|
|
|
2015-09-04 22:29:06 +02:00
|
|
|
def set_script(self, location, script):
|
2015-09-07 13:33:44 +02:00
|
|
|
if self.cur_lookup_name_:
|
|
|
|
raise FeatureLibError(
|
|
|
|
"Within a named lookup block, it is not allowed "
|
|
|
|
"to change the script", location)
|
2015-09-08 12:18:03 +02:00
|
|
|
if self.cur_feature_name_ in ('aalt', 'size'):
|
|
|
|
raise FeatureLibError(
|
|
|
|
"Script statements are not allowed "
|
|
|
|
"within \"feature %s\"" % self.cur_feature_name_, location)
|
2015-09-07 11:14:03 +02:00
|
|
|
self.cur_lookup_ = None
|
2015-09-04 22:29:06 +02:00
|
|
|
self.script_ = script
|
2015-12-10 19:17:11 +01:00
|
|
|
self.lookupflag_ = 0
|
|
|
|
self.lookupflag_markFilterSet_ = None
|
2015-09-08 15:55:54 +02:00
|
|
|
self.set_language(location, "dflt",
|
|
|
|
include_default=True, required=False)
|
2015-09-07 11:14:03 +02:00
|
|
|
|
2015-12-03 13:05:42 +01:00
|
|
|
def find_lookup_builders_(self, lookups):
|
|
|
|
"""Helper for building chain contextual substitutions
|
|
|
|
|
|
|
|
Given a list of lookup names, finds the LookupBuilder for each name.
|
|
|
|
If an input name is None, it gets mapped to a None LookupBuilder.
|
|
|
|
"""
|
2015-11-30 15:02:09 +01:00
|
|
|
lookup_builders = []
|
|
|
|
for lookup in lookups:
|
|
|
|
if lookup is not None:
|
|
|
|
lookup_builders.append(self.named_lookups_.get(lookup.name))
|
|
|
|
else:
|
|
|
|
lookup_builders.append(None)
|
2015-12-03 13:05:42 +01:00
|
|
|
return lookup_builders
|
|
|
|
|
2016-01-08 08:32:47 +01:00
|
|
|
def add_attach_points(self, location, glyphs, contourPoints):
|
|
|
|
for glyph in glyphs:
|
|
|
|
self.attachPoints_.setdefault(glyph, set()).update(contourPoints)
|
|
|
|
|
2015-12-09 22:56:24 +01:00
|
|
|
def add_chain_context_pos(self, location, prefix, glyphs, suffix, lookups):
|
2015-12-09 23:53:20 +01:00
|
|
|
lookup = self.get_lookup_(location, ChainContextPosBuilder)
|
|
|
|
lookup.rules.append((prefix, glyphs, suffix,
|
|
|
|
self.find_lookup_builders_(lookups)))
|
2015-12-09 22:56:24 +01:00
|
|
|
|
2015-12-09 13:58:05 +01:00
|
|
|
def add_chain_context_subst(self, location,
|
2015-12-09 22:56:24 +01:00
|
|
|
prefix, glyphs, suffix, lookups):
|
2015-11-30 15:02:09 +01:00
|
|
|
lookup = self.get_lookup_(location, ChainContextSubstBuilder)
|
2015-12-09 22:56:24 +01:00
|
|
|
lookup.substitutions.append((prefix, glyphs, suffix,
|
2015-12-03 13:05:42 +01:00
|
|
|
self.find_lookup_builders_(lookups)))
|
2015-11-30 15:02:09 +01:00
|
|
|
|
2016-01-07 11:32:54 +01:00
|
|
|
def add_alternate_subst(self, location,
|
|
|
|
prefix, glyph, suffix, replacement):
|
|
|
|
if prefix or suffix:
|
|
|
|
lookup = self.get_chained_lookup_(location, AlternateSubstBuilder)
|
|
|
|
chain = self.get_lookup_(location, ChainContextSubstBuilder)
|
|
|
|
chain.substitutions.append((prefix, [glyph], suffix, [lookup]))
|
|
|
|
else:
|
|
|
|
lookup = self.get_lookup_(location, AlternateSubstBuilder)
|
2015-09-07 11:14:03 +02:00
|
|
|
if glyph in lookup.alternates:
|
|
|
|
raise FeatureLibError(
|
|
|
|
'Already defined alternates for glyph "%s"' % glyph,
|
|
|
|
location)
|
2016-01-07 11:32:54 +01:00
|
|
|
lookup.alternates[glyph] = replacement
|
2015-09-07 11:14:03 +02:00
|
|
|
|
2016-01-07 10:31:13 +01:00
|
|
|
def add_ligature_subst(self, location,
|
|
|
|
prefix, glyphs, suffix, replacement):
|
|
|
|
if prefix or suffix:
|
|
|
|
lookup = self.get_chained_lookup_(location, LigatureSubstBuilder)
|
|
|
|
chain = self.get_lookup_(location, ChainContextSubstBuilder)
|
|
|
|
chain.substitutions.append((prefix, glyphs, suffix, [lookup]))
|
|
|
|
else:
|
|
|
|
lookup = self.get_lookup_(location, LigatureSubstBuilder)
|
|
|
|
|
|
|
|
# OpenType feature file syntax, section 5.d, "Ligature substitution":
|
|
|
|
# "Since the OpenType specification does not allow ligature
|
|
|
|
# substitutions to be specified on target sequences that contain
|
|
|
|
# glyph classes, the implementation software will enumerate
|
|
|
|
# all specific glyph sequences if glyph classes are detected"
|
|
|
|
for g in sorted(itertools.product(*glyphs)):
|
|
|
|
lookup.ligatures[g] = replacement
|
2015-09-07 16:10:13 +02:00
|
|
|
|
2016-01-06 17:33:34 +01:00
|
|
|
def add_multiple_subst(self, location,
|
|
|
|
prefix, glyph, suffix, replacements):
|
|
|
|
if prefix or suffix:
|
|
|
|
sub = self.get_chained_lookup_(location, MultipleSubstBuilder)
|
|
|
|
sub.mapping[glyph] = replacements
|
|
|
|
lookup = self.get_lookup_(location, ChainContextSubstBuilder)
|
|
|
|
lookup.substitutions.append((prefix, [{glyph}], suffix, [sub]))
|
|
|
|
return
|
2015-09-10 15:28:02 +02:00
|
|
|
lookup = self.get_lookup_(location, MultipleSubstBuilder)
|
|
|
|
if glyph in lookup.mapping:
|
|
|
|
raise FeatureLibError(
|
|
|
|
'Already defined substitution for glyph "%s"' % glyph,
|
|
|
|
location)
|
|
|
|
lookup.mapping[glyph] = replacements
|
2015-09-08 12:05:44 +02:00
|
|
|
|
2015-12-09 13:58:05 +01:00
|
|
|
def add_reverse_chain_single_subst(self, location, old_prefix,
|
|
|
|
old_suffix, mapping):
|
2015-12-03 13:05:42 +01:00
|
|
|
lookup = self.get_lookup_(location, ReverseChainSingleSubstBuilder)
|
|
|
|
lookup.substitutions.append((old_prefix, old_suffix, mapping))
|
|
|
|
|
2016-01-06 16:15:26 +01:00
|
|
|
def add_single_subst(self, location, prefix, suffix, mapping):
|
|
|
|
if prefix or suffix:
|
|
|
|
sub = self.get_chained_lookup_(location, SingleSubstBuilder)
|
|
|
|
sub.mapping.update(mapping)
|
|
|
|
lookup = self.get_lookup_(location, ChainContextSubstBuilder)
|
|
|
|
lookup.substitutions.append(
|
|
|
|
(prefix, [mapping.keys()], suffix, [sub]))
|
|
|
|
return
|
2015-09-08 10:33:07 +02:00
|
|
|
lookup = self.get_lookup_(location, SingleSubstBuilder)
|
|
|
|
for (from_glyph, to_glyph) in mapping.items():
|
|
|
|
if from_glyph in lookup.mapping:
|
|
|
|
raise FeatureLibError(
|
|
|
|
'Already defined rule for replacing glyph "%s" by "%s"' %
|
|
|
|
(from_glyph, lookup.mapping[from_glyph]),
|
|
|
|
location)
|
|
|
|
lookup.mapping[from_glyph] = to_glyph
|
|
|
|
|
2015-12-09 12:59:20 +01:00
|
|
|
def add_cursive_pos(self, location, glyphclass, entryAnchor, exitAnchor):
|
|
|
|
lookup = self.get_lookup_(location, CursivePosBuilder)
|
2015-12-07 23:56:08 +01:00
|
|
|
lookup.add_attachment(
|
|
|
|
location, glyphclass,
|
|
|
|
makeOpenTypeAnchor(entryAnchor, otTables.EntryAnchor),
|
|
|
|
makeOpenTypeAnchor(exitAnchor, otTables.ExitAnchor))
|
|
|
|
|
2015-12-09 16:51:15 +01:00
|
|
|
def add_marks_(self, location, lookupBuilder, marks):
|
|
|
|
"""Helper for add_mark_{base,liga,mark}_pos."""
|
|
|
|
for _, markClass in marks:
|
2015-12-12 12:54:23 +01:00
|
|
|
for markClassDef in markClass.definitions:
|
|
|
|
for mark in markClassDef.glyphs.glyphSet():
|
|
|
|
if mark not in lookupBuilder.marks:
|
|
|
|
otMarkAnchor = makeOpenTypeAnchor(markClassDef.anchor,
|
|
|
|
otTables.MarkAnchor)
|
|
|
|
lookupBuilder.marks[mark] = (
|
|
|
|
markClass.name, otMarkAnchor)
|
2015-12-09 16:51:15 +01:00
|
|
|
|
2015-12-09 12:59:20 +01:00
|
|
|
def add_mark_base_pos(self, location, bases, marks):
|
|
|
|
builder = self.get_lookup_(location, MarkBasePosBuilder)
|
2015-12-09 16:51:15 +01:00
|
|
|
self.add_marks_(location, builder, marks)
|
2015-12-08 22:28:02 +01:00
|
|
|
for baseAnchor, markClass in marks:
|
|
|
|
otBaseAnchor = makeOpenTypeAnchor(baseAnchor, otTables.BaseAnchor)
|
2015-12-09 16:51:15 +01:00
|
|
|
for base in bases:
|
|
|
|
builder.bases.setdefault(base, {})[markClass.name] = (
|
|
|
|
otBaseAnchor)
|
|
|
|
|
|
|
|
def add_mark_lig_pos(self, location, ligatures, components):
|
|
|
|
builder = self.get_lookup_(location, MarkLigPosBuilder)
|
|
|
|
componentAnchors = []
|
|
|
|
for marks in components:
|
|
|
|
anchors = {}
|
|
|
|
self.add_marks_(location, builder, marks)
|
|
|
|
for ligAnchor, markClass in marks:
|
|
|
|
anchors[markClass.name] = (
|
|
|
|
makeOpenTypeAnchor(ligAnchor, otTables.LigatureAnchor))
|
|
|
|
componentAnchors.append(anchors)
|
|
|
|
for glyph in ligatures:
|
|
|
|
builder.ligatures[glyph] = componentAnchors
|
2015-12-08 19:04:42 +01:00
|
|
|
|
2015-12-09 17:56:47 +01:00
|
|
|
def add_mark_mark_pos(self, location, baseMarks, marks):
|
|
|
|
builder = self.get_lookup_(location, MarkMarkPosBuilder)
|
|
|
|
self.add_marks_(location, builder, marks)
|
|
|
|
for baseAnchor, markClass in marks:
|
|
|
|
otBaseAnchor = makeOpenTypeAnchor(baseAnchor, otTables.Mark2Anchor)
|
|
|
|
for baseMark in baseMarks:
|
|
|
|
builder.baseMarks.setdefault(baseMark, {})[markClass.name] = (
|
|
|
|
otBaseAnchor)
|
|
|
|
|
2015-12-21 16:06:59 +01:00
|
|
|
def add_class_pair_pos(self, location, glyphclass1, value1,
|
|
|
|
glyphclass2, value2):
|
2015-12-23 11:35:49 +01:00
|
|
|
lookup = self.get_lookup_(location, ClassPairPosBuilder)
|
|
|
|
lookup.add_pair(location, glyphclass1, value1, glyphclass2, value2)
|
2015-12-21 16:06:59 +01:00
|
|
|
|
|
|
|
def add_specific_pair_pos(self, location, glyph1, value1, glyph2, value2):
|
|
|
|
lookup = self.get_lookup_(location, SpecificPairPosBuilder)
|
|
|
|
lookup.add_pair(location, glyph1, value1, glyph2, value2)
|
2015-12-07 17:18:18 +01:00
|
|
|
|
2015-12-04 11:16:43 +01:00
|
|
|
def add_single_pos(self, location, glyph, valuerecord):
|
|
|
|
lookup = self.get_lookup_(location, SinglePosBuilder)
|
|
|
|
curValue = lookup.mapping.get(glyph)
|
|
|
|
if curValue is not None and curValue != valuerecord:
|
|
|
|
otherLoc = valuerecord.location
|
|
|
|
raise FeatureLibError(
|
|
|
|
'Already defined different position for glyph "%s" at %s:%d:%d'
|
|
|
|
% (glyph, otherLoc[0], otherLoc[1], otherLoc[2]),
|
|
|
|
location)
|
|
|
|
lookup.mapping[glyph] = valuerecord
|
|
|
|
|
2016-01-07 17:22:31 +01:00
|
|
|
def add_ligatureCaretByIndex_(self, location, glyphs, carets):
|
|
|
|
for glyph in glyphs:
|
2016-01-08 11:03:46 +01:00
|
|
|
self.ligatureCaretByIndex_.setdefault(glyph, set()).update(carets)
|
2016-01-07 17:22:31 +01:00
|
|
|
|
2016-01-07 16:39:35 +01:00
|
|
|
def add_ligatureCaretByPos_(self, location, glyphs, carets):
|
|
|
|
for glyph in glyphs:
|
2016-01-08 11:03:46 +01:00
|
|
|
self.ligatureCaretByPos_.setdefault(glyph, set()).update(carets)
|
2016-01-07 16:39:35 +01:00
|
|
|
|
2015-12-04 11:16:43 +01:00
|
|
|
|
2015-12-05 08:15:05 +00:00
|
|
|
def _makeOpenTypeDeviceTable(deviceTable, device):
|
|
|
|
device = tuple(sorted(device))
|
|
|
|
deviceTable.StartSize = startSize = device[0][0]
|
|
|
|
deviceTable.EndSize = endSize = device[-1][0]
|
|
|
|
deviceDict = dict(device)
|
|
|
|
deviceTable.DeltaValue = deltaValues = [
|
|
|
|
deviceDict.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:
|
|
|
|
deviceTable.DeltaFormat = 1
|
|
|
|
elif minDelta > -9 and maxDelta < 8:
|
|
|
|
deviceTable.DeltaFormat = 2
|
|
|
|
else:
|
|
|
|
deviceTable.DeltaFormat = 3
|
|
|
|
|
|
|
|
|
2015-12-07 23:56:08 +01:00
|
|
|
def makeOpenTypeAnchor(anchor, anchorClass):
|
|
|
|
"""ast.Anchor --> otTables.Anchor"""
|
|
|
|
if anchor is None:
|
|
|
|
return None
|
|
|
|
anch = anchorClass()
|
|
|
|
anch.Format = 1
|
|
|
|
anch.XCoordinate, anch.YCoordinate = anchor.x, anchor.y
|
|
|
|
if anchor.contourpoint is not None:
|
|
|
|
anch.AnchorPoint = anchor.contourpoint
|
|
|
|
anch.Format = 2
|
|
|
|
if anchor.xDeviceTable is not None:
|
|
|
|
anch.XDeviceTable = otTables.XDeviceTable()
|
|
|
|
_makeOpenTypeDeviceTable(anch.XDeviceTable, anchor.xDeviceTable)
|
|
|
|
anch.Format = 3
|
|
|
|
if anchor.yDeviceTable is not None:
|
|
|
|
anch.YDeviceTable = otTables.YDeviceTable()
|
|
|
|
_makeOpenTypeDeviceTable(anch.YDeviceTable, anchor.yDeviceTable)
|
|
|
|
anch.Format = 3
|
|
|
|
return anch
|
|
|
|
|
|
|
|
|
2015-12-04 11:16:43 +01:00
|
|
|
def makeOpenTypeValueRecord(v):
|
|
|
|
"""ast.ValueRecord --> (otBase.ValueRecord, int ValueFormat)"""
|
2015-12-07 21:26:58 +01:00
|
|
|
if v is None:
|
|
|
|
return None, 0
|
2015-12-04 11:16:43 +01:00
|
|
|
vr = otBase.ValueRecord()
|
|
|
|
if v.xPlacement:
|
|
|
|
vr.XPlacement = v.xPlacement
|
|
|
|
if v.yPlacement:
|
|
|
|
vr.YPlacement = v.yPlacement
|
|
|
|
if v.xAdvance:
|
|
|
|
vr.XAdvance = v.xAdvance
|
|
|
|
if v.yAdvance:
|
|
|
|
vr.YAdvance = v.yAdvance
|
2015-12-04 15:49:04 +01:00
|
|
|
|
2015-12-05 08:15:05 +00:00
|
|
|
if v.xPlaDevice:
|
|
|
|
vr.XPlaDevice = otTables.XPlaDevice()
|
|
|
|
_makeOpenTypeDeviceTable(vr.XPlaDevice, v.xPlaDevice)
|
|
|
|
if v.yPlaDevice:
|
|
|
|
vr.YPlaDevice = otTables.YPlaDevice()
|
|
|
|
_makeOpenTypeDeviceTable(vr.YPlaDevice, v.yPlaDevice)
|
|
|
|
if v.xAdvDevice:
|
|
|
|
vr.XAdvDevice = otTables.XAdvDevice()
|
|
|
|
_makeOpenTypeDeviceTable(vr.XAdvDevice, v.xAdvDevice)
|
|
|
|
if v.yAdvDevice:
|
|
|
|
vr.YAdvDevice = otTables.YAdvDevice()
|
|
|
|
_makeOpenTypeDeviceTable(vr.YAdvDevice, v.yAdvDevice)
|
2015-12-04 15:49:04 +01:00
|
|
|
|
2015-12-04 11:16:43 +01:00
|
|
|
vrMask = 0
|
|
|
|
for mask, name, _, _ in otBase.valueRecordFormat:
|
|
|
|
if getattr(vr, name, 0) != 0:
|
|
|
|
vrMask |= mask
|
2015-12-07 11:47:55 +01:00
|
|
|
|
|
|
|
if vrMask == 0:
|
|
|
|
return None, 0
|
|
|
|
else:
|
|
|
|
return vr, vrMask
|
2015-12-04 11:16:43 +01:00
|
|
|
|
2015-09-07 11:14:03 +02:00
|
|
|
|
|
|
|
class LookupBuilder(object):
|
2015-12-10 19:17:11 +01:00
|
|
|
def __init__(self, font, location, table, lookup_type):
|
2015-12-04 11:04:37 +01:00
|
|
|
self.font = font
|
2015-09-07 11:14:03 +02:00
|
|
|
self.location = location
|
|
|
|
self.table, self.lookup_type = table, lookup_type
|
2015-12-10 19:17:11 +01:00
|
|
|
self.lookupflag = 0
|
|
|
|
self.markFilterSet = None
|
2015-09-07 17:22:37 +02:00
|
|
|
self.lookup_index = None # assigned when making final tables
|
2015-09-07 11:14:03 +02:00
|
|
|
assert table in ('GPOS', 'GSUB')
|
|
|
|
|
|
|
|
def equals(self, other):
|
|
|
|
return (isinstance(other, self.__class__) and
|
|
|
|
self.table == other.table and
|
2015-12-10 19:17:11 +01:00
|
|
|
self.lookupflag == other.lookupflag and
|
|
|
|
self.markFilterSet == other.markFilterSet)
|
2015-09-07 11:14:03 +02:00
|
|
|
|
2015-12-08 22:28:02 +01:00
|
|
|
def inferGlyphClasses(self):
|
|
|
|
"""Infers glyph glasses for the GDEF table, such as {"cedilla":3}."""
|
|
|
|
return {}
|
|
|
|
|
2015-12-09 12:30:57 +01:00
|
|
|
def buildCoverage_(self, glyphs, tableClass=otTables.Coverage):
|
|
|
|
coverage = tableClass()
|
|
|
|
coverage.glyphs = sorted(glyphs, key=self.font.getGlyphID)
|
|
|
|
return coverage
|
|
|
|
|
2015-12-09 12:51:01 +01:00
|
|
|
def buildLookup_(self, subtables):
|
|
|
|
lookup = otTables.Lookup()
|
2015-12-10 19:17:11 +01:00
|
|
|
lookup.LookupFlag = self.lookupflag
|
2015-12-09 12:51:01 +01:00
|
|
|
lookup.LookupType = self.lookup_type
|
|
|
|
lookup.SubTable = subtables
|
|
|
|
lookup.SubTableCount = len(subtables)
|
2015-12-10 19:17:11 +01:00
|
|
|
if self.markFilterSet is not None:
|
|
|
|
lookup.MarkFilteringSet = self.markFilterSet
|
2015-12-09 12:51:01 +01:00
|
|
|
return lookup
|
|
|
|
|
2015-12-09 16:51:15 +01:00
|
|
|
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
|
|
|
|
|
2015-12-04 11:04:37 +01:00
|
|
|
def setBacktrackCoverage_(self, prefix, subtable):
|
2015-12-03 13:05:42 +01:00
|
|
|
subtable.BacktrackGlyphCount = len(prefix)
|
|
|
|
subtable.BacktrackCoverage = []
|
|
|
|
for p in reversed(prefix):
|
2015-12-09 12:30:57 +01:00
|
|
|
coverage = self.buildCoverage_(p, otTables.BacktrackCoverage)
|
2015-12-03 13:05:42 +01:00
|
|
|
subtable.BacktrackCoverage.append(coverage)
|
|
|
|
|
2015-12-04 11:04:37 +01:00
|
|
|
def setLookAheadCoverage_(self, suffix, subtable):
|
2015-12-03 13:05:42 +01:00
|
|
|
subtable.LookAheadGlyphCount = len(suffix)
|
|
|
|
subtable.LookAheadCoverage = []
|
|
|
|
for s in suffix:
|
2015-12-09 12:30:57 +01:00
|
|
|
coverage = self.buildCoverage_(s, otTables.LookAheadCoverage)
|
2015-12-03 13:05:42 +01:00
|
|
|
subtable.LookAheadCoverage.append(coverage)
|
|
|
|
|
2015-12-04 11:04:37 +01:00
|
|
|
def setInputCoverage_(self, glyphs, subtable):
|
2015-12-03 13:05:42 +01:00
|
|
|
subtable.InputGlyphCount = len(glyphs)
|
|
|
|
subtable.InputCoverage = []
|
|
|
|
for g in glyphs:
|
2015-12-09 12:30:57 +01:00
|
|
|
coverage = self.buildCoverage_(g, otTables.InputCoverage)
|
2015-12-03 13:05:42 +01:00
|
|
|
subtable.InputCoverage.append(coverage)
|
|
|
|
|
2015-12-09 13:23:50 +01:00
|
|
|
def setMarkArray_(self, marks, markClassIDs, subtable):
|
|
|
|
"""Helper for MarkBasePosBuilder and MarkLigPosBuilder."""
|
|
|
|
subtable.MarkArray = otTables.MarkArray()
|
|
|
|
subtable.MarkArray.MarkCount = len(marks)
|
|
|
|
subtable.MarkArray.MarkRecord = []
|
|
|
|
for mark in subtable.MarkCoverage.glyphs:
|
|
|
|
markClassName, markAnchor = self.marks[mark]
|
|
|
|
markrec = otTables.MarkRecord()
|
|
|
|
markrec.Class = markClassIDs[markClassName]
|
|
|
|
markrec.MarkAnchor = markAnchor
|
|
|
|
subtable.MarkArray.MarkRecord.append(markrec)
|
|
|
|
|
2015-12-09 17:56:47 +01:00
|
|
|
def setMark1Array_(self, marks, markClassIDs, subtable):
|
|
|
|
"""Helper for MarkMarkPosBuilder."""
|
|
|
|
subtable.Mark1Array = otTables.Mark1Array()
|
|
|
|
subtable.Mark1Array.MarkCount = len(marks)
|
|
|
|
subtable.Mark1Array.MarkRecord = []
|
|
|
|
for mark in subtable.Mark1Coverage.glyphs:
|
|
|
|
markClassName, markAnchor = self.marks[mark]
|
|
|
|
markrec = otTables.MarkRecord()
|
|
|
|
markrec.Class = markClassIDs[markClassName]
|
|
|
|
markrec.MarkAnchor = markAnchor
|
|
|
|
subtable.Mark1Array.MarkRecord.append(markrec)
|
|
|
|
|
2015-09-07 11:14:03 +02:00
|
|
|
|
|
|
|
class AlternateSubstBuilder(LookupBuilder):
|
2015-12-10 19:17:11 +01:00
|
|
|
def __init__(self, font, location):
|
|
|
|
LookupBuilder.__init__(self, font, location, 'GSUB', 3)
|
2015-09-07 11:14:03 +02:00
|
|
|
self.alternates = {}
|
|
|
|
|
|
|
|
def equals(self, other):
|
|
|
|
return (LookupBuilder.equals(self, other) and
|
|
|
|
self.alternates == other.alternates)
|
|
|
|
|
|
|
|
def build(self):
|
2015-12-09 12:51:01 +01:00
|
|
|
subtable = otTables.AlternateSubst()
|
|
|
|
subtable.Format = 1
|
|
|
|
subtable.alternates = self.alternates
|
|
|
|
return self.buildLookup_([subtable])
|
2015-11-30 15:02:09 +01:00
|
|
|
|
|
|
|
|
2015-12-09 23:53:20 +01:00
|
|
|
class ChainContextPosBuilder(LookupBuilder):
|
2015-12-10 19:17:11 +01:00
|
|
|
def __init__(self, font, location):
|
|
|
|
LookupBuilder.__init__(self, font, location, 'GPOS', 8)
|
2015-12-09 23:53:20 +01:00
|
|
|
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:
|
|
|
|
st = otTables.ChainContextPos()
|
|
|
|
subtables.append(st)
|
|
|
|
st.Format = 3
|
|
|
|
self.setBacktrackCoverage_(prefix, st)
|
|
|
|
self.setLookAheadCoverage_(suffix, st)
|
|
|
|
self.setInputCoverage_(glyphs, st)
|
|
|
|
|
|
|
|
st.PosCount = len([l for l in lookups if l is not None])
|
|
|
|
st.PosLookupRecord = []
|
|
|
|
for sequenceIndex, l in enumerate(lookups):
|
|
|
|
if l is not None:
|
|
|
|
rec = otTables.PosLookupRecord()
|
|
|
|
rec.SequenceIndex = sequenceIndex
|
|
|
|
rec.LookupListIndex = l.lookup_index
|
|
|
|
st.PosLookupRecord.append(rec)
|
|
|
|
return self.buildLookup_(subtables)
|
|
|
|
|
|
|
|
|
2015-11-30 15:02:09 +01:00
|
|
|
class ChainContextSubstBuilder(LookupBuilder):
|
2015-12-10 19:17:11 +01:00
|
|
|
def __init__(self, font, location):
|
|
|
|
LookupBuilder.__init__(self, font, location, 'GSUB', 6)
|
2015-11-30 15:02:09 +01:00
|
|
|
self.substitutions = [] # (prefix, input, suffix, lookups)
|
|
|
|
|
|
|
|
def equals(self, other):
|
|
|
|
return (LookupBuilder.equals(self, other) and
|
|
|
|
self.substitutions == other.substitutions)
|
|
|
|
|
|
|
|
def build(self):
|
2015-12-09 12:51:01 +01:00
|
|
|
subtables = []
|
2015-11-30 15:02:09 +01:00
|
|
|
for (prefix, input, suffix, lookups) in self.substitutions:
|
|
|
|
st = otTables.ChainContextSubst()
|
2015-12-09 12:51:01 +01:00
|
|
|
subtables.append(st)
|
2015-11-30 15:02:09 +01:00
|
|
|
st.Format = 3
|
2015-12-03 13:05:42 +01:00
|
|
|
self.setBacktrackCoverage_(prefix, st)
|
|
|
|
self.setLookAheadCoverage_(suffix, st)
|
|
|
|
self.setInputCoverage_(input, st)
|
2015-11-30 15:02:09 +01:00
|
|
|
|
|
|
|
st.SubstCount = len([l for l in lookups if l is not None])
|
|
|
|
st.SubstLookupRecord = []
|
|
|
|
for sequenceIndex, l in enumerate(lookups):
|
|
|
|
if l is not None:
|
|
|
|
rec = otTables.SubstLookupRecord()
|
|
|
|
rec.SequenceIndex = sequenceIndex
|
|
|
|
rec.LookupListIndex = l.lookup_index
|
|
|
|
st.SubstLookupRecord.append(rec)
|
2015-12-09 12:51:01 +01:00
|
|
|
return self.buildLookup_(subtables)
|
2015-09-07 16:10:13 +02:00
|
|
|
|
|
|
|
|
|
|
|
class LigatureSubstBuilder(LookupBuilder):
|
2015-12-10 19:17:11 +01:00
|
|
|
def __init__(self, font, location):
|
|
|
|
LookupBuilder.__init__(self, font, location, 'GSUB', 4)
|
2015-09-07 16:10:13 +02:00
|
|
|
self.ligatures = {} # {('f','f','i'): 'f_f_i'}
|
|
|
|
|
|
|
|
def equals(self, other):
|
|
|
|
return (LookupBuilder.equals(self, other) and
|
|
|
|
self.ligatures == other.ligatures)
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def make_key(components):
|
|
|
|
"""Computes a key for ordering ligatures in a GSUB Type-4 lookup.
|
|
|
|
|
|
|
|
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.
|
|
|
|
To make the tables easier to read, we use the 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)
|
|
|
|
|
|
|
|
def build(self):
|
2015-12-09 12:51:01 +01:00
|
|
|
subtable = otTables.LigatureSubst()
|
|
|
|
subtable.Format = 1
|
|
|
|
subtable.ligatures = {}
|
2015-09-07 16:10:13 +02:00
|
|
|
for components in sorted(self.ligatures.keys(), key=self.make_key):
|
|
|
|
lig = otTables.Ligature()
|
2015-10-27 22:47:26 +01:00
|
|
|
lig.Component = components[1:]
|
2015-09-07 16:10:13 +02:00
|
|
|
lig.LigGlyph = self.ligatures[components]
|
2015-12-09 12:51:01 +01:00
|
|
|
subtable.ligatures.setdefault(components[0], []).append(lig)
|
|
|
|
return self.buildLookup_([subtable])
|
2015-09-08 10:33:07 +02:00
|
|
|
|
|
|
|
|
2015-09-10 15:28:02 +02:00
|
|
|
class MultipleSubstBuilder(LookupBuilder):
|
2015-12-10 19:17:11 +01:00
|
|
|
def __init__(self, font, location):
|
|
|
|
LookupBuilder.__init__(self, font, location, 'GSUB', 2)
|
2015-09-10 15:28:02 +02:00
|
|
|
self.mapping = {}
|
|
|
|
|
|
|
|
def equals(self, other):
|
|
|
|
return (LookupBuilder.equals(self, other) and
|
|
|
|
self.mapping == other.mapping)
|
|
|
|
|
|
|
|
def build(self):
|
2015-12-09 12:51:01 +01:00
|
|
|
subtable = otTables.MultipleSubst()
|
|
|
|
subtable.mapping = self.mapping
|
|
|
|
return self.buildLookup_([subtable])
|
2015-09-10 15:28:02 +02:00
|
|
|
|
|
|
|
|
2015-12-21 16:06:59 +01:00
|
|
|
class SpecificPairPosBuilder(LookupBuilder):
|
2015-12-10 19:17:11 +01:00
|
|
|
def __init__(self, font, location):
|
|
|
|
LookupBuilder.__init__(self, font, location, 'GPOS', 2)
|
2015-12-07 21:26:58 +01:00
|
|
|
self.pairs = {} # (gc1, gc2) -> (location, value1, value2)
|
|
|
|
|
2015-12-21 16:06:59 +01:00
|
|
|
def add_pair(self, location, glyph1, value1, glyph2, value2):
|
|
|
|
oldValue = self.pairs.get((glyph1, glyph2), None)
|
2015-12-07 21:26:58 +01:00
|
|
|
if oldValue is not None:
|
|
|
|
otherLoc, _, _ = oldValue
|
|
|
|
raise FeatureLibError(
|
2015-12-21 16:06:59 +01:00
|
|
|
'Already defined position for pair %s %s at %s:%d:%d'
|
|
|
|
% (glyph1, glyph2, otherLoc[0], otherLoc[1], otherLoc[2]),
|
2015-12-07 21:26:58 +01:00
|
|
|
location)
|
2015-12-21 16:06:59 +01:00
|
|
|
self.pairs[(glyph1, glyph2)] = (location, value1, value2)
|
2015-12-07 21:26:58 +01:00
|
|
|
|
|
|
|
def equals(self, other):
|
|
|
|
return (LookupBuilder.equals(self, other) and
|
|
|
|
self.pairs == other.pairs)
|
|
|
|
|
|
|
|
def build(self):
|
|
|
|
subtables = []
|
|
|
|
|
|
|
|
# (valueFormat1, valueFormat2) --> [(glyph1, glyph2, value1, value2)*]
|
|
|
|
format1 = {}
|
2015-12-21 16:06:59 +01:00
|
|
|
for (glyph1, glyph2), (location, value1, value2) in self.pairs.items():
|
|
|
|
val1, valFormat1 = makeOpenTypeValueRecord(value1)
|
|
|
|
val2, valFormat2 = makeOpenTypeValueRecord(value2)
|
|
|
|
format1.setdefault(((valFormat1, valFormat2)), []).append(
|
|
|
|
(glyph1, glyph2, val1, val2))
|
2015-12-07 21:26:58 +01:00
|
|
|
for (vf1, vf2), pairs in sorted(format1.items()):
|
|
|
|
p = {}
|
|
|
|
for glyph1, glyph2, val1, val2 in pairs:
|
|
|
|
p.setdefault(glyph1, []).append((glyph2, val1, val2))
|
|
|
|
st = otTables.PairPos()
|
|
|
|
subtables.append(st)
|
|
|
|
st.Format = 1
|
|
|
|
st.ValueFormat1, st.ValueFormat2 = vf1, vf2
|
2015-12-09 12:30:57 +01:00
|
|
|
st.Coverage = self.buildCoverage_(p)
|
2015-12-07 21:26:58 +01:00
|
|
|
st.PairSet = []
|
|
|
|
for glyph in st.Coverage.glyphs:
|
|
|
|
ps = otTables.PairSet()
|
|
|
|
ps.PairValueRecord = []
|
|
|
|
st.PairSet.append(ps)
|
|
|
|
for glyph2, val1, val2 in sorted(
|
|
|
|
p[glyph], key=lambda x: self.font.getGlyphID(x[0])):
|
|
|
|
pvr = otTables.PairValueRecord()
|
|
|
|
pvr.SecondGlyph = glyph2
|
|
|
|
pvr.Value1, pvr.Value2 = val1, val2
|
|
|
|
ps.PairValueRecord.append(pvr)
|
|
|
|
ps.PairValueCount = len(ps.PairValueRecord)
|
|
|
|
st.PairSetCount = len(st.PairSet)
|
2015-12-09 12:51:01 +01:00
|
|
|
return self.buildLookup_(subtables)
|
2015-12-07 21:26:58 +01:00
|
|
|
|
|
|
|
|
2015-12-09 12:59:20 +01:00
|
|
|
class CursivePosBuilder(LookupBuilder):
|
2015-12-10 19:17:11 +01:00
|
|
|
def __init__(self, font, location):
|
|
|
|
LookupBuilder.__init__(self, font, location, 'GPOS', 3)
|
2015-12-07 23:56:08 +01:00
|
|
|
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] = (location, entryAnchor, exitAnchor)
|
|
|
|
|
|
|
|
def build(self):
|
|
|
|
st = otTables.CursivePos()
|
|
|
|
st.Format = 1
|
2015-12-09 12:30:57 +01:00
|
|
|
st.Coverage = self.buildCoverage_(self.attachments.keys())
|
2015-12-07 23:56:08 +01:00
|
|
|
st.EntryExitCount = len(self.attachments)
|
|
|
|
st.EntryExitRecord = []
|
|
|
|
for glyph in st.Coverage.glyphs:
|
|
|
|
location, entryAnchor, exitAnchor = self.attachments[glyph]
|
|
|
|
rec = otTables.EntryExitRecord()
|
|
|
|
st.EntryExitRecord.append(rec)
|
|
|
|
rec.EntryAnchor = entryAnchor
|
|
|
|
rec.ExitAnchor = exitAnchor
|
2015-12-09 12:51:01 +01:00
|
|
|
return self.buildLookup_([st])
|
2015-12-07 23:56:08 +01:00
|
|
|
|
|
|
|
|
2015-12-09 12:59:20 +01:00
|
|
|
class MarkBasePosBuilder(LookupBuilder):
|
2015-12-10 19:17:11 +01:00
|
|
|
def __init__(self, font, location):
|
|
|
|
LookupBuilder.__init__(self, font, location, 'GPOS', 4)
|
2015-12-08 22:28:02 +01:00
|
|
|
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):
|
2015-12-09 16:51:15 +01:00
|
|
|
result = {glyph: 1 for glyph in self.bases}
|
|
|
|
result.update({glyph: 3 for glyph in self.marks})
|
2015-12-08 22:28:02 +01:00
|
|
|
return result
|
2015-12-08 19:04:42 +01:00
|
|
|
|
|
|
|
def build(self):
|
2015-12-08 22:28:02 +01:00
|
|
|
# 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.
|
|
|
|
|
|
|
|
st = otTables.MarkBasePos()
|
|
|
|
st.Format = 1
|
2015-12-09 13:23:50 +01:00
|
|
|
st.MarkCoverage = \
|
|
|
|
self.buildCoverage_(self.marks, otTables.MarkCoverage)
|
2015-12-09 16:51:15 +01:00
|
|
|
markClasses = self.buildMarkClasses_(self.marks)
|
2015-12-09 13:23:50 +01:00
|
|
|
st.ClassCount = len(markClasses)
|
|
|
|
self.setMarkArray_(self.marks, markClasses, st)
|
|
|
|
|
|
|
|
st.BaseCoverage = \
|
|
|
|
self.buildCoverage_(self.bases, otTables.BaseCoverage)
|
2015-12-08 22:28:02 +01:00
|
|
|
st.BaseArray = otTables.BaseArray()
|
2015-12-09 12:30:57 +01:00
|
|
|
st.BaseArray.BaseCount = len(st.BaseCoverage.glyphs)
|
2015-12-08 22:28:02 +01:00
|
|
|
st.BaseArray.BaseRecord = []
|
2015-12-09 12:30:57 +01:00
|
|
|
for base in st.BaseCoverage.glyphs:
|
2015-12-08 22:28:02 +01:00
|
|
|
baserec = otTables.BaseRecord()
|
|
|
|
st.BaseArray.BaseRecord.append(baserec)
|
|
|
|
baserec.BaseAnchor = []
|
2015-12-09 13:23:50 +01:00
|
|
|
for markClass in sorted(markClasses.keys(), key=markClasses.get):
|
2015-12-08 22:28:02 +01:00
|
|
|
baserec.BaseAnchor.append(self.bases[base].get(markClass))
|
|
|
|
|
2015-12-09 12:51:01 +01:00
|
|
|
return self.buildLookup_([st])
|
2015-12-08 22:28:02 +01:00
|
|
|
|
2015-12-09 16:51:15 +01:00
|
|
|
|
|
|
|
class MarkLigPosBuilder(LookupBuilder):
|
2015-12-10 19:17:11 +01:00
|
|
|
def __init__(self, font, location):
|
|
|
|
LookupBuilder.__init__(self, font, location, 'GPOS', 5)
|
2015-12-09 16:51:15 +01:00
|
|
|
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):
|
|
|
|
st = otTables.MarkLigPos()
|
|
|
|
st.Format = 1
|
|
|
|
st.MarkCoverage = \
|
|
|
|
self.buildCoverage_(self.marks, otTables.MarkCoverage)
|
|
|
|
markClasses = self.buildMarkClasses_(self.marks)
|
|
|
|
st.ClassCount = len(markClasses)
|
|
|
|
self.setMarkArray_(self.marks, markClasses, st)
|
|
|
|
|
|
|
|
st.LigatureCoverage = \
|
|
|
|
self.buildCoverage_(self.ligatures, otTables.LigatureCoverage)
|
|
|
|
st.LigatureArray = otTables.LigatureArray()
|
|
|
|
st.LigatureArray.LigatureCount = len(self.ligatures)
|
|
|
|
st.LigatureArray.LigatureAttach = []
|
|
|
|
for lig in st.LigatureCoverage.glyphs:
|
|
|
|
components = self.ligatures[lig]
|
|
|
|
attach = otTables.LigatureAttach()
|
|
|
|
attach.ComponentCount = len(components)
|
|
|
|
attach.ComponentRecord = []
|
|
|
|
for component in components:
|
|
|
|
crec = otTables.ComponentRecord()
|
|
|
|
attach.ComponentRecord.append(crec)
|
|
|
|
crec.LigatureAnchor = []
|
|
|
|
for markClass in sorted(markClasses.keys(),
|
|
|
|
key=markClasses.get):
|
|
|
|
crec.LigatureAnchor.append(component.get(markClass))
|
|
|
|
st.LigatureArray.LigatureAttach.append(attach)
|
|
|
|
|
|
|
|
return self.buildLookup_([st])
|
2015-12-08 19:04:42 +01:00
|
|
|
|
|
|
|
|
2015-12-09 17:56:47 +01:00
|
|
|
class MarkMarkPosBuilder(LookupBuilder):
|
2015-12-10 19:17:11 +01:00
|
|
|
def __init__(self, font, location):
|
|
|
|
LookupBuilder.__init__(self, font, location, 'GPOS', 6)
|
2015-12-09 17:56:47 +01:00
|
|
|
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):
|
|
|
|
st = otTables.MarkMarkPos()
|
|
|
|
st.Format = 1
|
|
|
|
st.Mark1Coverage = \
|
|
|
|
self.buildCoverage_(self.marks, otTables.Mark1Coverage)
|
|
|
|
markClasses = self.buildMarkClasses_(self.marks)
|
|
|
|
st.ClassCount = len(markClasses)
|
|
|
|
self.setMark1Array_(self.marks, markClasses, st)
|
|
|
|
|
|
|
|
st.Mark2Coverage = \
|
|
|
|
self.buildCoverage_(self.baseMarks, otTables.Mark2Coverage)
|
|
|
|
st.Mark2Array = otTables.Mark2Array()
|
|
|
|
st.Mark2Array.Mark2Count = len(st.Mark2Coverage.glyphs)
|
|
|
|
st.Mark2Array.Mark2Record = []
|
|
|
|
for base in st.Mark2Coverage.glyphs:
|
|
|
|
baserec = otTables.Mark2Record()
|
|
|
|
st.Mark2Array.Mark2Record.append(baserec)
|
|
|
|
baserec.Mark2Anchor = []
|
|
|
|
for markClass in sorted(markClasses.keys(), key=markClasses.get):
|
|
|
|
baserec.Mark2Anchor.append(self.baseMarks[base].get(markClass))
|
|
|
|
|
|
|
|
return self.buildLookup_([st])
|
|
|
|
|
|
|
|
|
2015-12-03 13:05:42 +01:00
|
|
|
class ReverseChainSingleSubstBuilder(LookupBuilder):
|
2015-12-10 19:17:11 +01:00
|
|
|
def __init__(self, font, location):
|
|
|
|
LookupBuilder.__init__(self, font, location, 'GSUB', 8)
|
2015-12-03 13:05:42 +01:00
|
|
|
self.substitutions = [] # (prefix, suffix, mapping)
|
|
|
|
|
|
|
|
def equals(self, other):
|
|
|
|
return (LookupBuilder.equals(self, other) and
|
|
|
|
self.substitutions == other.substitutions)
|
|
|
|
|
|
|
|
def build(self):
|
2015-12-09 12:51:01 +01:00
|
|
|
subtables = []
|
2015-12-03 13:05:42 +01:00
|
|
|
for prefix, suffix, mapping in self.substitutions:
|
|
|
|
st = otTables.ReverseChainSingleSubst()
|
|
|
|
st.Format = 1
|
|
|
|
self.setBacktrackCoverage_(prefix, st)
|
|
|
|
self.setLookAheadCoverage_(suffix, st)
|
2015-12-09 12:30:57 +01:00
|
|
|
st.Coverage = self.buildCoverage_(mapping.keys())
|
|
|
|
st.GlyphCount = len(mapping)
|
|
|
|
st.Substitute = [mapping[g] for g in st.Coverage.glyphs]
|
2015-12-09 12:51:01 +01:00
|
|
|
subtables.append(st)
|
|
|
|
return self.buildLookup_(subtables)
|
2015-12-03 13:05:42 +01:00
|
|
|
|
|
|
|
|
2015-09-08 10:33:07 +02:00
|
|
|
class SingleSubstBuilder(LookupBuilder):
|
2015-12-10 19:17:11 +01:00
|
|
|
def __init__(self, font, location):
|
|
|
|
LookupBuilder.__init__(self, font, location, 'GSUB', 1)
|
2015-09-08 10:33:07 +02:00
|
|
|
self.mapping = {}
|
|
|
|
|
|
|
|
def equals(self, other):
|
|
|
|
return (LookupBuilder.equals(self, other) and
|
|
|
|
self.mapping == other.mapping)
|
|
|
|
|
|
|
|
def build(self):
|
2015-12-09 12:51:01 +01:00
|
|
|
subtable = otTables.SingleSubst()
|
|
|
|
subtable.mapping = self.mapping
|
|
|
|
return self.buildLookup_([subtable])
|
2015-12-04 11:16:43 +01:00
|
|
|
|
|
|
|
|
2015-12-23 11:35:49 +01:00
|
|
|
class ClassPairPosSubtableBuilder(object):
|
|
|
|
def __init__(self, builder, valueFormat1, valueFormat2):
|
|
|
|
self.builder_ = builder
|
|
|
|
self.classDef1_, self.classDef2_ = None, None
|
|
|
|
self.coverage_ = set()
|
|
|
|
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(otTables.ClassDef1)
|
|
|
|
self.classDef2_ = ClassDefBuilder(otTables.ClassDef2)
|
|
|
|
self.coverage_ = set()
|
|
|
|
self.values_ = {}
|
|
|
|
self.classDef1_.add(gc1)
|
|
|
|
self.classDef2_.add(gc2)
|
|
|
|
self.coverage_.update(gc1)
|
|
|
|
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 = otTables.PairPos()
|
|
|
|
st.Format = 2
|
|
|
|
st.Coverage = self.builder_.buildCoverage_(self.coverage_)
|
|
|
|
st.ValueFormat1 = self.valueFormat1_
|
|
|
|
st.ValueFormat2 = self.valueFormat2_
|
2015-12-23 15:14:00 +01:00
|
|
|
st.ClassDef1 = self.classDef1_.build()
|
|
|
|
st.ClassDef2 = self.classDef2_.build()
|
2015-12-23 11:35:49 +01:00
|
|
|
classes1 = self.classDef1_.classes()
|
|
|
|
classes2 = self.classDef2_.classes()
|
|
|
|
st.Class1Count, st.Class2Count = len(classes1), len(classes2)
|
|
|
|
st.Class1Record = []
|
|
|
|
for c1 in classes1:
|
|
|
|
rec1 = otTables.Class1Record()
|
|
|
|
rec1.Class2Record = []
|
|
|
|
st.Class1Record.append(rec1)
|
|
|
|
for c2 in classes2:
|
|
|
|
rec2 = otTables.Class2Record()
|
|
|
|
val1, val2 = self.values_.get((c1, c2), (None, None))
|
|
|
|
rec2.Value1, rec2.Value2 = val1, val2
|
|
|
|
rec1.Class2Record.append(rec2)
|
|
|
|
self.subtables_.append(st)
|
|
|
|
|
|
|
|
|
|
|
|
class ClassPairPosBuilder(LookupBuilder):
|
|
|
|
SUBTABLE_BREAK_ = "SUBTABLE_BREAK"
|
|
|
|
|
|
|
|
def __init__(self, font, location):
|
|
|
|
LookupBuilder.__init__(self, font, location, 'GPOS', 2)
|
|
|
|
self.pairs = [] # [(location, gc1, value1, gc2, value2)*]
|
|
|
|
|
|
|
|
def add_pair(self, location, glyphclass1, value1, glyphclass2, value2):
|
|
|
|
self.pairs.append((location, glyphclass1, value1, glyphclass2, value2))
|
|
|
|
|
|
|
|
def add_subtable_break(self, location):
|
|
|
|
self.pairs.append((location,
|
|
|
|
self.SUBTABLE_BREAK_, self.SUBTABLE_BREAK_,
|
|
|
|
self.SUBTABLE_BREAK_, self.SUBTABLE_BREAK_))
|
|
|
|
|
|
|
|
def equals(self, other):
|
|
|
|
return (LookupBuilder.equals(self, other) and
|
|
|
|
self.pairs == other.pairs)
|
|
|
|
|
|
|
|
def build(self):
|
|
|
|
builders = {}
|
|
|
|
builder = None
|
|
|
|
for location, glyphclass1, value1, glyphclass2, value2 in self.pairs:
|
|
|
|
if glyphclass1 is self.SUBTABLE_BREAK_:
|
|
|
|
if builder is not None:
|
|
|
|
builder.addSubtableBreak()
|
|
|
|
continue
|
|
|
|
val1, valFormat1 = makeOpenTypeValueRecord(value1)
|
|
|
|
val2, valFormat2 = makeOpenTypeValueRecord(value2)
|
|
|
|
builder = builders.get((valFormat1, valFormat2))
|
|
|
|
if builder is None:
|
|
|
|
builder = ClassPairPosSubtableBuilder(
|
|
|
|
self, valFormat1, valFormat2)
|
|
|
|
builders[(valFormat1, valFormat2)] = builder
|
|
|
|
builder.addPair(glyphclass1, val1, glyphclass2, val2)
|
|
|
|
subtables = []
|
|
|
|
for key in sorted(builders.keys()):
|
|
|
|
subtables.extend(builders[key].subtables())
|
|
|
|
return self.buildLookup_(subtables)
|
|
|
|
|
|
|
|
|
2015-12-04 11:16:43 +01:00
|
|
|
class SinglePosBuilder(LookupBuilder):
|
2015-12-10 19:17:11 +01:00
|
|
|
def __init__(self, font, location):
|
|
|
|
LookupBuilder.__init__(self, font, location, 'GPOS', 1)
|
2015-12-04 11:16:43 +01:00
|
|
|
self.mapping = {} # glyph -> ast.ValueRecord
|
|
|
|
|
|
|
|
def equals(self, other):
|
|
|
|
return (LookupBuilder.equals(self, other) and
|
|
|
|
self.mapping == other.mapping)
|
|
|
|
|
|
|
|
def build(self):
|
|
|
|
subtables = []
|
|
|
|
|
|
|
|
# If multiple glyphs have the same ValueRecord, they can go into
|
|
|
|
# the same subtable which saves space. Therefore, we first build
|
|
|
|
# a reverse mapping from ValueRecord to glyph coverage.
|
|
|
|
values = {}
|
|
|
|
for glyph, valuerecord in self.mapping.items():
|
|
|
|
values.setdefault(valuerecord, []).append(glyph)
|
|
|
|
|
|
|
|
# For compliance with the OpenType specification,
|
|
|
|
# we sort the glyph coverage by glyph ID.
|
|
|
|
for glyphs in values.values():
|
|
|
|
glyphs.sort(key=self.font.getGlyphID)
|
|
|
|
|
2015-12-07 11:39:14 +01:00
|
|
|
# Make a list of (glyphs, (otBase.ValueRecord, int valueFormat)).
|
|
|
|
# Glyphs with the same otBase.ValueRecord are grouped into one item.
|
|
|
|
values = [(glyphs, makeOpenTypeValueRecord(valrec))
|
|
|
|
for valrec, glyphs in values.items()]
|
|
|
|
|
|
|
|
# Find out which glyphs should be encoded as SinglePos format 2.
|
|
|
|
# Format 2 is more compact than format 1 when multiple glyphs
|
|
|
|
# have different values but share the same integer valueFormat.
|
|
|
|
format2 = {} # valueFormat --> [(glyph, value), (glyph, value), ...]
|
|
|
|
for glyphs, (value, valueFormat) in values:
|
|
|
|
if len(glyphs) == 1:
|
|
|
|
glyph = glyphs[0]
|
|
|
|
format2.setdefault(valueFormat, []).append((glyph, value))
|
|
|
|
|
|
|
|
# Only use format 2 if multiple glyphs share the same valueFormat.
|
|
|
|
# Otherwise, format 1 is more compact.
|
|
|
|
format2 = [(valueFormat, valueList)
|
|
|
|
for valueFormat, valueList in format2.items()
|
|
|
|
if len(valueList) > 1]
|
|
|
|
format2.sort()
|
|
|
|
format2Glyphs = set() # {"A", "B", "C"}
|
|
|
|
for _, valueList in format2:
|
|
|
|
for (glyph, _) in valueList:
|
|
|
|
format2Glyphs.add(glyph)
|
|
|
|
for valueFormat, valueList in format2:
|
|
|
|
valueList.sort(key=lambda x: self.font.getGlyphID(x[0]))
|
|
|
|
st = otTables.SinglePos()
|
|
|
|
subtables.append(st)
|
|
|
|
st.Format = 2
|
|
|
|
st.ValueFormat = valueFormat
|
|
|
|
st.Coverage = otTables.Coverage()
|
|
|
|
st.Coverage.glyphs = [glyph for glyph, _value in valueList]
|
|
|
|
st.ValueCount = len(valueList)
|
|
|
|
st.Value = [value for _glyph, value in valueList]
|
|
|
|
|
2015-12-04 11:16:43 +01:00
|
|
|
# To make the ordering of our subtables deterministic,
|
|
|
|
# we sort subtables by the first glyph ID in their coverage.
|
|
|
|
# Not doing this would be OK for OpenType, but testing the
|
|
|
|
# compiler would be harder with non-deterministic output.
|
2015-12-07 11:39:14 +01:00
|
|
|
values.sort(key=lambda x: self.font.getGlyphID(x[0][0]))
|
2015-12-04 11:16:43 +01:00
|
|
|
|
2015-12-07 11:39:14 +01:00
|
|
|
for glyphs, (value, valueFormat) in values:
|
|
|
|
if len(glyphs) == 1 and glyphs[0] in format2Glyphs:
|
|
|
|
continue # already emitted as part of a format 2 subtable
|
2015-12-04 11:16:43 +01:00
|
|
|
st = otTables.SinglePos()
|
|
|
|
subtables.append(st)
|
|
|
|
st.Format = 1
|
2015-12-09 12:30:57 +01:00
|
|
|
st.Coverage = self.buildCoverage_(glyphs)
|
2015-12-07 11:39:14 +01:00
|
|
|
st.Value, st.ValueFormat = value, valueFormat
|
2015-12-04 11:16:43 +01:00
|
|
|
|
2015-12-09 12:51:01 +01:00
|
|
|
return self.buildLookup_(subtables)
|
2015-12-22 14:42:13 +01:00
|
|
|
|
|
|
|
|
|
|
|
class ClassDefBuilder(object):
|
|
|
|
"""Helper for building ClassDef tables."""
|
|
|
|
def __init__(self, otClass):
|
2015-12-23 11:35:49 +01:00
|
|
|
self.classes_ = set()
|
|
|
|
self.glyphs_ = {}
|
|
|
|
self.otClass_ = otClass
|
2015-12-22 14:42:13 +01:00
|
|
|
|
|
|
|
def canAdd(self, glyphs):
|
|
|
|
glyphs = frozenset(glyphs)
|
2015-12-23 11:35:49 +01:00
|
|
|
if glyphs in self.classes_:
|
2015-12-22 14:42:13 +01:00
|
|
|
return True
|
|
|
|
for glyph in glyphs:
|
2015-12-23 11:35:49 +01:00
|
|
|
if glyph in self.glyphs_:
|
2015-12-22 14:42:13 +01:00
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
|
|
def add(self, glyphs):
|
|
|
|
glyphs = frozenset(glyphs)
|
2015-12-23 11:35:49 +01:00
|
|
|
if glyphs in self.classes_:
|
2015-12-22 14:42:13 +01:00
|
|
|
return
|
2015-12-23 11:35:49 +01:00
|
|
|
self.classes_.add(glyphs)
|
2015-12-22 14:42:13 +01:00
|
|
|
for glyph in glyphs:
|
2015-12-23 11:35:49 +01:00
|
|
|
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
|
2015-12-23 15:14:00 +01:00
|
|
|
# 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.
|
2015-12-23 11:35:49 +01:00
|
|
|
#
|
|
|
|
# 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
|
2015-12-23 15:14:00 +01:00
|
|
|
# compact, whereas a non-contiguous set might need a lot of bytes
|
|
|
|
# in the output file. We don't get this right with key=len below.
|
|
|
|
result = sorted(self.classes_, key=len, reverse=True)
|
|
|
|
if self.otClass_ is not otTables.ClassDef1:
|
|
|
|
result.insert(0, frozenset())
|
|
|
|
return result
|
2015-12-23 11:35:49 +01:00
|
|
|
|
2015-12-23 15:14:00 +01:00
|
|
|
def build(self):
|
2015-12-22 14:42:13 +01:00
|
|
|
glyphClasses = {}
|
2015-12-23 11:35:49 +01:00
|
|
|
for classID, glyphs in enumerate(self.classes()):
|
2015-12-23 15:14:00 +01:00
|
|
|
if classID == 0:
|
2015-12-23 11:35:49 +01:00
|
|
|
continue
|
|
|
|
for glyph in glyphs:
|
|
|
|
glyphClasses[glyph] = classID
|
|
|
|
classDef = self.otClass_()
|
2015-12-22 14:42:13 +01:00
|
|
|
classDef.classDefs = glyphClasses
|
|
|
|
return classDef
|