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-09-04 15:06:11 +02:00
|
|
|
from fontTools.ttLib.tables import otTables
|
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-09-07 11:14:03 +02:00
|
|
|
self.lookup_flag_ = 0
|
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*]
|
|
|
|
self.required_features_ = {} # ('latn', 'DEU ') --> 'scmp'
|
2015-09-04 15:06:11 +02:00
|
|
|
|
|
|
|
def build(self):
|
|
|
|
parsetree = Parser(self.featurefile_path).parse()
|
|
|
|
parsetree.build(self)
|
2015-09-09 16:57:29 +02:00
|
|
|
for tag in ('GPOS', 'GSUB'):
|
|
|
|
fontTable = self.font[tag] = getTableClass(tag)()
|
|
|
|
fontTable.table = self.makeTable(tag)
|
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
|
|
|
|
type(self.cur_lookup_) == builder_class and
|
|
|
|
self.cur_lookup_.lookup_flag == self.lookup_flag_):
|
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-04 11:04:37 +01:00
|
|
|
self.cur_lookup_ = builder_class(
|
|
|
|
self.font, location, self.lookup_flag_)
|
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.
|
2015-09-07 17:22:37 +02:00
|
|
|
for script, lang in self.language_systems:
|
|
|
|
key = (script, lang, self.cur_feature_name_)
|
|
|
|
self.features_.setdefault(key, []).append(self.cur_lookup_)
|
2015-09-07 11:14:03 +02:00
|
|
|
return self.cur_lookup_
|
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
|
|
|
|
|
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
|
|
|
|
|
|
|
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-09-07 11:14:03 +02:00
|
|
|
self.lookup_flag_ = 0
|
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
|
|
|
|
|
|
|
|
def add_substitution(self, location, old_prefix, old, old_suffix, new,
|
|
|
|
lookups):
|
|
|
|
assert len(new) == 0, new
|
2015-11-30 15:02:09 +01:00
|
|
|
lookup = self.get_lookup_(location, ChainContextSubstBuilder)
|
|
|
|
lookup.substitutions.append((old_prefix, old, old_suffix,
|
2015-12-03 13:05:42 +01:00
|
|
|
self.find_lookup_builders_(lookups)))
|
2015-11-30 15:02:09 +01:00
|
|
|
|
2015-09-07 11:14:03 +02:00
|
|
|
def add_alternate_substitution(self, location, glyph, from_class):
|
|
|
|
lookup = self.get_lookup_(location, AlternateSubstBuilder)
|
|
|
|
if glyph in lookup.alternates:
|
|
|
|
raise FeatureLibError(
|
|
|
|
'Already defined alternates for glyph "%s"' % glyph,
|
|
|
|
location)
|
|
|
|
lookup.alternates[glyph] = from_class
|
|
|
|
|
2015-09-07 16:10:13 +02:00
|
|
|
def add_ligature_substitution(self, location, glyphs, replacement):
|
|
|
|
lookup = self.get_lookup_(location, LigatureSubstBuilder)
|
|
|
|
lookup.ligatures[glyphs] = replacement
|
|
|
|
|
2015-09-08 12:05:44 +02:00
|
|
|
def add_multiple_substitution(self, location, glyph, replacements):
|
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-03 13:05:42 +01:00
|
|
|
def add_reverse_chaining_single_substitution(self, location, old_prefix,
|
|
|
|
old_suffix, mapping):
|
|
|
|
lookup = self.get_lookup_(location, ReverseChainSingleSubstBuilder)
|
|
|
|
lookup.substitutions.append((old_prefix, old_suffix, mapping))
|
|
|
|
|
2015-09-08 10:33:07 +02:00
|
|
|
def add_single_substitution(self, location, mapping):
|
|
|
|
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-09-07 11:14:03 +02:00
|
|
|
|
|
|
|
class LookupBuilder(object):
|
2015-12-04 11:04:37 +01:00
|
|
|
def __init__(self, font, location, table, lookup_type, lookup_flag):
|
|
|
|
self.font = font
|
2015-09-07 11:14:03 +02:00
|
|
|
self.location = location
|
|
|
|
self.table, self.lookup_type = table, lookup_type
|
|
|
|
self.lookup_flag = lookup_flag
|
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
|
|
|
|
self.lookup_flag == other.lookup_flag)
|
|
|
|
|
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):
|
|
|
|
coverage = otTables.BacktrackCoverage()
|
2015-12-04 11:04:37 +01:00
|
|
|
coverage.glyphs = sorted(list(p), key=self.font.getGlyphID)
|
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:
|
|
|
|
coverage = otTables.LookAheadCoverage()
|
2015-12-04 11:04:37 +01:00
|
|
|
coverage.glyphs = sorted(list(s), key=self.font.getGlyphID)
|
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:
|
|
|
|
coverage = otTables.InputCoverage()
|
2015-12-04 11:04:37 +01:00
|
|
|
coverage.glyphs = sorted(list(g), key=self.font.getGlyphID)
|
2015-12-03 13:05:42 +01:00
|
|
|
subtable.InputCoverage.append(coverage)
|
|
|
|
|
2015-09-07 11:14:03 +02:00
|
|
|
|
|
|
|
class AlternateSubstBuilder(LookupBuilder):
|
2015-12-04 11:04:37 +01:00
|
|
|
def __init__(self, font, location, lookup_flag):
|
|
|
|
LookupBuilder.__init__(self, font, location, 'GSUB', 3, lookup_flag)
|
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-09-09 18:13:28 +01:00
|
|
|
lookup = otTables.Lookup()
|
|
|
|
lookup.SubTable = []
|
|
|
|
st = otTables.AlternateSubst()
|
|
|
|
st.Format = 1
|
|
|
|
st.alternates = self.alternates
|
|
|
|
lookup.SubTable.append(st)
|
|
|
|
lookup.LookupFlag = self.lookup_flag
|
2015-11-30 15:02:09 +01:00
|
|
|
lookup.LookupType = self.lookup_type
|
|
|
|
lookup.SubTableCount = len(lookup.SubTable)
|
|
|
|
return lookup
|
|
|
|
|
|
|
|
|
|
|
|
class ChainContextSubstBuilder(LookupBuilder):
|
2015-12-04 11:04:37 +01:00
|
|
|
def __init__(self, font, location, lookup_flag):
|
|
|
|
LookupBuilder.__init__(self, font, location, 'GSUB', 6, lookup_flag)
|
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):
|
|
|
|
lookup = otTables.Lookup()
|
|
|
|
lookup.SubTable = []
|
|
|
|
for (prefix, input, suffix, lookups) in self.substitutions:
|
|
|
|
st = otTables.ChainContextSubst()
|
|
|
|
lookup.SubTable.append(st)
|
|
|
|
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)
|
|
|
|
|
|
|
|
lookup.LookupFlag = self.lookup_flag
|
2015-09-09 18:13:28 +01:00
|
|
|
lookup.LookupType = self.lookup_type
|
|
|
|
lookup.SubTableCount = len(lookup.SubTable)
|
2015-09-07 11:14:03 +02:00
|
|
|
return lookup
|
2015-09-07 16:10:13 +02:00
|
|
|
|
|
|
|
|
|
|
|
class LigatureSubstBuilder(LookupBuilder):
|
2015-12-04 11:04:37 +01:00
|
|
|
def __init__(self, font, location, lookup_flag):
|
|
|
|
LookupBuilder.__init__(self, font, location, 'GSUB', 4, lookup_flag)
|
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-09-09 18:13:28 +01:00
|
|
|
lookup = otTables.Lookup()
|
|
|
|
lookup.SubTable = []
|
|
|
|
st = otTables.LigatureSubst()
|
|
|
|
st.Format = 1
|
|
|
|
st.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-09-09 18:13:28 +01:00
|
|
|
st.ligatures.setdefault(components[0], []).append(lig)
|
|
|
|
lookup.SubTable.append(st)
|
|
|
|
lookup.LookupFlag = self.lookup_flag
|
|
|
|
lookup.LookupType = self.lookup_type
|
|
|
|
lookup.SubTableCount = len(lookup.SubTable)
|
2015-09-07 16:10:13 +02:00
|
|
|
return lookup
|
2015-09-08 10:33:07 +02:00
|
|
|
|
|
|
|
|
2015-09-10 15:28:02 +02:00
|
|
|
class MultipleSubstBuilder(LookupBuilder):
|
2015-12-04 11:04:37 +01:00
|
|
|
def __init__(self, font, location, lookup_flag):
|
|
|
|
LookupBuilder.__init__(self, font, location, 'GSUB', 2, lookup_flag)
|
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):
|
|
|
|
lookup = otTables.Lookup()
|
|
|
|
lookup.SubTable = []
|
|
|
|
st = otTables.MultipleSubst()
|
|
|
|
st.mapping = self.mapping
|
|
|
|
lookup.SubTable.append(st)
|
|
|
|
lookup.LookupFlag = self.lookup_flag
|
|
|
|
lookup.LookupType = self.lookup_type
|
|
|
|
lookup.SubTableCount = len(lookup.SubTable)
|
|
|
|
return lookup
|
|
|
|
|
|
|
|
|
2015-12-03 13:05:42 +01:00
|
|
|
class ReverseChainSingleSubstBuilder(LookupBuilder):
|
2015-12-04 11:04:37 +01:00
|
|
|
def __init__(self, font, location, lookup_flag):
|
|
|
|
LookupBuilder.__init__(self, font, location, 'GSUB', 8, lookup_flag)
|
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):
|
|
|
|
lookup = otTables.Lookup()
|
|
|
|
lookup.SubTable = []
|
|
|
|
for prefix, suffix, mapping in self.substitutions:
|
|
|
|
st = otTables.ReverseChainSingleSubst()
|
|
|
|
st.Format = 1
|
|
|
|
lookup.SubTable.append(st)
|
|
|
|
self.setBacktrackCoverage_(prefix, st)
|
|
|
|
self.setLookAheadCoverage_(suffix, st)
|
2015-12-04 11:04:37 +01:00
|
|
|
coverage = sorted(mapping.keys(), key=self.font.getGlyphID)
|
2015-12-03 13:05:42 +01:00
|
|
|
st.Coverage = otTables.Coverage()
|
|
|
|
st.Coverage.glyphs = coverage
|
|
|
|
st.GlyphCount = len(coverage)
|
|
|
|
st.Substitute = [mapping[g] for g in coverage]
|
|
|
|
|
|
|
|
lookup.LookupFlag = self.lookup_flag
|
|
|
|
lookup.LookupType = self.lookup_type
|
|
|
|
lookup.SubTableCount = len(lookup.SubTable)
|
|
|
|
return lookup
|
|
|
|
|
|
|
|
|
2015-09-08 10:33:07 +02:00
|
|
|
class SingleSubstBuilder(LookupBuilder):
|
2015-12-04 11:04:37 +01:00
|
|
|
def __init__(self, font, location, lookup_flag):
|
|
|
|
LookupBuilder.__init__(self, font, location, 'GSUB', 1, lookup_flag)
|
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-09-09 18:13:28 +01:00
|
|
|
lookup = otTables.Lookup()
|
|
|
|
lookup.SubTable = []
|
|
|
|
st = otTables.SingleSubst()
|
|
|
|
st.mapping = self.mapping
|
|
|
|
lookup.SubTable.append(st)
|
|
|
|
lookup.LookupFlag = self.lookup_flag
|
|
|
|
lookup.LookupType = self.lookup_type
|
|
|
|
lookup.SubTableCount = len(lookup.SubTable)
|
2015-09-08 10:33:07 +02:00
|
|
|
return lookup
|