2016-03-28 12:16:19 -07:00
|
|
|
from fontTools.misc.py23 import *
|
2016-03-19 22:05:38 +04:00
|
|
|
from fontTools.misc import sstruct
|
|
|
|
from fontTools.misc.textTools import binary2num, safeEval
|
2015-09-04 15:06:11 +02:00
|
|
|
from fontTools.feaLib.error import FeatureLibError
|
|
|
|
from fontTools.feaLib.parser import Parser
|
2018-01-25 09:53:42 -08:00
|
|
|
from fontTools.feaLib.ast import FeatureFile
|
2016-01-14 17:15:52 +01:00
|
|
|
from fontTools.otlLib import builder as otl
|
2019-04-23 16:52:15 -07:00
|
|
|
from fontTools.otlLib.maxContextCalc import maxCtxFont
|
2016-04-23 02:00:24 +02:00
|
|
|
from fontTools.ttLib import newTable, getTableModule
|
2015-12-04 11:16:43 +01:00
|
|
|
from fontTools.ttLib.tables import otBase, otTables
|
2020-07-02 14:09:10 +01:00
|
|
|
from fontTools.otlLib.builder import (
|
|
|
|
AlternateSubstBuilder,
|
|
|
|
ChainContextPosBuilder,
|
|
|
|
ChainContextSubstBuilder,
|
|
|
|
LigatureSubstBuilder,
|
|
|
|
MultipleSubstBuilder,
|
|
|
|
CursivePosBuilder,
|
|
|
|
MarkBasePosBuilder,
|
|
|
|
MarkLigPosBuilder,
|
|
|
|
MarkMarkPosBuilder,
|
|
|
|
ReverseChainSingleSubstBuilder,
|
|
|
|
SingleSubstBuilder,
|
|
|
|
ClassPairPosSubtableBuilder,
|
|
|
|
PairPosBuilder,
|
|
|
|
SinglePosBuilder,
|
2020-07-15 17:07:19 +01:00
|
|
|
ChainContextualRule,
|
2020-07-02 14:09:10 +01:00
|
|
|
)
|
|
|
|
from fontTools.otlLib.error import OpenTypeLibError
|
|
|
|
from collections import defaultdict
|
2016-01-07 10:31:13 +01:00
|
|
|
import itertools
|
2018-01-15 18:43:10 +00:00
|
|
|
import logging
|
|
|
|
|
|
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
2015-09-04 15:06:11 +02:00
|
|
|
|
|
|
|
|
2018-01-24 12:56:24 -08:00
|
|
|
def addOpenTypeFeatures(font, featurefile, tables=None):
|
2020-05-12 23:11:17 +01:00
|
|
|
"""Add features from a file to a font. Note that this replaces any features
|
|
|
|
currently present.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
font (feaLib.ttLib.TTFont): The font object.
|
|
|
|
featurefile: Either a path or file object (in which case we
|
|
|
|
parse it into an AST), or a pre-parsed AST instance.
|
|
|
|
tables: If passed, restrict the set of affected tables to those in the
|
|
|
|
list.
|
|
|
|
|
|
|
|
"""
|
2016-03-21 18:46:50 +00:00
|
|
|
builder = Builder(font, featurefile)
|
2018-01-24 12:56:24 -08:00
|
|
|
builder.build(tables=tables)
|
2015-09-04 15:06:11 +02:00
|
|
|
|
|
|
|
|
2018-01-24 12:56:24 -08:00
|
|
|
def addOpenTypeFeaturesFromString(font, features, filename=None, tables=None):
|
2020-05-12 23:11:17 +01:00
|
|
|
"""Add features from a string to a font. Note that this replaces any
|
|
|
|
features currently present.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
font (feaLib.ttLib.TTFont): The font object.
|
|
|
|
features: A string containing feature code.
|
|
|
|
filename: The directory containing ``filename`` is used as the root of
|
|
|
|
relative ``include()`` paths; if ``None`` is provided, the current
|
|
|
|
directory is assumed.
|
|
|
|
tables: If passed, restrict the set of affected tables to those in the
|
|
|
|
list.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
2016-03-21 19:39:07 +00:00
|
|
|
featurefile = UnicodeIO(tounicode(features))
|
2016-03-21 18:46:50 +00:00
|
|
|
if filename:
|
|
|
|
featurefile.name = filename
|
2018-01-24 12:56:24 -08:00
|
|
|
addOpenTypeFeatures(font, featurefile, tables=tables)
|
2016-03-21 18:46:50 +00:00
|
|
|
|
|
|
|
|
2015-09-04 15:06:11 +02:00
|
|
|
class Builder(object):
|
2018-01-24 12:56:24 -08:00
|
|
|
|
2020-07-15 14:14:01 +01:00
|
|
|
supportedTables = frozenset(
|
|
|
|
Tag(tag)
|
|
|
|
for tag in [
|
|
|
|
"BASE",
|
|
|
|
"GDEF",
|
|
|
|
"GPOS",
|
|
|
|
"GSUB",
|
|
|
|
"OS/2",
|
|
|
|
"head",
|
|
|
|
"hhea",
|
|
|
|
"name",
|
|
|
|
"vhea",
|
|
|
|
]
|
|
|
|
)
|
2018-01-24 12:56:24 -08:00
|
|
|
|
2016-03-21 18:46:50 +00:00
|
|
|
def __init__(self, font, featurefile):
|
2015-09-04 15:06:11 +02:00
|
|
|
self.font = font
|
2018-01-25 09:53:42 -08:00
|
|
|
# 'featurefile' can be either a path or file object (in which case we
|
|
|
|
# parse it into an AST), or a pre-parsed AST instance
|
|
|
|
if isinstance(featurefile, FeatureFile):
|
|
|
|
self.parseTree, self.file = featurefile, None
|
|
|
|
else:
|
|
|
|
self.parseTree, self.file = None, featurefile
|
2016-01-14 17:10:45 +01:00
|
|
|
self.glyphMap = font.getReverseGlyphMap()
|
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()
|
2018-07-24 16:17:23 +01:00
|
|
|
self.seen_non_DFLT_script_ = False
|
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-11 16:00:52 +01:00
|
|
|
self.required_features_ = {} # ('latn', 'DEU ') --> 'scmp'
|
|
|
|
# for feature 'aalt'
|
|
|
|
self.aalt_features_ = [] # [(location, featureName)*], for 'aalt'
|
|
|
|
self.aalt_location_ = None
|
2016-02-03 17:38:38 +01:00
|
|
|
self.aalt_alternates_ = {}
|
2016-03-18 09:55:51 +04:00
|
|
|
# for 'featureNames'
|
2018-02-05 22:45:56 -08:00
|
|
|
self.featureNames_ = set()
|
2016-03-18 09:55:51 +04:00
|
|
|
self.featureNames_ids_ = {}
|
2018-02-05 23:31:30 -08:00
|
|
|
# for 'cvParameters'
|
|
|
|
self.cv_parameters_ = set()
|
|
|
|
self.cv_parameters_ids_ = {}
|
|
|
|
self.cv_num_named_params_ = {}
|
|
|
|
self.cv_characters_ = defaultdict(list)
|
2016-03-18 13:18:31 +04:00
|
|
|
# for feature 'size'
|
|
|
|
self.size_parameters_ = None
|
2016-01-11 18:01:47 +01:00
|
|
|
# for table 'head'
|
|
|
|
self.fontRevision_ = None # 2.71
|
2016-03-14 23:44:25 +04:00
|
|
|
# for table 'name'
|
|
|
|
self.names_ = []
|
2016-03-19 02:47:04 +04:00
|
|
|
# for table 'BASE'
|
|
|
|
self.base_horiz_axis_ = None
|
|
|
|
self.base_vert_axis_ = None
|
2016-01-11 16:00:52 +01:00
|
|
|
# for table 'GDEF'
|
2016-01-08 08:32:47 +01:00
|
|
|
self.attachPoints_ = {} # "a" --> {3, 7}
|
2016-01-20 09:49:09 +01:00
|
|
|
self.ligCaretCoords_ = {} # "f_f_i" --> {300, 600}
|
|
|
|
self.ligCaretPoints_ = {} # "f_f_i" --> {3, 7}
|
2016-01-08 19:06:52 +01:00
|
|
|
self.glyphClassDefs_ = {} # "fi" --> (2, (file, line, column))
|
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
|
2016-03-19 22:05:38 +04:00
|
|
|
# for table 'OS/2'
|
|
|
|
self.os2_ = {}
|
2016-04-09 17:42:38 +02:00
|
|
|
# for table 'hhea'
|
|
|
|
self.hhea_ = {}
|
2016-10-17 09:00:45 +01:00
|
|
|
# for table 'vhea'
|
|
|
|
self.vhea_ = {}
|
2015-09-04 15:06:11 +02:00
|
|
|
|
2018-01-24 12:56:24 -08:00
|
|
|
def build(self, tables=None):
|
2018-01-25 09:53:42 -08:00
|
|
|
if self.parseTree is None:
|
|
|
|
self.parseTree = Parser(self.file, self.glyphMap).parse()
|
2015-12-08 17:04:21 +01:00
|
|
|
self.parseTree.build(self)
|
2018-01-24 12:56:24 -08:00
|
|
|
# by default, build all the supported tables
|
|
|
|
if tables is None:
|
|
|
|
tables = self.supportedTables
|
|
|
|
else:
|
|
|
|
tables = frozenset(tables)
|
|
|
|
unsupported = tables - self.supportedTables
|
2020-05-29 16:44:19 +01:00
|
|
|
if unsupported:
|
|
|
|
unsupported_string = ", ".join(sorted(unsupported))
|
|
|
|
raise NotImplementedError(
|
|
|
|
"The following tables were requested but are unsupported: "
|
|
|
|
f"{unsupported_string}."
|
|
|
|
)
|
2018-01-24 12:56:24 -08:00
|
|
|
if "GSUB" in tables:
|
|
|
|
self.build_feature_aalt_()
|
|
|
|
if "head" in tables:
|
|
|
|
self.build_head()
|
|
|
|
if "hhea" in tables:
|
|
|
|
self.build_hhea()
|
|
|
|
if "vhea" in tables:
|
|
|
|
self.build_vhea()
|
|
|
|
if "name" in tables:
|
|
|
|
self.build_name()
|
|
|
|
if "OS/2" in tables:
|
|
|
|
self.build_OS_2()
|
2020-07-15 14:14:01 +01:00
|
|
|
for tag in ("GPOS", "GSUB"):
|
2018-01-24 12:56:24 -08:00
|
|
|
if tag not in tables:
|
|
|
|
continue
|
2015-12-07 21:37:42 +01:00
|
|
|
table = self.makeTable(tag)
|
2020-07-15 14:14:01 +01:00
|
|
|
if (
|
|
|
|
table.ScriptList.ScriptCount > 0
|
|
|
|
or table.FeatureList.FeatureCount > 0
|
|
|
|
or table.LookupList.LookupCount > 0
|
|
|
|
):
|
2016-04-23 02:00:24 +02:00
|
|
|
fontTable = self.font[tag] = newTable(tag)
|
2015-12-07 21:37:42 +01:00
|
|
|
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]
|
2020-07-15 14:14:01 +01:00
|
|
|
if any(tag in self.font for tag in ("GPOS", "GSUB")) and "OS/2" in self.font:
|
2019-04-23 16:52:15 -07:00
|
|
|
self.font["OS/2"].usMaxContext = maxCtxFont(self.font)
|
2018-01-24 12:56:24 -08:00
|
|
|
if "GDEF" in tables:
|
|
|
|
gdef = self.buildGDEF()
|
|
|
|
if gdef:
|
|
|
|
self.font["GDEF"] = gdef
|
|
|
|
elif "GDEF" in self.font:
|
|
|
|
del self.font["GDEF"]
|
|
|
|
if "BASE" in tables:
|
|
|
|
base = self.buildBASE()
|
|
|
|
if base:
|
|
|
|
self.font["BASE"] = base
|
|
|
|
elif "BASE" in self.font:
|
|
|
|
del self.font["BASE"]
|
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):
|
2020-07-15 14:14:01 +01:00
|
|
|
if (
|
|
|
|
self.cur_lookup_
|
|
|
|
and 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 "
|
2020-07-15 14:14:01 +01:00
|
|
|
"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.
|
2020-07-15 14:14:01 +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
|
|
|
|
2016-01-11 16:00:52 +01:00
|
|
|
def build_feature_aalt_(self):
|
2016-02-03 17:38:38 +01:00
|
|
|
if not self.aalt_features_ and not self.aalt_alternates_:
|
2016-01-11 16:00:52 +01:00
|
|
|
return
|
2016-02-03 17:38:38 +01:00
|
|
|
alternates = {g: set(a) for g, a in self.aalt_alternates_.items()}
|
2016-01-11 16:00:52 +01:00
|
|
|
for location, name in self.aalt_features_ + [(None, "aalt")]:
|
2020-07-15 14:14:01 +01:00
|
|
|
feature = [
|
|
|
|
(script, lang, feature, lookups)
|
|
|
|
for (script, lang, feature), lookups in self.features_.items()
|
|
|
|
if feature == name
|
|
|
|
]
|
2016-01-11 18:12:23 +01:00
|
|
|
# "aalt" does not have to specify its own lookups, but it might.
|
|
|
|
if not feature and name != "aalt":
|
2020-07-15 14:14:01 +01:00
|
|
|
raise FeatureLibError(
|
|
|
|
"Feature %s has not been defined" % name, location
|
|
|
|
)
|
2016-01-11 16:00:52 +01:00
|
|
|
for script, lang, feature, lookups in feature:
|
2020-05-12 06:28:25 +01:00
|
|
|
for lookuplist in lookups:
|
|
|
|
if not isinstance(lookuplist, list):
|
|
|
|
lookuplist = [lookuplist]
|
|
|
|
for lookup in lookuplist:
|
|
|
|
for glyph, alts in lookup.getAlternateGlyphs().items():
|
|
|
|
alternates.setdefault(glyph, set()).update(alts)
|
2020-07-15 14:14:01 +01:00
|
|
|
single = {
|
|
|
|
glyph: list(repl)[0] for glyph, repl in alternates.items() if len(repl) == 1
|
|
|
|
}
|
2017-02-11 16:48:27 +01:00
|
|
|
# TODO: Figure out the glyph alternate ordering used by makeotf.
|
|
|
|
# https://github.com/fonttools/fonttools/issues/836
|
2020-07-15 14:14:01 +01:00
|
|
|
multi = {
|
|
|
|
glyph: sorted(repl, key=self.font.getGlyphID)
|
|
|
|
for glyph, repl in alternates.items()
|
|
|
|
if len(repl) > 1
|
|
|
|
}
|
2016-01-11 16:00:52 +01:00
|
|
|
if not single and not multi:
|
|
|
|
return
|
2020-07-15 14:14:01 +01:00
|
|
|
self.features_ = {
|
|
|
|
(script, lang, feature): lookups
|
|
|
|
for (script, lang, feature), lookups in self.features_.items()
|
|
|
|
if feature != "aalt"
|
|
|
|
}
|
2016-01-11 16:00:52 +01:00
|
|
|
old_lookups = self.lookups_
|
|
|
|
self.lookups_ = []
|
|
|
|
self.start_feature(self.aalt_location_, "aalt")
|
|
|
|
if single:
|
2016-02-03 17:38:38 +01:00
|
|
|
single_lookup = self.get_lookup_(location, SingleSubstBuilder)
|
|
|
|
single_lookup.mapping = single
|
|
|
|
if multi:
|
|
|
|
multi_lookup = self.get_lookup_(location, AlternateSubstBuilder)
|
|
|
|
multi_lookup.alternates = multi
|
2016-01-11 16:00:52 +01:00
|
|
|
self.end_feature()
|
|
|
|
self.lookups_.extend(old_lookups)
|
|
|
|
|
2016-01-11 18:01:47 +01:00
|
|
|
def build_head(self):
|
|
|
|
if not self.fontRevision_:
|
|
|
|
return
|
|
|
|
table = self.font.get("head")
|
|
|
|
if not table: # this only happens for unit tests
|
2016-04-23 02:00:24 +02:00
|
|
|
table = self.font["head"] = newTable("head")
|
2016-01-11 18:01:47 +01:00
|
|
|
table.decompile(b"\0" * 54, self.font)
|
|
|
|
table.tableVersion = 1.0
|
2016-01-11 19:36:19 +01:00
|
|
|
table.created = table.modified = 3406620153 # 2011-12-13 11:22:33
|
2016-01-11 18:01:47 +01:00
|
|
|
table.fontRevision = self.fontRevision_
|
|
|
|
|
2016-04-09 17:42:38 +02:00
|
|
|
def build_hhea(self):
|
|
|
|
if not self.hhea_:
|
|
|
|
return
|
|
|
|
table = self.font.get("hhea")
|
|
|
|
if not table: # this only happens for unit tests
|
2016-04-23 02:00:24 +02:00
|
|
|
table = self.font["hhea"] = newTable("hhea")
|
2016-04-09 17:42:38 +02:00
|
|
|
table.decompile(b"\0" * 36, self.font)
|
2016-10-17 08:59:43 +01:00
|
|
|
table.tableVersion = 0x00010000
|
2016-04-09 17:42:38 +02:00
|
|
|
if "caretoffset" in self.hhea_:
|
|
|
|
table.caretOffset = self.hhea_["caretoffset"]
|
|
|
|
if "ascender" in self.hhea_:
|
|
|
|
table.ascent = self.hhea_["ascender"]
|
|
|
|
if "descender" in self.hhea_:
|
|
|
|
table.descent = self.hhea_["descender"]
|
|
|
|
if "linegap" in self.hhea_:
|
|
|
|
table.lineGap = self.hhea_["linegap"]
|
|
|
|
|
2016-10-17 09:00:45 +01:00
|
|
|
def build_vhea(self):
|
|
|
|
if not self.vhea_:
|
|
|
|
return
|
|
|
|
table = self.font.get("vhea")
|
|
|
|
if not table: # this only happens for unit tests
|
|
|
|
table = self.font["vhea"] = newTable("vhea")
|
|
|
|
table.decompile(b"\0" * 36, self.font)
|
|
|
|
table.tableVersion = 0x00011000
|
|
|
|
if "verttypoascender" in self.vhea_:
|
|
|
|
table.ascent = self.vhea_["verttypoascender"]
|
|
|
|
if "verttypodescender" in self.vhea_:
|
|
|
|
table.descent = self.vhea_["verttypodescender"]
|
|
|
|
if "verttypolinegap" in self.vhea_:
|
|
|
|
table.lineGap = self.vhea_["verttypolinegap"]
|
|
|
|
|
2016-03-18 09:55:51 +04:00
|
|
|
def get_user_name_id(self, table):
|
|
|
|
# Try to find first unused font-specific name id
|
|
|
|
nameIDs = [name.nameID for name in table.names]
|
|
|
|
for user_name_id in range(256, 32767):
|
|
|
|
if user_name_id not in nameIDs:
|
|
|
|
return user_name_id
|
|
|
|
|
|
|
|
def buildFeatureParams(self, tag):
|
|
|
|
params = None
|
2016-03-18 13:18:31 +04:00
|
|
|
if tag == "size":
|
|
|
|
params = otTables.FeatureParamsSize()
|
2020-07-15 14:14:01 +01:00
|
|
|
(
|
|
|
|
params.DesignSize,
|
|
|
|
params.SubfamilyID,
|
|
|
|
params.RangeStart,
|
|
|
|
params.RangeEnd,
|
|
|
|
) = self.size_parameters_
|
2016-03-18 13:18:31 +04:00
|
|
|
if tag in self.featureNames_ids_:
|
|
|
|
params.SubfamilyNameID = self.featureNames_ids_[tag]
|
|
|
|
else:
|
|
|
|
params.SubfamilyNameID = 0
|
|
|
|
elif tag in self.featureNames_:
|
2018-07-26 10:51:37 +01:00
|
|
|
if not self.featureNames_ids_:
|
|
|
|
# name table wasn't selected among the tables to build; skip
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
assert tag in self.featureNames_ids_
|
|
|
|
params = otTables.FeatureParamsStylisticSet()
|
|
|
|
params.Version = 0
|
|
|
|
params.UINameID = self.featureNames_ids_[tag]
|
2018-02-05 23:31:30 -08:00
|
|
|
elif tag in self.cv_parameters_:
|
|
|
|
params = otTables.FeatureParamsCharacterVariants()
|
|
|
|
params.Format = 0
|
|
|
|
params.FeatUILabelNameID = self.cv_parameters_ids_.get(
|
2020-07-15 14:14:01 +01:00
|
|
|
(tag, "FeatUILabelNameID"), 0
|
|
|
|
)
|
2018-02-05 23:31:30 -08:00
|
|
|
params.FeatUITooltipTextNameID = self.cv_parameters_ids_.get(
|
2020-07-15 14:14:01 +01:00
|
|
|
(tag, "FeatUITooltipTextNameID"), 0
|
|
|
|
)
|
2018-02-05 23:31:30 -08:00
|
|
|
params.SampleTextNameID = self.cv_parameters_ids_.get(
|
2020-07-15 14:14:01 +01:00
|
|
|
(tag, "SampleTextNameID"), 0
|
|
|
|
)
|
2018-02-05 23:31:30 -08:00
|
|
|
params.NumNamedParameters = self.cv_num_named_params_.get(tag, 0)
|
|
|
|
params.FirstParamUILabelNameID = self.cv_parameters_ids_.get(
|
2020-07-15 14:14:01 +01:00
|
|
|
(tag, "ParamUILabelNameID_0"), 0
|
|
|
|
)
|
2018-02-05 23:31:30 -08:00
|
|
|
params.CharCount = len(self.cv_characters_[tag])
|
|
|
|
params.Character = self.cv_characters_[tag]
|
2016-03-18 09:55:51 +04:00
|
|
|
return params
|
|
|
|
|
2016-03-14 23:44:25 +04:00
|
|
|
def build_name(self):
|
|
|
|
if not self.names_:
|
|
|
|
return
|
|
|
|
table = self.font.get("name")
|
|
|
|
if not table: # this only happens for unit tests
|
2016-04-23 02:00:24 +02:00
|
|
|
table = self.font["name"] = newTable("name")
|
2016-03-18 09:55:51 +04:00
|
|
|
table.names = []
|
2016-03-14 23:44:25 +04:00
|
|
|
for name in self.names_:
|
|
|
|
nameID, platformID, platEncID, langID, string = name
|
2018-02-05 23:31:30 -08:00
|
|
|
# For featureNames block, nameID is 'feature tag'
|
2018-02-28 22:57:06 -08:00
|
|
|
# For cvParameters blocks, nameID is ('feature tag', 'block name')
|
2016-03-18 09:55:51 +04:00
|
|
|
if not isinstance(nameID, int):
|
|
|
|
tag = nameID
|
2018-02-05 23:31:30 -08:00
|
|
|
if tag in self.featureNames_:
|
|
|
|
if tag not in self.featureNames_ids_:
|
|
|
|
self.featureNames_ids_[tag] = self.get_user_name_id(table)
|
|
|
|
assert self.featureNames_ids_[tag] is not None
|
|
|
|
nameID = self.featureNames_ids_[tag]
|
2018-02-28 22:57:06 -08:00
|
|
|
elif tag[0] in self.cv_parameters_:
|
2018-02-05 23:31:30 -08:00
|
|
|
if tag not in self.cv_parameters_ids_:
|
|
|
|
self.cv_parameters_ids_[tag] = self.get_user_name_id(table)
|
|
|
|
assert self.cv_parameters_ids_[tag] is not None
|
|
|
|
nameID = self.cv_parameters_ids_[tag]
|
2016-03-14 23:44:25 +04:00
|
|
|
table.setName(string, nameID, platformID, platEncID, langID)
|
|
|
|
|
2016-03-19 22:05:38 +04:00
|
|
|
def build_OS_2(self):
|
|
|
|
if not self.os2_:
|
|
|
|
return
|
|
|
|
table = self.font.get("OS/2")
|
|
|
|
if not table: # this only happens for unit tests
|
2016-04-23 02:00:24 +02:00
|
|
|
table = self.font["OS/2"] = newTable("OS/2")
|
2016-03-19 22:05:38 +04:00
|
|
|
data = b"\0" * sstruct.calcsize(getTableModule("OS/2").OS2_format_0)
|
|
|
|
table.decompile(data, self.font)
|
|
|
|
version = 0
|
|
|
|
if "fstype" in self.os2_:
|
|
|
|
table.fsType = self.os2_["fstype"]
|
|
|
|
if "panose" in self.os2_:
|
|
|
|
panose = getTableModule("OS/2").Panose()
|
2020-07-15 14:14:01 +01:00
|
|
|
(
|
|
|
|
panose.bFamilyType,
|
|
|
|
panose.bSerifStyle,
|
|
|
|
panose.bWeight,
|
|
|
|
panose.bProportion,
|
|
|
|
panose.bContrast,
|
|
|
|
panose.bStrokeVariation,
|
|
|
|
panose.bArmStyle,
|
|
|
|
panose.bLetterForm,
|
|
|
|
panose.bMidline,
|
|
|
|
panose.bXHeight,
|
|
|
|
) = self.os2_["panose"]
|
2016-03-19 22:05:38 +04:00
|
|
|
table.panose = panose
|
|
|
|
if "typoascender" in self.os2_:
|
|
|
|
table.sTypoAscender = self.os2_["typoascender"]
|
|
|
|
if "typodescender" in self.os2_:
|
|
|
|
table.sTypoDescender = self.os2_["typodescender"]
|
|
|
|
if "typolinegap" in self.os2_:
|
|
|
|
table.sTypoLineGap = self.os2_["typolinegap"]
|
|
|
|
if "winascent" in self.os2_:
|
|
|
|
table.usWinAscent = self.os2_["winascent"]
|
|
|
|
if "windescent" in self.os2_:
|
|
|
|
table.usWinDescent = self.os2_["windescent"]
|
|
|
|
if "vendor" in self.os2_:
|
|
|
|
table.achVendID = safeEval("'''" + self.os2_["vendor"] + "'''")
|
|
|
|
if "weightclass" in self.os2_:
|
|
|
|
table.usWeightClass = self.os2_["weightclass"]
|
|
|
|
if "widthclass" in self.os2_:
|
|
|
|
table.usWidthClass = self.os2_["widthclass"]
|
|
|
|
if "unicoderange" in self.os2_:
|
|
|
|
table.setUnicodeRanges(self.os2_["unicoderange"])
|
|
|
|
if "codepagerange" in self.os2_:
|
|
|
|
pages = self.build_codepages_(self.os2_["codepagerange"])
|
|
|
|
table.ulCodePageRange1, table.ulCodePageRange2 = pages
|
|
|
|
version = 1
|
|
|
|
if "xheight" in self.os2_:
|
|
|
|
table.sxHeight = self.os2_["xheight"]
|
|
|
|
version = 2
|
|
|
|
if "capheight" in self.os2_:
|
|
|
|
table.sCapHeight = self.os2_["capheight"]
|
|
|
|
version = 2
|
|
|
|
if "loweropsize" in self.os2_:
|
|
|
|
table.usLowerOpticalPointSize = self.os2_["loweropsize"]
|
|
|
|
version = 5
|
|
|
|
if "upperopsize" in self.os2_:
|
|
|
|
table.usUpperOpticalPointSize = self.os2_["upperopsize"]
|
|
|
|
version = 5
|
2020-07-15 14:14:01 +01:00
|
|
|
|
2016-03-19 22:05:38 +04:00
|
|
|
def checkattr(table, attrs):
|
|
|
|
for attr in attrs:
|
|
|
|
if not hasattr(table, attr):
|
|
|
|
setattr(table, attr, 0)
|
2020-07-15 14:14:01 +01:00
|
|
|
|
2016-03-19 22:05:38 +04:00
|
|
|
table.version = max(version, table.version)
|
|
|
|
# this only happens for unit tests
|
|
|
|
if version >= 1:
|
|
|
|
checkattr(table, ("ulCodePageRange1", "ulCodePageRange2"))
|
|
|
|
if version >= 2:
|
2020-07-15 14:14:01 +01:00
|
|
|
checkattr(
|
|
|
|
table,
|
|
|
|
(
|
|
|
|
"sxHeight",
|
|
|
|
"sCapHeight",
|
|
|
|
"usDefaultChar",
|
|
|
|
"usBreakChar",
|
|
|
|
"usMaxContext",
|
|
|
|
),
|
|
|
|
)
|
2016-03-19 22:05:38 +04:00
|
|
|
if version >= 5:
|
2020-07-15 14:14:01 +01:00
|
|
|
checkattr(table, ("usLowerOpticalPointSize", "usUpperOpticalPointSize"))
|
2016-03-19 22:05:38 +04:00
|
|
|
|
|
|
|
def build_codepages_(self, pages):
|
|
|
|
pages2bits = {
|
2020-07-15 14:14:01 +01:00
|
|
|
1252: 0,
|
|
|
|
1250: 1,
|
|
|
|
1251: 2,
|
|
|
|
1253: 3,
|
|
|
|
1254: 4,
|
|
|
|
1255: 5,
|
|
|
|
1256: 6,
|
|
|
|
1257: 7,
|
|
|
|
1258: 8,
|
|
|
|
874: 16,
|
|
|
|
932: 17,
|
|
|
|
936: 18,
|
|
|
|
949: 19,
|
|
|
|
950: 20,
|
|
|
|
1361: 21,
|
|
|
|
869: 48,
|
|
|
|
866: 49,
|
|
|
|
865: 50,
|
|
|
|
864: 51,
|
|
|
|
863: 52,
|
|
|
|
862: 53,
|
|
|
|
861: 54,
|
|
|
|
860: 55,
|
|
|
|
857: 56,
|
|
|
|
855: 57,
|
|
|
|
852: 58,
|
|
|
|
775: 59,
|
|
|
|
737: 60,
|
|
|
|
708: 61,
|
|
|
|
850: 62,
|
|
|
|
437: 63,
|
2016-03-19 22:05:38 +04:00
|
|
|
}
|
|
|
|
bits = [pages2bits[p] for p in pages if p in pages2bits]
|
|
|
|
pages = []
|
|
|
|
for i in range(2):
|
|
|
|
pages.append("")
|
|
|
|
for j in range(i * 32, (i + 1) * 32):
|
|
|
|
if j in bits:
|
|
|
|
pages[i] += "1"
|
|
|
|
else:
|
|
|
|
pages[i] += "0"
|
|
|
|
return [binary2num(p[::-1]) for p in pages]
|
|
|
|
|
2016-03-19 02:47:04 +04:00
|
|
|
def buildBASE(self):
|
|
|
|
if not self.base_horiz_axis_ and not self.base_vert_axis_:
|
|
|
|
return None
|
|
|
|
base = otTables.BASE()
|
|
|
|
base.Version = 0x00010000
|
|
|
|
base.HorizAxis = self.buildBASEAxis(self.base_horiz_axis_)
|
|
|
|
base.VertAxis = self.buildBASEAxis(self.base_vert_axis_)
|
|
|
|
|
2016-04-23 02:00:24 +02:00
|
|
|
result = newTable("BASE")
|
2016-03-19 02:47:04 +04:00
|
|
|
result.table = base
|
|
|
|
return result
|
|
|
|
|
|
|
|
def buildBASEAxis(self, axis):
|
|
|
|
if not axis:
|
|
|
|
return
|
|
|
|
bases, scripts = axis
|
|
|
|
axis = otTables.Axis()
|
|
|
|
axis.BaseTagList = otTables.BaseTagList()
|
|
|
|
axis.BaseTagList.BaselineTag = bases
|
|
|
|
axis.BaseTagList.BaseTagCount = len(bases)
|
|
|
|
axis.BaseScriptList = otTables.BaseScriptList()
|
|
|
|
axis.BaseScriptList.BaseScriptRecord = []
|
|
|
|
axis.BaseScriptList.BaseScriptCount = len(scripts)
|
|
|
|
for script in sorted(scripts):
|
|
|
|
record = otTables.BaseScriptRecord()
|
|
|
|
record.BaseScriptTag = script[0]
|
|
|
|
record.BaseScript = otTables.BaseScript()
|
|
|
|
record.BaseScript.BaseLangSysCount = 0
|
|
|
|
record.BaseScript.BaseValues = otTables.BaseValues()
|
|
|
|
record.BaseScript.BaseValues.DefaultIndex = bases.index(script[1])
|
|
|
|
record.BaseScript.BaseValues.BaseCoord = []
|
|
|
|
record.BaseScript.BaseValues.BaseCoordCount = len(script[2])
|
|
|
|
for c in script[2]:
|
|
|
|
coord = otTables.BaseCoord()
|
|
|
|
coord.Format = 1
|
|
|
|
coord.Coordinate = c
|
|
|
|
record.BaseScript.BaseValues.BaseCoord.append(coord)
|
|
|
|
axis.BaseScriptList.BaseScriptRecord.append(record)
|
|
|
|
return axis
|
|
|
|
|
2016-01-08 17:38:49 +01:00
|
|
|
def buildGDEF(self):
|
2015-12-08 17:04:21 +01:00
|
|
|
gdef = otTables.GDEF()
|
2016-01-08 17:38:49 +01:00
|
|
|
gdef.GlyphClassDef = self.buildGDEFGlyphClassDef_()
|
2020-07-15 14:14:01 +01:00
|
|
|
gdef.AttachList = otl.buildAttachList(self.attachPoints_, self.glyphMap)
|
|
|
|
gdef.LigCaretList = otl.buildLigCaretList(
|
|
|
|
self.ligCaretCoords_, self.ligCaretPoints_, self.glyphMap
|
|
|
|
)
|
2016-01-08 17:38:49 +01:00
|
|
|
gdef.MarkAttachClassDef = self.buildGDEFMarkAttachClassDef_()
|
|
|
|
gdef.MarkGlyphSetsDef = self.buildGDEFMarkGlyphSetsDef_()
|
2017-01-02 13:08:36 +01:00
|
|
|
gdef.Version = 0x00010002 if gdef.MarkGlyphSetsDef else 0x00010000
|
2020-07-15 14:14:01 +01:00
|
|
|
if any(
|
|
|
|
(
|
|
|
|
gdef.GlyphClassDef,
|
|
|
|
gdef.AttachList,
|
|
|
|
gdef.LigCaretList,
|
|
|
|
gdef.MarkAttachClassDef,
|
|
|
|
gdef.MarkGlyphSetsDef,
|
|
|
|
)
|
|
|
|
):
|
2016-04-23 02:00:24 +02:00
|
|
|
result = newTable("GDEF")
|
2016-01-08 17:38:49 +01:00
|
|
|
result.table = gdef
|
|
|
|
return result
|
|
|
|
else:
|
|
|
|
return None
|
|
|
|
|
|
|
|
def buildGDEFGlyphClassDef_(self):
|
2016-01-08 19:06:52 +01:00
|
|
|
if self.glyphClassDefs_:
|
|
|
|
classes = {g: c for (g, (c, _)) in self.glyphClassDefs_.items()}
|
|
|
|
else:
|
2017-12-05 13:06:05 +02:00
|
|
|
classes = {}
|
|
|
|
for lookup in self.lookups_:
|
|
|
|
classes.update(lookup.inferGlyphClasses())
|
|
|
|
for markClass in self.parseTree.markClasses.values():
|
|
|
|
for markClassDef in markClass.definitions:
|
|
|
|
for glyph in markClassDef.glyphSet():
|
|
|
|
classes[glyph] = 3
|
2016-01-08 19:06:52 +01:00
|
|
|
if classes:
|
2016-01-08 17:38:49 +01:00
|
|
|
result = otTables.GlyphClassDef()
|
2016-01-08 19:06:52 +01:00
|
|
|
result.classDefs = classes
|
2016-01-08 08:32:47 +01:00
|
|
|
return result
|
|
|
|
else:
|
2015-12-08 17:04:21 +01:00
|
|
|
return None
|
2016-01-08 08:32:47 +01:00
|
|
|
|
2016-01-08 17:38:49 +01:00
|
|
|
def buildGDEFMarkAttachClassDef_(self):
|
|
|
|
classDefs = {g: c for g, (c, _) in self.markAttach_.items()}
|
|
|
|
if not classDefs:
|
|
|
|
return None
|
|
|
|
result = otTables.MarkAttachClassDef()
|
|
|
|
result.classDefs = classDefs
|
|
|
|
return result
|
|
|
|
|
|
|
|
def buildGDEFMarkGlyphSetsDef_(self):
|
2017-02-25 19:04:38 +00:00
|
|
|
sets = []
|
2020-07-15 14:14:01 +01:00
|
|
|
for glyphs, id_ in sorted(
|
|
|
|
self.markFilterSets_.items(), key=lambda item: item[1]
|
|
|
|
):
|
2017-02-25 19:04:38 +00:00
|
|
|
sets.append(glyphs)
|
2016-01-20 11:28:33 +01:00
|
|
|
return otl.buildMarkGlyphSetsDef(sets, self.glyphMap)
|
2016-01-08 17:38:49 +01:00
|
|
|
|
2016-01-26 12:09:52 +01:00
|
|
|
def buildLookups_(self, tag):
|
2020-07-15 14:14:01 +01:00
|
|
|
assert tag in ("GPOS", "GSUB"), tag
|
2015-09-07 17:22:37 +02:00
|
|
|
for lookup in self.lookups_:
|
|
|
|
lookup.lookup_index = None
|
2016-01-26 12:09:52 +01:00
|
|
|
lookups = []
|
2017-05-30 10:28:40 +01:00
|
|
|
for lookup in self.lookups_:
|
2016-01-26 12:09:52 +01:00
|
|
|
if lookup.table != tag:
|
2015-09-07 11:14:03 +02:00
|
|
|
continue
|
2016-01-26 12:09:52 +01:00
|
|
|
lookup.lookup_index = len(lookups)
|
|
|
|
lookups.append(lookup)
|
2020-07-02 14:09:10 +01:00
|
|
|
try:
|
|
|
|
otLookups = [l.build() for l in lookups]
|
|
|
|
except OpenTypeLibError as e:
|
|
|
|
raise FeatureLibError(str(e), e.location) from e
|
|
|
|
return otLookups
|
2016-01-26 12:09:52 +01:00
|
|
|
|
|
|
|
def makeTable(self, tag):
|
|
|
|
table = getattr(otTables, tag, None)()
|
2016-09-29 20:43:50 +01:00
|
|
|
table.Version = 0x00010000
|
2016-01-26 12:09:52 +01:00
|
|
|
table.ScriptList = otTables.ScriptList()
|
|
|
|
table.ScriptList.ScriptRecord = []
|
|
|
|
table.FeatureList = otTables.FeatureList()
|
|
|
|
table.FeatureList.FeatureRecord = []
|
|
|
|
table.LookupList = otTables.LookupList()
|
|
|
|
table.LookupList.Lookup = self.buildLookups_(tag)
|
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
|
2016-04-12 13:53:25 +02:00
|
|
|
# Sort the feature table by feature tag:
|
2019-03-06 16:01:28 +01:00
|
|
|
# https://github.com/fonttools/fonttools/issues/568
|
2016-04-12 13:53:25 +02:00
|
|
|
sortFeatureTag = lambda f: (f[0][2], f[0][1], f[0][0], f[1])
|
|
|
|
for key, lookups in sorted(self.features_.items(), key=sortFeatureTag):
|
2015-09-07 17:22:37 +02:00
|
|
|
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.
|
2020-07-15 14:14:01 +01:00
|
|
|
lookup_indices = tuple(
|
|
|
|
[l.lookup_index for l in lookups if l.lookup_index is not None]
|
|
|
|
)
|
2016-03-18 13:18:31 +04:00
|
|
|
|
2020-07-15 14:14:01 +01:00
|
|
|
size_feature = tag == "GPOS" and feature_tag == "size"
|
2016-03-18 13:18:31 +04:00
|
|
|
if len(lookup_indices) == 0 and not size_feature:
|
2015-09-07 17:22:37 +02:00
|
|
|
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()
|
2020-07-15 14:14:01 +01:00
|
|
|
frec.Feature.FeatureParams = self.buildFeatureParams(feature_tag)
|
2018-01-25 12:35:02 -08:00
|
|
|
frec.Feature.LookupListIndex = list(lookup_indices)
|
2015-09-07 17:22:37 +02:00
|
|
|
frec.Feature.LookupCount = len(lookup_indices)
|
|
|
|
table.FeatureList.FeatureRecord.append(frec)
|
|
|
|
feature_indices[feature_key] = feature_index
|
2020-07-15 14:14:01 +01: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
|
|
|
|
2020-07-15 14:14:01 +01:00
|
|
|
req_feature_index = required_feature_indices.get((script, lang))
|
2015-09-08 15:55:54 +02:00
|
|
|
if req_feature_index is None:
|
|
|
|
langrec.LangSys.ReqFeatureIndex = 0xFFFF
|
|
|
|
else:
|
|
|
|
langrec.LangSys.ReqFeatureIndex = req_feature_index
|
|
|
|
|
2020-07-15 14:14:01 +01:00
|
|
|
langrec.LangSys.FeatureIndex = [
|
|
|
|
i for i in feature_indices if i != req_feature_index
|
|
|
|
]
|
|
|
|
langrec.LangSys.FeatureCount = len(langrec.LangSys.FeatureIndex)
|
2015-09-08 15:55:54 +02:00
|
|
|
|
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
|
2020-07-15 14:14:01 +01:00
|
|
|
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 '
|
2020-07-15 14:14:01 +01:00
|
|
|
"the first of the languagesystem statements",
|
|
|
|
location,
|
|
|
|
)
|
2018-07-24 16:17:23 +01:00
|
|
|
if script == "DFLT":
|
|
|
|
if self.seen_non_DFLT_script_:
|
|
|
|
raise FeatureLibError(
|
|
|
|
'languagesystems using the "DFLT" script tag must '
|
|
|
|
"precede all other languagesystems",
|
2020-07-15 14:14:01 +01:00
|
|
|
location,
|
2018-07-24 16:17:23 +01:00
|
|
|
)
|
|
|
|
else:
|
|
|
|
self.seen_non_DFLT_script_ = True
|
2015-09-08 10:56:07 +02:00
|
|
|
if (script, language) in self.default_language_systems_:
|
|
|
|
raise FeatureLibError(
|
2020-07-15 14:14:01 +01:00
|
|
|
'"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:
|
2020-07-15 14:14:01 +01:00
|
|
|
return frozenset({("DFLT", "dflt")})
|
2015-09-04 22:29:06 +02:00
|
|
|
|
|
|
|
def start_feature(self, location, name):
|
|
|
|
self.language_systems = self.get_default_language_systems_()
|
2020-07-15 14:14:01 +01:00
|
|
|
self.script_ = "DFLT"
|
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
|
2016-03-13 00:16:27 +04:00
|
|
|
self.lookupflag_ = 0
|
|
|
|
self.lookupflag_markFilterSet_ = None
|
2016-01-11 16:00:52 +01:00
|
|
|
if name == "aalt":
|
|
|
|
self.aalt_location_ = location
|
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
|
2016-03-13 00:16:27 +04:00
|
|
|
self.lookupflag_ = 0
|
|
|
|
self.lookupflag_markFilterSet_ = None
|
2015-09-07 11:14:03 +02:00
|
|
|
|
2015-09-07 13:33:44 +02:00
|
|
|
def start_lookup_block(self, location, name):
|
|
|
|
if name in self.named_lookups_:
|
|
|
|
raise FeatureLibError(
|
2020-07-15 14:14:01 +01:00
|
|
|
'Lookup "%s" has already been defined' % name, location
|
|
|
|
)
|
2016-02-03 11:39:50 +01:00
|
|
|
if self.cur_feature_name_ == "aalt":
|
|
|
|
raise FeatureLibError(
|
|
|
|
"Lookup blocks cannot be placed inside 'aalt' features; "
|
|
|
|
"move it out, and then refer to it with a lookup statement",
|
2020-07-15 14:14:01 +01:00
|
|
|
location,
|
|
|
|
)
|
2015-09-07 13:33:44 +02:00
|
|
|
self.cur_lookup_name_ = name
|
|
|
|
self.named_lookups_[name] = None
|
|
|
|
self.cur_lookup_ = None
|
2020-01-29 23:42:17 +02:00
|
|
|
if self.cur_feature_name_ is None:
|
|
|
|
self.lookupflag_ = 0
|
|
|
|
self.lookupflag_markFilterSet_ = None
|
2015-09-07 13:33:44 +02:00
|
|
|
|
|
|
|
def end_lookup_block(self):
|
|
|
|
assert self.cur_lookup_name_ is not None
|
|
|
|
self.cur_lookup_name_ = None
|
|
|
|
self.cur_lookup_ = None
|
2020-01-29 23:42:17 +02:00
|
|
|
if self.cur_feature_name_ is None:
|
|
|
|
self.lookupflag_ = 0
|
|
|
|
self.lookupflag_markFilterSet_ = None
|
2015-09-07 13:33:44 +02:00
|
|
|
|
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_)
|
|
|
|
|
2016-01-11 18:01:47 +01:00
|
|
|
def set_font_revision(self, location, revision):
|
|
|
|
self.fontRevision_ = revision
|
|
|
|
|
2015-09-08 15:55:54 +02:00
|
|
|
def set_language(self, location, language, include_default, required):
|
2020-07-15 14:14:01 +01:00
|
|
|
assert len(language) == 4
|
|
|
|
if self.cur_feature_name_ in ("aalt", "size"):
|
2015-09-08 12:18:03 +02:00
|
|
|
raise FeatureLibError(
|
|
|
|
"Language statements are not allowed "
|
2020-07-15 14:14:01 +01:00
|
|
|
'within "feature %s"' % self.cur_feature_name_,
|
|
|
|
location,
|
|
|
|
)
|
2020-04-20 23:09:53 +02:00
|
|
|
if self.cur_feature_name_ is None:
|
|
|
|
raise FeatureLibError(
|
|
|
|
"Language statements are not allowed "
|
2020-07-15 14:14:01 +01:00
|
|
|
"within standalone lookup blocks",
|
|
|
|
location,
|
|
|
|
)
|
2015-09-07 11:14:03 +02:00
|
|
|
self.cur_lookup_ = None
|
2016-06-21 14:28:00 -07:00
|
|
|
|
|
|
|
key = (self.script_, language, self.cur_feature_name_)
|
2020-07-15 14:14:01 +01:00
|
|
|
lookups = self.features_.get((key[0], "dflt", key[2]))
|
|
|
|
if (language == "dflt" or include_default) and lookups:
|
2018-08-17 10:32:55 +07:00
|
|
|
self.features_[key] = lookups[:]
|
|
|
|
else:
|
2016-06-21 14:28:00 -07:00
|
|
|
self.features_[key] = []
|
2018-07-09 21:10:46 +01:00
|
|
|
self.language_systems = frozenset([(self.script_, language)])
|
2016-06-21 14:28:00 -07:00
|
|
|
|
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 "
|
2020-07-15 14:14:01 +01:00
|
|
|
"specified feature %s as its required feature"
|
|
|
|
% (
|
|
|
|
language.strip(),
|
|
|
|
self.script_.strip(),
|
|
|
|
self.required_features_[key].strip(),
|
|
|
|
),
|
|
|
|
location,
|
|
|
|
)
|
2015-09-08 15:55:54 +02:00
|
|
|
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):
|
2017-02-25 18:51:28 +00:00
|
|
|
glyphs = frozenset(glyphs)
|
2017-02-25 19:06:39 +00:00
|
|
|
id_ = self.markAttachClassID_.get(glyphs)
|
|
|
|
if id_ is not None:
|
|
|
|
return id_
|
|
|
|
id_ = len(self.markAttachClassID_) + 1
|
|
|
|
self.markAttachClassID_[glyphs] = id_
|
2015-12-10 19:17:11 +01:00
|
|
|
for glyph in glyphs:
|
|
|
|
if glyph in self.markAttach_:
|
|
|
|
_, loc = self.markAttach_[glyph]
|
|
|
|
raise FeatureLibError(
|
|
|
|
"Glyph %s already has been assigned "
|
2020-07-02 14:09:10 +01:00
|
|
|
"a MarkAttachmentType at %s" % (glyph, loc),
|
2020-07-15 14:14:01 +01:00
|
|
|
location,
|
|
|
|
)
|
2017-02-25 19:06:39 +00:00
|
|
|
self.markAttach_[glyph] = (id_, location)
|
|
|
|
return id_
|
2015-12-10 19:17:11 +01:00
|
|
|
|
|
|
|
def getMarkFilterSet_(self, location, glyphs):
|
2017-02-25 18:51:28 +00:00
|
|
|
glyphs = frozenset(glyphs)
|
2017-02-25 19:06:39 +00:00
|
|
|
id_ = self.markFilterSets_.get(glyphs)
|
|
|
|
if id_ is not None:
|
|
|
|
return id_
|
|
|
|
id_ = len(self.markFilterSets_)
|
|
|
|
self.markFilterSets_[glyphs] = id_
|
|
|
|
return id_
|
2015-12-10 19:17:11 +01:00
|
|
|
|
|
|
|
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):
|
2020-07-15 14:14:01 +01:00
|
|
|
if self.cur_feature_name_ in ("aalt", "size"):
|
2015-09-08 12:18:03 +02:00
|
|
|
raise FeatureLibError(
|
|
|
|
"Script statements are not allowed "
|
2020-07-15 14:14:01 +01:00
|
|
|
'within "feature %s"' % self.cur_feature_name_,
|
|
|
|
location,
|
|
|
|
)
|
2020-04-20 23:09:53 +02:00
|
|
|
if self.cur_feature_name_ is None:
|
|
|
|
raise FeatureLibError(
|
2020-07-15 14:14:01 +01:00
|
|
|
"Script statements are not allowed " "within standalone lookup blocks",
|
|
|
|
location,
|
|
|
|
)
|
|
|
|
if self.language_systems == {(script, "dflt")}:
|
2020-04-20 23:45:45 +02:00
|
|
|
# Nothing to do.
|
|
|
|
return
|
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
|
2020-07-15 14:14:01 +01: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 = []
|
2020-05-12 06:28:25 +01:00
|
|
|
for lookuplist in lookups:
|
|
|
|
if lookuplist is not None:
|
2020-07-15 14:14:01 +01:00
|
|
|
lookup_builders.append(
|
|
|
|
[self.named_lookups_.get(l.name) for l in lookuplist]
|
|
|
|
)
|
2015-11-30 15:02:09 +01:00
|
|
|
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)
|
2020-07-15 14:14:01 +01:00
|
|
|
lookup.rules.append(
|
2020-07-15 17:07:19 +01:00
|
|
|
ChainContextualRule(
|
|
|
|
prefix, glyphs, suffix, self.find_lookup_builders_(lookups)
|
|
|
|
)
|
2020-07-15 14:14:01 +01:00
|
|
|
)
|
2015-12-09 22:56:24 +01:00
|
|
|
|
2020-07-15 14:14:01 +01:00
|
|
|
def add_chain_context_subst(self, location, prefix, glyphs, suffix, lookups):
|
2015-11-30 15:02:09 +01:00
|
|
|
lookup = self.get_lookup_(location, ChainContextSubstBuilder)
|
2020-07-15 14:14:01 +01:00
|
|
|
lookup.rules.append(
|
2020-07-15 17:07:19 +01:00
|
|
|
ChainContextualRule(
|
|
|
|
prefix, glyphs, suffix, self.find_lookup_builders_(lookups)
|
|
|
|
)
|
2020-07-15 14:14:01 +01:00
|
|
|
)
|
2015-11-30 15:02:09 +01:00
|
|
|
|
2020-07-15 14:14:01 +01:00
|
|
|
def add_alternate_subst(self, location, prefix, glyph, suffix, replacement):
|
2016-02-03 17:38:38 +01:00
|
|
|
if self.cur_feature_name_ == "aalt":
|
|
|
|
alts = self.aalt_alternates_.setdefault(glyph, set())
|
|
|
|
alts.update(replacement)
|
|
|
|
return
|
2016-01-07 11:32:54 +01:00
|
|
|
if prefix or suffix:
|
|
|
|
chain = self.get_lookup_(location, ChainContextSubstBuilder)
|
2016-02-04 14:34:45 +01:00
|
|
|
lookup = self.get_chained_lookup_(location, AlternateSubstBuilder)
|
2020-07-15 17:07:19 +01:00
|
|
|
chain.rules.append(ChainContextualRule(prefix, [{glyph}], suffix, [lookup]))
|
2016-01-07 11:32:54 +01:00
|
|
|
else:
|
|
|
|
lookup = self.get_lookup_(location, AlternateSubstBuilder)
|
2015-09-07 11:14:03 +02:00
|
|
|
if glyph in lookup.alternates:
|
|
|
|
raise FeatureLibError(
|
2020-07-15 14:14:01 +01:00
|
|
|
'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-11 16:00:52 +01:00
|
|
|
def add_feature_reference(self, location, featureName):
|
|
|
|
if self.cur_feature_name_ != "aalt":
|
|
|
|
raise FeatureLibError(
|
2020-07-15 14:14:01 +01:00
|
|
|
'Feature references are only allowed inside "feature aalt"', location
|
|
|
|
)
|
2016-01-11 16:00:52 +01:00
|
|
|
self.aalt_features_.append((location, featureName))
|
|
|
|
|
2018-02-05 22:45:56 -08:00
|
|
|
def add_featureName(self, tag):
|
|
|
|
self.featureNames_.add(tag)
|
2016-03-18 09:55:51 +04:00
|
|
|
|
2018-02-05 23:31:30 -08:00
|
|
|
def add_cv_parameter(self, tag):
|
|
|
|
self.cv_parameters_.add(tag)
|
|
|
|
|
|
|
|
def add_to_cv_num_named_params(self, tag):
|
2020-05-12 23:11:17 +01:00
|
|
|
"""Adds new items to ``self.cv_num_named_params_``
|
2018-02-05 23:31:30 -08:00
|
|
|
or increments the count of existing items."""
|
|
|
|
if tag in self.cv_num_named_params_:
|
|
|
|
self.cv_num_named_params_[tag] += 1
|
|
|
|
else:
|
|
|
|
self.cv_num_named_params_[tag] = 1
|
|
|
|
|
|
|
|
def add_cv_character(self, character, tag):
|
|
|
|
self.cv_characters_[tag].append(character)
|
|
|
|
|
2016-03-19 02:47:04 +04:00
|
|
|
def set_base_axis(self, bases, scripts, vertical):
|
|
|
|
if vertical:
|
|
|
|
self.base_vert_axis_ = (bases, scripts)
|
|
|
|
else:
|
|
|
|
self.base_horiz_axis_ = (bases, scripts)
|
|
|
|
|
2020-07-15 14:14:01 +01:00
|
|
|
def set_size_parameters(
|
|
|
|
self, location, DesignSize, SubfamilyID, RangeStart, RangeEnd
|
|
|
|
):
|
|
|
|
if self.cur_feature_name_ != "size":
|
2016-03-18 13:18:31 +04:00
|
|
|
raise FeatureLibError(
|
|
|
|
"Parameters statements are not allowed "
|
2020-07-15 14:14:01 +01:00
|
|
|
'within "feature %s"' % self.cur_feature_name_,
|
|
|
|
location,
|
|
|
|
)
|
2016-03-18 13:18:31 +04:00
|
|
|
self.size_parameters_ = [DesignSize, SubfamilyID, RangeStart, RangeEnd]
|
|
|
|
for script, lang in self.language_systems:
|
|
|
|
key = (script, lang, self.cur_feature_name_)
|
|
|
|
self.features_.setdefault(key, [])
|
|
|
|
|
2020-07-15 14:14:01 +01:00
|
|
|
def add_ligature_subst(
|
|
|
|
self, location, prefix, glyphs, suffix, replacement, forceChain
|
|
|
|
):
|
2016-02-04 15:31:29 +01:00
|
|
|
if prefix or suffix or forceChain:
|
2016-01-07 10:31:13 +01:00
|
|
|
chain = self.get_lookup_(location, ChainContextSubstBuilder)
|
2016-02-04 14:22:39 +01:00
|
|
|
lookup = self.get_chained_lookup_(location, LigatureSubstBuilder)
|
2020-07-15 17:07:19 +01:00
|
|
|
chain.rules.append(ChainContextualRule(prefix, glyphs, suffix, [lookup]))
|
2016-01-07 10:31:13 +01:00
|
|
|
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
|
|
|
|
2020-07-15 14:14:01 +01:00
|
|
|
def add_multiple_subst(
|
|
|
|
self, location, prefix, glyph, suffix, replacements, forceChain=False
|
|
|
|
):
|
2019-02-19 04:42:28 +02:00
|
|
|
if prefix or suffix or forceChain:
|
2016-02-04 14:46:22 +01:00
|
|
|
chain = self.get_lookup_(location, ChainContextSubstBuilder)
|
2016-01-06 17:33:34 +01:00
|
|
|
sub = self.get_chained_lookup_(location, MultipleSubstBuilder)
|
|
|
|
sub.mapping[glyph] = replacements
|
2020-07-15 17:07:19 +01:00
|
|
|
chain.rules.append(ChainContextualRule(prefix, [{glyph}], suffix, [sub]))
|
2016-01-06 17:33:34 +01:00
|
|
|
return
|
2015-09-10 15:28:02 +02:00
|
|
|
lookup = self.get_lookup_(location, MultipleSubstBuilder)
|
|
|
|
if glyph in lookup.mapping:
|
2020-01-26 10:25:52 -05:00
|
|
|
if replacements == lookup.mapping[glyph]:
|
|
|
|
log.info(
|
2020-07-15 14:14:01 +01:00
|
|
|
"Removing duplicate multiple substitution from glyph"
|
2020-01-26 10:25:52 -05:00
|
|
|
' "%s" to %s%s',
|
2020-07-15 14:14:01 +01:00
|
|
|
glyph,
|
|
|
|
replacements,
|
|
|
|
f" at {location}" if location else "",
|
2020-01-26 10:25:52 -05:00
|
|
|
)
|
|
|
|
else:
|
|
|
|
raise FeatureLibError(
|
2020-07-15 14:14:01 +01:00
|
|
|
'Already defined substitution for glyph "%s"' % glyph, location
|
|
|
|
)
|
2015-09-10 15:28:02 +02:00
|
|
|
lookup.mapping[glyph] = replacements
|
2015-09-08 12:05:44 +02:00
|
|
|
|
2020-07-15 14:14:01 +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)
|
[otlLib] Refactor chained contextual builders (#2007)
* Introduce a new subclass for chained contextual (sub and pos)
* Rename .substitutions to .rules in subst builders to allow for code reuse
* Make format of subtable break marker tuple common between sub/pos
Note that prior to this patch, add_subtable_break in a Subst builder adds:
(self.SUBTABLE_BREAK_, self.SUBTABLE_BREAK_, self.SUBTABLE_BREAK_, self.SUBTABLE_BREAK_)
while add_subtable_break in a Pos builder adds:
(self.SUBTABLE_BREAK_, self.SUBTABLE_BREAK_, self.SUBTABLE_BREAK_, [self.SUBTABLE_BREAK_])
This is messy. If we read the first element from the tuple instead of the last one to test if a rule is a subtable break, we can make the marker tuple the same.
* And now the subtable break code can be hoisted into superclass
* These helper methods will make the build routine common
* Hoist common build method to superclass
The diff doesn’t show it very clearly because it’s being too clever, but all I’ve done is moved one method. Everything works apart from the error message, which comes next.
* Fix the error message
2020-07-02 18:40:20 +01:00
|
|
|
lookup.rules.append((old_prefix, old_suffix, mapping))
|
2015-12-03 13:05:42 +01:00
|
|
|
|
2016-02-04 16:45:05 +01:00
|
|
|
def add_single_subst(self, location, prefix, suffix, mapping, forceChain):
|
2016-02-03 17:38:38 +01:00
|
|
|
if self.cur_feature_name_ == "aalt":
|
|
|
|
for (from_glyph, to_glyph) in mapping.items():
|
|
|
|
alts = self.aalt_alternates_.setdefault(from_glyph, set())
|
|
|
|
alts.add(to_glyph)
|
|
|
|
return
|
2016-02-04 16:45:05 +01:00
|
|
|
if prefix or suffix or forceChain:
|
2016-02-05 15:12:07 +01:00
|
|
|
self.add_single_subst_chained_(location, prefix, suffix, mapping)
|
2016-01-06 16:15:26 +01:00
|
|
|
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:
|
2019-12-02 17:05:59 +01:00
|
|
|
if to_glyph == lookup.mapping[from_glyph]:
|
2019-12-18 10:03:08 +01:00
|
|
|
log.info(
|
2020-07-15 14:14:01 +01:00
|
|
|
"Removing duplicate single substitution from glyph"
|
2020-07-02 14:09:10 +01:00
|
|
|
' "%s" to "%s" at %s',
|
2020-07-15 14:14:01 +01:00
|
|
|
from_glyph,
|
|
|
|
to_glyph,
|
|
|
|
location,
|
2019-12-18 10:03:08 +01:00
|
|
|
)
|
2019-12-02 17:05:59 +01:00
|
|
|
else:
|
|
|
|
raise FeatureLibError(
|
2020-07-15 14:14:01 +01:00
|
|
|
'Already defined rule for replacing glyph "%s" by "%s"'
|
|
|
|
% (from_glyph, lookup.mapping[from_glyph]),
|
|
|
|
location,
|
|
|
|
)
|
2015-09-08 10:33:07 +02:00
|
|
|
lookup.mapping[from_glyph] = to_glyph
|
|
|
|
|
2016-02-05 15:12:07 +01:00
|
|
|
def add_single_subst_chained_(self, location, prefix, suffix, mapping):
|
2019-03-06 16:01:28 +01:00
|
|
|
# https://github.com/fonttools/fonttools/issues/512
|
2016-02-05 15:12:07 +01:00
|
|
|
chain = self.get_lookup_(location, ChainContextSubstBuilder)
|
2019-02-25 23:35:22 +02:00
|
|
|
sub = chain.find_chainable_single_subst(set(mapping.keys()))
|
2016-02-05 15:12:07 +01:00
|
|
|
if sub is None:
|
|
|
|
sub = self.get_chained_lookup_(location, SingleSubstBuilder)
|
|
|
|
sub.mapping.update(mapping)
|
2020-07-15 17:07:19 +01:00
|
|
|
chain.rules.append(
|
|
|
|
ChainContextualRule(prefix, [list(mapping.keys())], suffix, [sub])
|
|
|
|
)
|
2016-02-05 15:12:07 +01:00
|
|
|
|
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(
|
2020-07-15 14:14:01 +01:00
|
|
|
location,
|
|
|
|
glyphclass,
|
2016-01-14 13:08:26 +01:00
|
|
|
makeOpenTypeAnchor(entryAnchor),
|
2020-07-15 14:14:01 +01:00
|
|
|
makeOpenTypeAnchor(exitAnchor),
|
|
|
|
)
|
2015-12-07 23:56:08 +01:00
|
|
|
|
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:
|
2016-01-14 13:08:26 +01:00
|
|
|
otMarkAnchor = makeOpenTypeAnchor(markClassDef.anchor)
|
2020-07-15 14:14:01 +01:00
|
|
|
lookupBuilder.marks[mark] = (markClass.name, otMarkAnchor)
|
2017-04-30 22:17:30 +02:00
|
|
|
else:
|
|
|
|
existingMarkClass = lookupBuilder.marks[mark][0]
|
|
|
|
if markClass.name != existingMarkClass:
|
|
|
|
raise FeatureLibError(
|
2020-07-15 14:14:01 +01:00
|
|
|
"Glyph %s cannot be in both @%s and @%s"
|
|
|
|
% (mark, existingMarkClass, markClass.name),
|
|
|
|
location,
|
|
|
|
)
|
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:
|
2016-01-14 13:08:26 +01:00
|
|
|
otBaseAnchor = makeOpenTypeAnchor(baseAnchor)
|
2015-12-09 16:51:15 +01:00
|
|
|
for base in bases:
|
2020-07-15 14:14:01 +01:00
|
|
|
builder.bases.setdefault(base, {})[markClass.name] = otBaseAnchor
|
2015-12-09 16:51:15 +01:00
|
|
|
|
|
|
|
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:
|
2016-01-14 13:08:26 +01:00
|
|
|
anchors[markClass.name] = makeOpenTypeAnchor(ligAnchor)
|
2015-12-09 16:51:15 +01:00
|
|
|
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:
|
2016-01-14 13:08:26 +01:00
|
|
|
otBaseAnchor = makeOpenTypeAnchor(baseAnchor)
|
2015-12-09 17:56:47 +01:00
|
|
|
for baseMark in baseMarks:
|
2020-07-15 14:14:01 +01:00
|
|
|
builder.baseMarks.setdefault(baseMark, {})[
|
|
|
|
markClass.name
|
|
|
|
] = otBaseAnchor
|
2015-12-09 17:56:47 +01:00
|
|
|
|
2020-07-15 14:14:01 +01:00
|
|
|
def add_class_pair_pos(self, location, glyphclass1, value1, glyphclass2, value2):
|
2016-02-02 18:23:37 +01:00
|
|
|
lookup = self.get_lookup_(location, PairPosBuilder)
|
2020-07-02 14:09:10 +01:00
|
|
|
v1 = makeOpenTypeValueRecord(value1, pairPosContext=True)
|
|
|
|
v2 = makeOpenTypeValueRecord(value2, pairPosContext=True)
|
|
|
|
lookup.addClassPair(location, glyphclass1, v1, glyphclass2, v2)
|
2015-12-21 16:06:59 +01:00
|
|
|
|
2018-07-24 17:13:52 +01:00
|
|
|
def add_subtable_break(self, location):
|
2019-02-25 23:30:03 +02:00
|
|
|
self.cur_lookup_.add_subtable_break(location)
|
2018-07-24 17:13:52 +01:00
|
|
|
|
2015-12-21 16:06:59 +01:00
|
|
|
def add_specific_pair_pos(self, location, glyph1, value1, glyph2, value2):
|
2016-02-02 18:23:37 +01:00
|
|
|
lookup = self.get_lookup_(location, PairPosBuilder)
|
2020-07-02 14:09:10 +01:00
|
|
|
v1 = makeOpenTypeValueRecord(value1, pairPosContext=True)
|
|
|
|
v2 = makeOpenTypeValueRecord(value2, pairPosContext=True)
|
|
|
|
lookup.addGlyphPair(location, glyph1, v1, glyph2, v2)
|
2015-12-07 17:18:18 +01:00
|
|
|
|
2016-02-09 08:58:18 +01:00
|
|
|
def add_single_pos(self, location, prefix, suffix, pos, forceChain):
|
|
|
|
if prefix or suffix or forceChain:
|
2016-02-09 15:38:18 +01:00
|
|
|
self.add_single_pos_chained_(location, prefix, suffix, pos)
|
|
|
|
else:
|
|
|
|
lookup = self.get_lookup_(location, SinglePosBuilder)
|
2016-01-25 16:27:18 +01:00
|
|
|
for glyphs, value in pos:
|
2020-07-02 14:09:10 +01:00
|
|
|
otValueRecord = makeOpenTypeValueRecord(value, pairPosContext=False)
|
2016-01-25 16:27:18 +01:00
|
|
|
for glyph in glyphs:
|
2020-07-02 14:09:10 +01:00
|
|
|
try:
|
|
|
|
lookup.add_pos(location, glyph, otValueRecord)
|
|
|
|
except OpenTypeLibError as e:
|
|
|
|
raise FeatureLibError(str(e), e.location) from e
|
2016-02-09 15:38:18 +01:00
|
|
|
|
|
|
|
def add_single_pos_chained_(self, location, prefix, suffix, pos):
|
2017-02-17 11:21:11 +01:00
|
|
|
# https://github.com/fonttools/fonttools/issues/514
|
2016-02-09 15:38:18 +01:00
|
|
|
chain = self.get_lookup_(location, ChainContextPosBuilder)
|
2017-02-17 11:21:11 +01:00
|
|
|
targets = []
|
|
|
|
for _, _, _, lookups in chain.rules:
|
2019-02-25 23:39:04 +02:00
|
|
|
targets.extend(lookups)
|
2016-02-09 15:38:18 +01:00
|
|
|
subs = []
|
2016-01-25 16:27:18 +01:00
|
|
|
for glyphs, value in pos:
|
2016-02-09 15:38:18 +01:00
|
|
|
if value is None:
|
|
|
|
subs.append(None)
|
|
|
|
continue
|
2020-07-02 14:09:10 +01:00
|
|
|
otValue = makeOpenTypeValueRecord(value, pairPosContext=False)
|
2019-02-25 23:39:04 +02:00
|
|
|
sub = chain.find_chainable_single_pos(targets, glyphs, otValue)
|
2017-02-17 11:21:11 +01:00
|
|
|
if sub is None:
|
2016-02-09 15:38:18 +01:00
|
|
|
sub = self.get_chained_lookup_(location, SinglePosBuilder)
|
2017-02-17 11:21:11 +01:00
|
|
|
targets.append(sub)
|
2016-01-25 16:27:18 +01:00
|
|
|
for glyph in glyphs:
|
2020-07-02 14:09:10 +01:00
|
|
|
sub.add_pos(location, glyph, otValue)
|
2016-02-09 15:38:18 +01:00
|
|
|
subs.append(sub)
|
|
|
|
assert len(pos) == len(subs), (pos, subs)
|
2020-07-15 17:07:19 +01:00
|
|
|
chain.rules.append(
|
|
|
|
ChainContextualRule(prefix, [g for g, v in pos], suffix, subs)
|
|
|
|
)
|
2015-12-04 11:16:43 +01:00
|
|
|
|
2016-01-08 19:06:52 +01:00
|
|
|
def setGlyphClass_(self, location, glyph, glyphClass):
|
|
|
|
oldClass, oldLocation = self.glyphClassDefs_.get(glyph, (None, None))
|
|
|
|
if oldClass and oldClass != glyphClass:
|
|
|
|
raise FeatureLibError(
|
2020-07-15 14:14:01 +01:00
|
|
|
"Glyph %s was assigned to a different class at %s"
|
|
|
|
% (glyph, oldLocation),
|
|
|
|
location,
|
|
|
|
)
|
2016-01-08 19:06:52 +01:00
|
|
|
self.glyphClassDefs_[glyph] = (glyphClass, location)
|
|
|
|
|
2020-07-15 14:14:01 +01:00
|
|
|
def add_glyphClassDef(
|
|
|
|
self, location, baseGlyphs, ligatureGlyphs, markGlyphs, componentGlyphs
|
|
|
|
):
|
2016-01-08 19:06:52 +01:00
|
|
|
for glyph in baseGlyphs:
|
|
|
|
self.setGlyphClass_(location, glyph, 1)
|
|
|
|
for glyph in ligatureGlyphs:
|
|
|
|
self.setGlyphClass_(location, glyph, 2)
|
|
|
|
for glyph in markGlyphs:
|
|
|
|
self.setGlyphClass_(location, glyph, 3)
|
|
|
|
for glyph in componentGlyphs:
|
|
|
|
self.setGlyphClass_(location, glyph, 4)
|
2016-01-08 17:11:29 +01:00
|
|
|
|
2016-01-07 17:22:31 +01:00
|
|
|
def add_ligatureCaretByIndex_(self, location, glyphs, carets):
|
|
|
|
for glyph in glyphs:
|
2019-09-26 16:44:46 +02:00
|
|
|
if glyph not in self.ligCaretPoints_:
|
|
|
|
self.ligCaretPoints_[glyph] = 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:
|
2019-09-26 16:44:46 +02:00
|
|
|
if glyph not in self.ligCaretCoords_:
|
|
|
|
self.ligCaretCoords_[glyph] = carets
|
2016-01-07 16:39:35 +01:00
|
|
|
|
2020-07-15 14:14:01 +01:00
|
|
|
def add_name_record(self, location, nameID, platformID, platEncID, langID, string):
|
2016-03-14 23:44:25 +04:00
|
|
|
self.names_.append([nameID, platformID, platEncID, langID, string])
|
|
|
|
|
2016-03-19 22:05:38 +04:00
|
|
|
def add_os2_field(self, key, value):
|
|
|
|
self.os2_[key] = value
|
|
|
|
|
2016-04-09 17:42:38 +02:00
|
|
|
def add_hhea_field(self, key, value):
|
|
|
|
self.hhea_[key] = value
|
|
|
|
|
2016-10-17 09:00:45 +01:00
|
|
|
def add_vhea_field(self, key, value):
|
|
|
|
self.vhea_[key] = value
|
|
|
|
|
2015-12-04 11:16:43 +01:00
|
|
|
|
2016-01-14 13:08:26 +01:00
|
|
|
def makeOpenTypeAnchor(anchor):
|
2015-12-07 23:56:08 +01:00
|
|
|
"""ast.Anchor --> otTables.Anchor"""
|
|
|
|
if anchor is None:
|
|
|
|
return None
|
2016-01-14 13:08:26 +01:00
|
|
|
deviceX, deviceY = None, None
|
2015-12-07 23:56:08 +01:00
|
|
|
if anchor.xDeviceTable is not None:
|
2016-01-22 19:45:56 +01:00
|
|
|
deviceX = otl.buildDevice(dict(anchor.xDeviceTable))
|
2015-12-07 23:56:08 +01:00
|
|
|
if anchor.yDeviceTable is not None:
|
2016-01-22 19:45:56 +01:00
|
|
|
deviceY = otl.buildDevice(dict(anchor.yDeviceTable))
|
2020-07-15 14:14:01 +01:00
|
|
|
return otl.buildAnchor(anchor.x, anchor.y, anchor.contourpoint, deviceX, deviceY)
|
2015-12-07 23:56:08 +01:00
|
|
|
|
|
|
|
|
2016-01-14 16:25:28 +01:00
|
|
|
_VALUEREC_ATTRS = {
|
|
|
|
name[0].lower() + name[1:]: (name, isDevice)
|
|
|
|
for _, name, isDevice, _ in otBase.valueRecordFormat
|
|
|
|
if not name.startswith("Reserved")
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-02-16 15:06:02 +01:00
|
|
|
def makeOpenTypeValueRecord(v, pairPosContext):
|
2020-07-02 14:09:10 +01:00
|
|
|
"""ast.ValueRecord --> otBase.ValueRecord"""
|
2019-01-19 12:11:14 +00:00
|
|
|
if not v:
|
2020-07-02 14:09:10 +01:00
|
|
|
return None
|
2015-12-04 15:49:04 +01:00
|
|
|
|
2016-01-14 16:25:28 +01:00
|
|
|
vr = {}
|
|
|
|
for astName, (otName, isDevice) in _VALUEREC_ATTRS.items():
|
|
|
|
val = getattr(v, astName, None)
|
|
|
|
if val:
|
2016-01-22 19:45:56 +01:00
|
|
|
vr[otName] = otl.buildDevice(dict(val)) if isDevice else val
|
2017-02-16 15:06:02 +01:00
|
|
|
if pairPosContext and not vr:
|
|
|
|
vr = {"YAdvance": 0} if v.vertical else {"XAdvance": 0}
|
2016-01-19 17:05:17 +01:00
|
|
|
valRec = otl.buildValue(vr)
|
2020-07-02 14:09:10 +01:00
|
|
|
return valRec
|