Support variable feature syntax (#2432)

This commit is contained in:
Simon Cozens 2021-10-28 11:58:54 +01:00 committed by GitHub
parent c194a18be7
commit 563730f8ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 1672 additions and 658 deletions

View File

@ -34,6 +34,7 @@ __all__ = [
"ChainContextPosStatement",
"ChainContextSubstStatement",
"CharacterStatement",
"ConditionsetStatement",
"CursivePosStatement",
"ElidedFallbackName",
"ElidedFallbackNameID",
@ -1261,11 +1262,21 @@ class MultipleSubstStatement(Statement):
if not self.replacement and hasattr(self.glyph, "glyphSet"):
for glyph in self.glyph.glyphSet():
builder.add_multiple_subst(
self.location, prefix, glyph, suffix, self.replacement, self.forceChain
self.location,
prefix,
glyph,
suffix,
self.replacement,
self.forceChain,
)
else:
builder.add_multiple_subst(
self.location, prefix, self.glyph, suffix, self.replacement, self.forceChain
self.location,
prefix,
self.glyph,
suffix,
self.replacement,
self.forceChain,
)
def asFea(self, indent=""):
@ -2033,3 +2044,79 @@ class AxisValueLocationStatement(Statement):
res += f"location {self.tag} "
res += f"{' '.join(str(i) for i in self.values)};\n"
return res
class ConditionsetStatement(Statement):
"""
A variable layout conditionset
Args:
name (str): the name of this conditionset
conditions (dict): a dictionary mapping axis tags to a
tuple of (min,max) userspace coordinates.
"""
def __init__(self, name, conditions, location=None):
Statement.__init__(self, location)
self.name = name
self.conditions = conditions
def build(self, builder):
builder.add_conditionset(self.name, self.conditions)
def asFea(self, res="", indent=""):
res += indent + f"conditionset {self.name} " + "{\n"
for tag, (minvalue, maxvalue) in self.conditions.items():
res += indent + SHIFT + f"{tag} {minvalue} {maxvalue};\n"
res += indent + "}" + f" {self.name};\n"
return res
class VariationBlock(Block):
"""A variation feature block, applicable in a given set of conditions."""
def __init__(self, name, conditionset, use_extension=False, location=None):
Block.__init__(self, location)
self.name, self.conditionset, self.use_extension = (
name,
conditionset,
use_extension,
)
def build(self, builder):
"""Call the ``start_feature`` callback on the builder object, visit
all the statements in this feature, and then call ``end_feature``."""
builder.start_feature(self.location, self.name)
if (
self.conditionset != "NULL"
and self.conditionset not in builder.conditionsets_
):
raise FeatureLibError(
f"variation block used undefined conditionset {self.conditionset}",
self.location,
)
# language exclude_dflt statements modify builder.features_
# limit them to this block with temporary builder.features_
features = builder.features_
builder.features_ = {}
Block.build(self, builder)
for key, value in builder.features_.items():
items = builder.feature_variations_.setdefault(key, {}).setdefault(
self.conditionset, []
)
items.extend(value)
if key not in features:
features[key] = [] # Ensure we make a feature record
builder.features_ = features
builder.end_feature()
def asFea(self, indent=""):
res = indent + "variation %s " % self.name.strip()
res += self.conditionset + " "
if self.use_extension:
res += "useExtension "
res += "{\n"
res += Block.asFea(self, indent=indent)
res += indent + "} %s;\n" % self.name.strip()
return res

View File

@ -8,6 +8,7 @@ from fontTools.feaLib.lookupDebugInfo import (
)
from fontTools.feaLib.parser import Parser
from fontTools.feaLib.ast import FeatureFile
from fontTools.feaLib.variableScalar import VariableScalar
from fontTools.otlLib import builder as otl
from fontTools.otlLib.maxContextCalc import maxCtxFont
from fontTools.ttLib import newTable, getTableModule
@ -30,6 +31,10 @@ from fontTools.otlLib.builder import (
ChainContextualRule,
)
from fontTools.otlLib.error import OpenTypeLibError
from fontTools.varLib.varStore import OnlineVarStoreBuilder
from fontTools.varLib.builder import buildVarDevTable
from fontTools.varLib.featureVars import addFeatureVariationsRaw
from fontTools.varLib.models import normalizeValue
from collections import defaultdict
import itertools
from io import StringIO
@ -111,6 +116,12 @@ class Builder(object):
else:
self.parseTree, self.file = None, featurefile
self.glyphMap = font.getReverseGlyphMap()
self.varstorebuilder = None
if "fvar" in font:
self.axes = font["fvar"].axes
self.varstorebuilder = OnlineVarStoreBuilder(
[ax.axisTag for ax in self.axes]
)
self.default_language_systems_ = set()
self.script_ = None
self.lookupflag_ = 0
@ -125,6 +136,7 @@ class Builder(object):
self.lookup_locations = {"GSUB": {}, "GPOS": {}}
self.features_ = {} # ('latn', 'DEU ', 'smcp') --> [LookupBuilder*]
self.required_features_ = {} # ('latn', 'DEU ') --> 'scmp'
self.feature_variations_ = {}
# for feature 'aalt'
self.aalt_features_ = [] # [(location, featureName)*], for 'aalt'
self.aalt_location_ = None
@ -162,6 +174,8 @@ class Builder(object):
self.vhea_ = {}
# for table 'STAT'
self.stat_ = {}
# for conditionsets
self.conditionsets_ = {}
def build(self, tables=None, debug=False):
if self.parseTree is None:
@ -197,6 +211,8 @@ class Builder(object):
if tag not in tables:
continue
table = self.makeTable(tag)
if self.feature_variations_:
self.makeFeatureVariations(table, tag)
if (
table.ScriptList.ScriptCount > 0
or table.FeatureList.FeatureCount > 0
@ -214,6 +230,8 @@ class Builder(object):
self.font["GDEF"] = gdef
elif "GDEF" in self.font:
del self.font["GDEF"]
elif self.varstorebuilder:
raise FeatureLibError("Must save GDEF when compiling a variable font")
if "BASE" in tables:
base = self.buildBASE()
if base:
@ -744,6 +762,16 @@ class Builder(object):
gdef.MarkAttachClassDef = self.buildGDEFMarkAttachClassDef_()
gdef.MarkGlyphSetsDef = self.buildGDEFMarkGlyphSetsDef_()
gdef.Version = 0x00010002 if gdef.MarkGlyphSetsDef else 0x00010000
if self.varstorebuilder:
store = self.varstorebuilder.finish()
if store.VarData:
gdef.Version = 0x00010003
gdef.VarStore = store
varidx_map = store.optimize()
gdef.remap_device_varidxes(varidx_map)
if 'GPOS' in self.font:
self.font['GPOS'].table.remap_device_varidxes(varidx_map)
if any(
(
gdef.GlyphClassDef,
@ -752,7 +780,7 @@ class Builder(object):
gdef.MarkAttachClassDef,
gdef.MarkGlyphSetsDef,
)
):
) or hasattr(gdef, "VarStore"):
result = newTable("GDEF")
result.table = gdef
return result
@ -848,7 +876,8 @@ class Builder(object):
)
size_feature = tag == "GPOS" and feature_tag == "size"
if len(lookup_indices) == 0 and not size_feature:
force_feature = self.any_feature_variations(feature_tag, tag)
if len(lookup_indices) == 0 and not size_feature and not force_feature:
continue
for ix in lookup_indices:
@ -914,6 +943,42 @@ class Builder(object):
table.LookupList.LookupCount = len(table.LookupList.Lookup)
return table
def makeFeatureVariations(self, table, table_tag):
feature_vars = {}
has_any_variations = False
# Sort out which lookups to build, gather their indices
for (
script_,
language,
feature_tag,
), variations in self.feature_variations_.items():
feature_vars[feature_tag] = []
for conditionset, builders in variations.items():
raw_conditionset = self.conditionsets_[conditionset]
indices = []
for b in builders:
if b.table != table_tag:
continue
assert b.lookup_index is not None
indices.append(b.lookup_index)
has_any_variations = True
feature_vars[feature_tag].append((raw_conditionset, indices))
if has_any_variations:
for feature_tag, conditions_and_lookups in feature_vars.items():
addFeatureVariationsRaw(
self.font, table, conditions_and_lookups, feature_tag
)
def any_feature_variations(self, feature_tag, table_tag):
for (_, _, feature), variations in self.feature_variations_.items():
if feature != feature_tag:
continue
for conditionset, builders in variations.items():
if any(b.table == table_tag for b in builders):
return True
return False
def get_lookup_name_(self, lookup):
rev = {v: k for k, v in self.named_lookups_.items()}
if lookup in rev:
@ -1298,8 +1363,8 @@ class Builder(object):
lookup.add_attachment(
location,
glyphclass,
makeOpenTypeAnchor(entryAnchor),
makeOpenTypeAnchor(exitAnchor),
self.makeOpenTypeAnchor(location, entryAnchor),
self.makeOpenTypeAnchor(location, exitAnchor),
)
def add_marks_(self, location, lookupBuilder, marks):
@ -1308,7 +1373,7 @@ class Builder(object):
for markClassDef in markClass.definitions:
for mark in markClassDef.glyphs.glyphSet():
if mark not in lookupBuilder.marks:
otMarkAnchor = makeOpenTypeAnchor(markClassDef.anchor)
otMarkAnchor = self.makeOpenTypeAnchor(location, markClassDef.anchor)
lookupBuilder.marks[mark] = (markClass.name, otMarkAnchor)
else:
existingMarkClass = lookupBuilder.marks[mark][0]
@ -1323,7 +1388,7 @@ class Builder(object):
builder = self.get_lookup_(location, MarkBasePosBuilder)
self.add_marks_(location, builder, marks)
for baseAnchor, markClass in marks:
otBaseAnchor = makeOpenTypeAnchor(baseAnchor)
otBaseAnchor = self.makeOpenTypeAnchor(location, baseAnchor)
for base in bases:
builder.bases.setdefault(base, {})[markClass.name] = otBaseAnchor
@ -1334,7 +1399,7 @@ class Builder(object):
anchors = {}
self.add_marks_(location, builder, marks)
for ligAnchor, markClass in marks:
anchors[markClass.name] = makeOpenTypeAnchor(ligAnchor)
anchors[markClass.name] = self.makeOpenTypeAnchor(location, ligAnchor)
componentAnchors.append(anchors)
for glyph in ligatures:
builder.ligatures[glyph] = componentAnchors
@ -1343,7 +1408,7 @@ class Builder(object):
builder = self.get_lookup_(location, MarkMarkPosBuilder)
self.add_marks_(location, builder, marks)
for baseAnchor, markClass in marks:
otBaseAnchor = makeOpenTypeAnchor(baseAnchor)
otBaseAnchor = self.makeOpenTypeAnchor(location, baseAnchor)
for baseMark in baseMarks:
builder.baseMarks.setdefault(baseMark, {})[
markClass.name
@ -1351,8 +1416,8 @@ class Builder(object):
def add_class_pair_pos(self, location, glyphclass1, value1, glyphclass2, value2):
lookup = self.get_lookup_(location, PairPosBuilder)
v1 = makeOpenTypeValueRecord(value1, pairPosContext=True)
v2 = makeOpenTypeValueRecord(value2, pairPosContext=True)
v1 = self.makeOpenTypeValueRecord(location, value1, pairPosContext=True)
v2 = self.makeOpenTypeValueRecord(location, value2, pairPosContext=True)
lookup.addClassPair(location, glyphclass1, v1, glyphclass2, v2)
def add_subtable_break(self, location):
@ -1360,8 +1425,8 @@ class Builder(object):
def add_specific_pair_pos(self, location, glyph1, value1, glyph2, value2):
lookup = self.get_lookup_(location, PairPosBuilder)
v1 = makeOpenTypeValueRecord(value1, pairPosContext=True)
v2 = makeOpenTypeValueRecord(value2, pairPosContext=True)
v1 = self.makeOpenTypeValueRecord(location, value1, pairPosContext=True)
v2 = self.makeOpenTypeValueRecord(location, value2, pairPosContext=True)
lookup.addGlyphPair(location, glyph1, v1, glyph2, v2)
def add_single_pos(self, location, prefix, suffix, pos, forceChain):
@ -1370,7 +1435,7 @@ class Builder(object):
else:
lookup = self.get_lookup_(location, SinglePosBuilder)
for glyphs, value in pos:
otValueRecord = makeOpenTypeValueRecord(value, pairPosContext=False)
otValueRecord = self.makeOpenTypeValueRecord(location, value, pairPosContext=False)
for glyph in glyphs:
try:
lookup.add_pos(location, glyph, otValueRecord)
@ -1388,7 +1453,7 @@ class Builder(object):
if value is None:
subs.append(None)
continue
otValue = makeOpenTypeValueRecord(value, pairPosContext=False)
otValue = self.makeOpenTypeValueRecord(location, value, pairPosContext=False)
sub = chain.find_chainable_single_pos(targets, glyphs, otValue)
if sub is None:
sub = self.get_chained_lookup_(location, SinglePosBuilder)
@ -1445,36 +1510,97 @@ class Builder(object):
def add_vhea_field(self, key, value):
self.vhea_[key] = value
def add_conditionset(self, key, value):
if not "fvar" in self.font:
raise FeatureLibError(
"Cannot add feature variations to a font without an 'fvar' table"
)
def makeOpenTypeAnchor(anchor):
# Normalize
axisMap = {
axis.axisTag: (axis.minValue, axis.defaultValue, axis.maxValue)
for axis in self.axes
}
value = {
tag: (
normalizeValue(bottom, axisMap[tag]),
normalizeValue(top, axisMap[tag]),
)
for tag, (bottom, top) in value.items()
}
self.conditionsets_[key] = value
def makeOpenTypeAnchor(self, location, anchor):
"""ast.Anchor --> otTables.Anchor"""
if anchor is None:
return None
variable = False
deviceX, deviceY = None, None
if anchor.xDeviceTable is not None:
deviceX = otl.buildDevice(dict(anchor.xDeviceTable))
if anchor.yDeviceTable is not None:
deviceY = otl.buildDevice(dict(anchor.yDeviceTable))
return otl.buildAnchor(anchor.x, anchor.y, anchor.contourpoint, deviceX, deviceY)
for dim in ("x", "y"):
if not isinstance(getattr(anchor, dim), VariableScalar):
continue
if getattr(anchor, dim+"DeviceTable") is not None:
raise FeatureLibError("Can't define a device coordinate and variable scalar", location)
if not self.varstorebuilder:
raise FeatureLibError("Can't define a variable scalar in a non-variable font", location)
varscalar = getattr(anchor,dim)
varscalar.axes = self.axes
default, index = varscalar.add_to_variation_store(self.varstorebuilder)
setattr(anchor, dim, default)
if index is not None and index != 0xFFFFFFFF:
if dim == "x":
deviceX = buildVarDevTable(index)
else:
deviceY = buildVarDevTable(index)
variable = True
otlanchor = otl.buildAnchor(anchor.x, anchor.y, anchor.contourpoint, deviceX, deviceY)
if variable:
otlanchor.Format = 3
return otlanchor
_VALUEREC_ATTRS = {
_VALUEREC_ATTRS = {
name[0].lower() + name[1:]: (name, isDevice)
for _, name, isDevice, _ in otBase.valueRecordFormat
if not name.startswith("Reserved")
}
}
def makeOpenTypeValueRecord(v, pairPosContext):
def makeOpenTypeValueRecord(self, location, v, pairPosContext):
"""ast.ValueRecord --> otBase.ValueRecord"""
if not v:
return None
vr = {}
for astName, (otName, isDevice) in _VALUEREC_ATTRS.items():
variable = False
for astName, (otName, isDevice) in self._VALUEREC_ATTRS.items():
val = getattr(v, astName, None)
if val:
vr[otName] = otl.buildDevice(dict(val)) if isDevice else val
if not val:
continue
if isDevice:
vr[otName] = otl.buildDevice(dict(val))
elif isinstance(val, VariableScalar):
otDeviceName = otName[0:4] + "Device"
feaDeviceName = otDeviceName[0].lower() + otDeviceName[1:]
if getattr(v, feaDeviceName):
raise FeatureLibError("Can't define a device coordinate and variable scalar", location)
if not self.varstorebuilder:
raise FeatureLibError("Can't define a variable scalar in a non-variable font", location)
val.axes = self.axes
default, index = val.add_to_variation_store(self.varstorebuilder)
vr[otName] = default
if index is not None and index != 0xFFFFFFFF:
vr[otDeviceName] = buildVarDevTable(index)
variable = True
else:
vr[otName] = val
if pairPosContext and not vr:
vr = {"YAdvance": 0} if v.vertical else {"XAdvance": 0}
valRec = otl.buildValue(vr)

View File

@ -1,5 +1,6 @@
from fontTools.feaLib.error import FeatureLibError
from fontTools.feaLib.lexer import Lexer, IncludingLexer, NonIncludingLexer
from fontTools.feaLib.variableScalar import VariableScalar
from fontTools.misc.encodingTools import getEncoding
from fontTools.misc.textTools import bytechr, tobytes, tostr
import fontTools.feaLib.ast as ast
@ -101,6 +102,10 @@ class Parser(object):
statements.append(self.parse_markClass_())
elif self.is_cur_keyword_("feature"):
statements.append(self.parse_feature_block_())
elif self.is_cur_keyword_("conditionset"):
statements.append(self.parse_conditionset_())
elif self.is_cur_keyword_("variation"):
statements.append(self.parse_feature_block_(variation=True))
elif self.is_cur_keyword_("table"):
statements.append(self.parse_table_())
elif self.is_cur_keyword_("valueRecordDef"):
@ -152,7 +157,7 @@ class Parser(object):
location=location,
)
x, y = self.expect_number_(), self.expect_number_()
x, y = self.expect_number_(variable=True), self.expect_number_(variable=True)
contourpoint = None
if self.next_token_ == "contourpoint": # Format B
@ -380,8 +385,7 @@ class Parser(object):
self.expect_symbol_("-")
range_end = self.expect_cid_()
self.check_glyph_name_in_glyph_set(
f"cid{range_start:05d}",
f"cid{range_end:05d}",
f"cid{range_start:05d}", f"cid{range_end:05d}",
)
glyphs.add_cid_range(
range_start,
@ -477,7 +481,7 @@ class Parser(object):
raise FeatureLibError(
"Positioning cannot be applied in the bactrack glyph sequence, "
"before the marked glyph sequence.",
self.cur_token_location_
self.cur_token_location_,
)
marked_values = values[len(prefix) : len(prefix) + len(glyphs)]
if any(marked_values):
@ -486,7 +490,7 @@ class Parser(object):
"Positioning values are allowed only in the marked glyph "
"sequence, or after the final glyph node when only one glyph "
"node is marked.",
self.cur_token_location_
self.cur_token_location_,
)
values = marked_values
elif values and values[-1]:
@ -495,7 +499,7 @@ class Parser(object):
"Positioning values are allowed only in the marked glyph "
"sequence, or after the final glyph node when only one glyph "
"node is marked.",
self.cur_token_location_
self.cur_token_location_,
)
values = values[-1:]
elif any(values):
@ -503,7 +507,7 @@ class Parser(object):
"Positioning values are allowed only in the marked glyph "
"sequence, or after the final glyph node when only one glyph "
"node is marked.",
self.cur_token_location_
self.cur_token_location_,
)
return (prefix, glyphs, lookups, values, suffix, hasMarks)
@ -1005,8 +1009,8 @@ class Parser(object):
location = self.cur_token_location_
DesignSize = self.expect_decipoint_()
SubfamilyID = self.expect_number_()
RangeStart = 0.
RangeEnd = 0.
RangeStart = 0.0
RangeEnd = 0.0
if self.next_token_type_ in (Lexer.NUMBER, Lexer.FLOAT) or SubfamilyID != 0:
RangeStart = self.expect_decipoint_()
RangeEnd = self.expect_decipoint_()
@ -1585,11 +1589,20 @@ class Parser(object):
return result
def is_next_value_(self):
return self.next_token_type_ is Lexer.NUMBER or self.next_token_ == "<"
return (
self.next_token_type_ is Lexer.NUMBER
or self.next_token_ == "<"
or self.next_token_ == "("
)
def parse_valuerecord_(self, vertical):
if self.next_token_type_ is Lexer.NUMBER:
number, location = self.expect_number_(), self.cur_token_location_
if (
self.next_token_type_ is Lexer.SYMBOL and self.next_token_ == "("
) or self.next_token_type_ is Lexer.NUMBER:
number, location = (
self.expect_number_(variable=True),
self.cur_token_location_,
)
if vertical:
val = self.ast.ValueRecord(
yAdvance=number, vertical=vertical, location=location
@ -1616,10 +1629,10 @@ class Parser(object):
xAdvance, yAdvance = (value.xAdvance, value.yAdvance)
else:
xPlacement, yPlacement, xAdvance, yAdvance = (
self.expect_number_(),
self.expect_number_(),
self.expect_number_(),
self.expect_number_(),
self.expect_number_(variable=True),
self.expect_number_(variable=True),
self.expect_number_(variable=True),
self.expect_number_(variable=True),
)
if self.next_token_ == "<":
@ -1679,7 +1692,10 @@ class Parser(object):
self.expect_symbol_(";")
return self.ast.LanguageSystemStatement(script, language, location=location)
def parse_feature_block_(self):
def parse_feature_block_(self, variation=False):
if variation:
assert self.cur_token_ == "variation"
else:
assert self.cur_token_ == "feature"
location = self.cur_token_location_
tag = self.expect_tag_()
@ -1695,11 +1711,19 @@ class Parser(object):
elif tag == "size":
size_feature = True
if variation:
conditionset = self.expect_name_()
use_extension = False
if self.next_token_ == "useExtension":
self.expect_keyword_("useExtension")
use_extension = True
if variation:
block = self.ast.VariationBlock(
tag, conditionset, use_extension=use_extension, location=location
)
else:
block = self.ast.FeatureBlock(
tag, use_extension=use_extension, location=location
)
@ -1850,6 +1874,43 @@ class Parser(object):
raise FeatureLibError("Font revision numbers must be positive", location)
return self.ast.FontRevisionStatement(version, location=location)
def parse_conditionset_(self):
name = self.expect_name_()
conditions = {}
self.expect_symbol_("{")
while self.next_token_ != "}":
self.advance_lexer_()
if self.cur_token_type_ is not Lexer.NAME:
raise FeatureLibError("Expected an axis name", self.cur_token_location_)
axis = self.cur_token_
if axis in conditions:
raise FeatureLibError(
f"Repeated condition for axis {axis}", self.cur_token_location_
)
if self.next_token_type_ is Lexer.FLOAT:
min_value = self.expect_float_()
elif self.next_token_type_ is Lexer.NUMBER:
min_value = self.expect_number_(variable=False)
if self.next_token_type_ is Lexer.FLOAT:
max_value = self.expect_float_()
elif self.next_token_type_ is Lexer.NUMBER:
max_value = self.expect_number_(variable=False)
self.expect_symbol_(";")
conditions[axis] = (min_value, max_value)
self.expect_symbol_("}")
finalname = self.expect_name_()
if finalname != name:
raise FeatureLibError('Expected "%s"' % name, self.cur_token_location_)
return self.ast.ConditionsetStatement(name, conditions)
def parse_block_(
self, block, vertical, stylisticset=None, size_feature=False, cv_feature=None
):
@ -2080,12 +2141,51 @@ class Parser(object):
return self.cur_token_
raise FeatureLibError("Expected a name", self.cur_token_location_)
def expect_number_(self):
def expect_number_(self, variable=False):
self.advance_lexer_()
if self.cur_token_type_ is Lexer.NUMBER:
return self.cur_token_
if variable and self.cur_token_type_ is Lexer.SYMBOL and self.cur_token_ == "(":
return self.expect_variable_scalar_()
raise FeatureLibError("Expected a number", self.cur_token_location_)
def expect_variable_scalar_(self):
self.advance_lexer_() # "("
scalar = VariableScalar()
while True:
if self.cur_token_type_ == Lexer.SYMBOL and self.cur_token_ == ")":
break
location, value = self.expect_master_()
scalar.add_value(location, value)
return scalar
def expect_master_(self):
location = {}
while True:
if self.cur_token_type_ is not Lexer.NAME:
raise FeatureLibError("Expected an axis name", self.cur_token_location_)
axis = self.cur_token_
self.advance_lexer_()
if not (self.cur_token_type_ is Lexer.SYMBOL and self.cur_token_ == "="):
raise FeatureLibError(
"Expected an equals sign", self.cur_token_location_
)
value = self.expect_number_()
location[axis] = value
if self.next_token_type_ is Lexer.NAME and self.next_token_[0] == ":":
# Lexer has just read the value as a glyph name. We'll correct it later
break
self.advance_lexer_()
if not (self.cur_token_type_ is Lexer.SYMBOL and self.cur_token_ == ","):
raise FeatureLibError(
"Expected an comma or an equals sign", self.cur_token_location_
)
self.advance_lexer_()
self.advance_lexer_()
value = int(self.cur_token_[1:])
self.advance_lexer_()
return location, value
def expect_any_number_(self):
self.advance_lexer_()
if self.cur_token_type_ in Lexer.NUMBERS:

View File

@ -0,0 +1,97 @@
from fontTools.varLib.models import VariationModel, normalizeValue
def Location(loc):
return tuple(sorted(loc.items()))
class VariableScalar:
"""A scalar with different values at different points in the designspace."""
def __init__(self, location_value={}):
self.values = {}
self.axes = {}
for location, value in location_value.items():
self.add_value(location, value)
def __repr__(self):
items = []
for location, value in self.values.items():
loc = ",".join(["%s=%i" % (ax, loc) for ax, loc in location])
items.append("%s:%i" % (loc, value))
return "(" + (" ".join(items)) + ")"
@property
def does_vary(self):
values = list(self.values.values())
return any(v != values[0] for v in values[1:])
@property
def axes_dict(self):
if not self.axes:
raise ValueError(
".axes must be defined on variable scalar before interpolating"
)
return {ax.axisTag: ax for ax in self.axes}
def _normalized_location(self, location):
location = self.fix_location(location)
normalized_location = {}
for axtag in location.keys():
if axtag not in self.axes_dict:
raise ValueError("Unknown axis %s in %s" % (axtag, location))
axis = self.axes_dict[axtag]
normalized_location[axtag] = normalizeValue(
location[axtag], (axis.minValue, axis.defaultValue, axis.maxValue)
)
return Location(normalized_location)
def fix_location(self, location):
location = dict(location)
for tag, axis in self.axes_dict.items():
if tag not in location:
location[tag] = axis.defaultValue
return location
def add_value(self, location, value):
if self.axes:
location = self.fix_location(location)
self.values[Location(location)] = value
def fix_all_locations(self):
self.values = {
Location(self.fix_location(l)): v for l, v in self.values.items()
}
@property
def default(self):
self.fix_all_locations()
key = Location({ax.axisTag: ax.defaultValue for ax in self.axes})
if key not in self.values:
raise ValueError("Default value could not be found")
# I *guess* we could interpolate one, but I don't know how.
return self.values[key]
def value_at_location(self, location):
loc = location
if loc in self.values.keys():
return self.values[loc]
values = list(self.values.values())
return self.model.interpolateFromMasters(loc, values)
@property
def model(self):
locations = [dict(self._normalized_location(k)) for k in self.values.keys()]
return VariationModel(locations)
def get_deltas_and_supports(self):
values = list(self.values.values())
return self.model.getDeltasAndSupports(values)
def add_to_variation_store(self, store_builder):
deltas, supports = self.get_deltas_and_supports()
store_builder.setSupports(supports)
index = store_builder.storeDeltas(deltas)
return int(self.default), index

View File

@ -959,12 +959,18 @@ class MarkBasePosBuilder(LookupBuilder):
positioning lookup.
"""
markClasses = self.buildMarkClasses_(self.marks)
marks = {
mark: (markClasses[mc], anchor) for mark, (mc, anchor) in self.marks.items()
}
marks = {}
for mark, (mc, anchor) in self.marks.items():
if mc not in markClasses:
raise ValueError("Mark class %s not found for mark glyph %s" % (mc, mark))
marks[mark] = (markClasses[mc], anchor)
bases = {}
for glyph, anchors in self.bases.items():
bases[glyph] = {markClasses[mc]: anchor for (mc, anchor) in anchors.items()}
bases[glyph] = {}
for mc, anchor in anchors.items():
if mc not in markClasses:
raise ValueError("Mark class %s not found for base glyph %s" % (mc, mark))
bases[glyph][markClasses[mc]] = anchor
subtables = buildMarkBasePos(marks, bases, self.glyphMap)
return self.buildLookup_(subtables)

View File

@ -44,8 +44,26 @@ def addFeatureVariations(font, conditionalSubstitutions, featureTag='rvrn'):
# >>> f.save(dstPath)
"""
addFeatureVariationsRaw(font,
overlayFeatureVariations(conditionalSubstitutions),
substitutions = overlayFeatureVariations(conditionalSubstitutions)
# turn substitution dicts into tuples of tuples, so they are hashable
conditionalSubstitutions, allSubstitutions = makeSubstitutionsHashable(substitutions)
if "GSUB" not in font:
font["GSUB"] = buildGSUB()
# setup lookups
lookupMap = buildSubstitutionLookups(font["GSUB"].table, allSubstitutions)
# addFeatureVariationsRaw takes a list of
# ( {condition}, [ lookup indices ] )
# so rearrange our lookups to match
conditionsAndLookups = []
for conditionSet, substitutions in conditionalSubstitutions:
conditionsAndLookups.append((conditionSet, [lookupMap[s] for s in substitutions]))
addFeatureVariationsRaw(font, font["GSUB"].table,
conditionsAndLookups,
featureTag)
def overlayFeatureVariations(conditionalSubstitutions):
@ -261,7 +279,7 @@ def cleanupBox(box):
# Low level implementation
#
def addFeatureVariationsRaw(font, conditionalSubstitutions, featureTag='rvrn'):
def addFeatureVariationsRaw(font, table, conditionalSubstitutions, featureTag='rvrn'):
"""Low level implementation of addFeatureVariations that directly
models the possibilities of the FeatureVariations table."""
@ -273,31 +291,25 @@ def addFeatureVariationsRaw(font, conditionalSubstitutions, featureTag='rvrn'):
# make lookups
# add feature variations
#
if table.Version < 0x00010001:
table.Version = 0x00010001 # allow table.FeatureVariations
if "GSUB" not in font:
font["GSUB"] = buildGSUB()
gsub = font["GSUB"].table
if gsub.Version < 0x00010001:
gsub.Version = 0x00010001 # allow gsub.FeatureVariations
gsub.FeatureVariations = None # delete any existing FeatureVariations
table.FeatureVariations = None # delete any existing FeatureVariations
varFeatureIndices = []
for index, feature in enumerate(gsub.FeatureList.FeatureRecord):
for index, feature in enumerate(table.FeatureList.FeatureRecord):
if feature.FeatureTag == featureTag:
varFeatureIndices.append(index)
if not varFeatureIndices:
varFeature = buildFeatureRecord(featureTag, [])
gsub.FeatureList.FeatureRecord.append(varFeature)
gsub.FeatureList.FeatureCount = len(gsub.FeatureList.FeatureRecord)
table.FeatureList.FeatureRecord.append(varFeature)
table.FeatureList.FeatureCount = len(table.FeatureList.FeatureRecord)
sortFeatureList(gsub)
varFeatureIndex = gsub.FeatureList.FeatureRecord.index(varFeature)
sortFeatureList(table)
varFeatureIndex = table.FeatureList.FeatureRecord.index(varFeature)
for scriptRecord in gsub.ScriptList.ScriptRecord:
for scriptRecord in table.ScriptList.ScriptRecord:
if scriptRecord.Script.DefaultLangSys is None:
raise VarLibError(
"Feature variations require that the script "
@ -309,17 +321,10 @@ def addFeatureVariationsRaw(font, conditionalSubstitutions, featureTag='rvrn'):
varFeatureIndices = [varFeatureIndex]
# setup lookups
# turn substitution dicts into tuples of tuples, so they are hashable
conditionalSubstitutions, allSubstitutions = makeSubstitutionsHashable(conditionalSubstitutions)
lookupMap = buildSubstitutionLookups(gsub, allSubstitutions)
axisIndices = {axis.axisTag: axisIndex for axisIndex, axis in enumerate(font["fvar"].axes)}
featureVariationRecords = []
for conditionSet, substitutions in conditionalSubstitutions:
for conditionSet, lookupIndices in conditionalSubstitutions:
conditionTable = []
for axisTag, (minValue, maxValue) in sorted(conditionSet.items()):
if minValue > maxValue:
@ -328,15 +333,13 @@ def addFeatureVariationsRaw(font, conditionalSubstitutions, featureTag='rvrn'):
)
ct = buildConditionTable(axisIndices[axisTag], minValue, maxValue)
conditionTable.append(ct)
lookupIndices = [lookupMap[subst] for subst in substitutions]
records = []
for varFeatureIndex in varFeatureIndices:
existingLookupIndices = gsub.FeatureList.FeatureRecord[varFeatureIndex].Feature.LookupListIndex
existingLookupIndices = table.FeatureList.FeatureRecord[varFeatureIndex].Feature.LookupListIndex
records.append(buildFeatureTableSubstitutionRecord(varFeatureIndex, existingLookupIndices + lookupIndices))
featureVariationRecords.append(buildFeatureVariationRecord(conditionTable, records))
gsub.FeatureVariations = buildFeatureVariations(featureVariationRecords)
table.FeatureVariations = buildFeatureVariations(featureVariationRecords)
#
@ -413,6 +416,7 @@ def buildFeatureVariations(featureVariationRecords):
fv = ot.FeatureVariations()
fv.Version = 0x00010000
fv.FeatureVariationRecord = featureVariationRecords
fv.FeatureVariationCount = len(featureVariationRecords)
return fv
@ -431,9 +435,11 @@ def buildFeatureVariationRecord(conditionTable, substitutionRecords):
fvr = ot.FeatureVariationRecord()
fvr.ConditionSet = ot.ConditionSet()
fvr.ConditionSet.ConditionTable = conditionTable
fvr.ConditionSet.ConditionCount = len(conditionTable)
fvr.FeatureTableSubstitution = ot.FeatureTableSubstitution()
fvr.FeatureTableSubstitution.Version = 0x00010000
fvr.FeatureTableSubstitution.SubstitutionRecord = substitutionRecords
fvr.FeatureTableSubstitution.SubstitutionCount = len(substitutionRecords)
return fvr
@ -443,6 +449,7 @@ def buildFeatureTableSubstitutionRecord(featureIndex, lookupListIndices):
ftsr.FeatureIndex = featureIndex
ftsr.Feature = ot.Feature()
ftsr.Feature.LookupListIndex = lookupListIndices
ftsr.Feature.LookupCount = len(lookupListIndices)
return ftsr

View File

@ -1,11 +1,15 @@
from fontTools.misc.loggingTools import CapturingLogHandler
from fontTools.feaLib.builder import Builder, addOpenTypeFeatures, \
addOpenTypeFeaturesFromString
from fontTools.feaLib.builder import (
Builder,
addOpenTypeFeatures,
addOpenTypeFeaturesFromString,
)
from fontTools.feaLib.error import FeatureLibError
from fontTools.ttLib import TTFont
from fontTools.ttLib import TTFont, newTable
from fontTools.feaLib.parser import Parser
from fontTools.feaLib import ast
from fontTools.feaLib.lexer import Lexer
from fontTools.fontBuilder import addFvar
import difflib
from io import StringIO
import os
@ -75,8 +79,14 @@ class BuilderTest(unittest.TestCase):
SingleSubstSubtable aalt_chain_contextual_subst AlternateChained
MultipleLookupsPerGlyph MultipleLookupsPerGlyph2 GSUB_6_formats
GSUB_5_formats delete_glyph STAT_test STAT_test_elidedFallbackNameID
variable_scalar_valuerecord variable_scalar_anchor variable_conditionset
""".split()
VARFONT_AXES = [
("wght", 200, 200, 1000, "Weight"),
("wdth", 100, 100, 200, "Width"),
]
def __init__(self, methodName):
unittest.TestCase.__init__(self, methodName)
# Python 3 renamed assertRaisesRegexp to assertRaisesRegex,
@ -101,8 +111,7 @@ class BuilderTest(unittest.TestCase):
if not self.tempdir:
self.tempdir = tempfile.mkdtemp()
self.num_tempfiles += 1
return os.path.join(self.tempdir,
"tmp%d%s" % (self.num_tempfiles, suffix))
return os.path.join(self.tempdir, "tmp%d%s" % (self.num_tempfiles, suffix))
def read_ttx(self, path):
lines = []
@ -117,8 +126,21 @@ class BuilderTest(unittest.TestCase):
def expect_ttx(self, font, expected_ttx, replace=None):
path = self.temp_path(suffix=".ttx")
font.saveXML(path, tables=['head', 'name', 'BASE', 'GDEF', 'GSUB',
'GPOS', 'OS/2', 'STAT', 'hhea', 'vhea'])
font.saveXML(
path,
tables=[
"head",
"name",
"BASE",
"GDEF",
"GSUB",
"GPOS",
"OS/2",
"STAT",
"hhea",
"vhea",
],
)
actual = self.read_ttx(path)
expected = self.read_ttx(expected_ttx)
if replace:
@ -127,7 +149,8 @@ class BuilderTest(unittest.TestCase):
expected[i] = expected[i].replace(k, v)
if actual != expected:
for line in difflib.unified_diff(
expected, actual, fromfile=expected_ttx, tofile=path):
expected, actual, fromfile=expected_ttx, tofile=path
):
sys.stderr.write(line)
self.fail("TTX output is different from expected")
@ -138,13 +161,17 @@ class BuilderTest(unittest.TestCase):
def check_feature_file(self, name):
font = makeTTFont()
if name.startswith("variable_"):
font["name"] = newTable("name")
addFvar(font, self.VARFONT_AXES, [])
del font["name"]
feapath = self.getpath("%s.fea" % name)
addOpenTypeFeatures(font, feapath)
self.expect_ttx(font, self.getpath("%s.ttx" % name))
# Check that:
# 1) tables do compile (only G* tables as long as we have a mock font)
# 2) dumping after save-reload yields the same TTX dump as before
for tag in ('GDEF', 'GSUB', 'GPOS'):
for tag in ("GDEF", "GSUB", "GPOS"):
if tag in font:
data = font[tag].compile(font)
font[tag].decompile(data, font)
@ -153,11 +180,11 @@ class BuilderTest(unittest.TestCase):
debugttx = self.getpath("%s-debug.ttx" % name)
if os.path.exists(debugttx):
addOpenTypeFeatures(font, feapath, debug=True)
self.expect_ttx(font, debugttx, replace = {"__PATH__": feapath})
self.expect_ttx(font, debugttx, replace={"__PATH__": feapath})
def check_fea2fea_file(self, name, base=None, parser=Parser):
font = makeTTFont()
fname = (name + ".fea") if '.' not in name else name
fname = (name + ".fea") if "." not in name else name
p = parser(self.getpath(fname), glyphNames=font.getGlyphOrder())
doc = p.parse()
actual = self.normal_fea(doc.asFea().split("\n"))
@ -167,12 +194,16 @@ class BuilderTest(unittest.TestCase):
if expected != actual:
fname = name.rsplit(".", 1)[0] + ".fea"
for line in difflib.unified_diff(
expected, actual,
expected,
actual,
fromfile=fname + " (expected)",
tofile=fname + " (actual)"):
sys.stderr.write(line+"\n")
self.fail("Fea2Fea output is different from expected. "
"Generated:\n{}\n".format("\n".join(actual)))
tofile=fname + " (actual)",
):
sys.stderr.write(line + "\n")
self.fail(
"Fea2Fea output is different from expected. "
"Generated:\n{}\n".format("\n".join(actual))
)
def normal_fea(self, lines):
output = []
@ -197,13 +228,14 @@ class BuilderTest(unittest.TestCase):
def test_alternateSubst_multipleSubstitutionsForSameGlyph(self):
self.assertRaisesRegex(
FeatureLibError,
"Already defined alternates for glyph \"A\"",
'Already defined alternates for glyph "A"',
self.build,
"feature test {"
" sub A from [A.alt1 A.alt2];"
" sub B from [B.alt1 B.alt2 B.alt3];"
" sub A from [A.alt1 A.alt2];"
"} test;")
"} test;",
)
def test_singleSubst_multipleIdenticalSubstitutionsForSameGlyph_info(self):
logger = logging.getLogger("fontTools.feaLib.builder")
@ -213,19 +245,23 @@ class BuilderTest(unittest.TestCase):
" sub A by A.sc;"
" sub B by B.sc;"
" sub A by A.sc;"
"} test;")
captor.assertRegex('Removing duplicate single substitution from glyph "A" to "A.sc"')
"} test;"
)
captor.assertRegex(
'Removing duplicate single substitution from glyph "A" to "A.sc"'
)
def test_multipleSubst_multipleSubstitutionsForSameGlyph(self):
self.assertRaisesRegex(
FeatureLibError,
"Already defined substitution for glyph \"f_f_i\"",
'Already defined substitution for glyph "f_f_i"',
self.build,
"feature test {"
" sub f_f_i by f f i;"
" sub c_t by c t;"
" sub f_f_i by f_f i;"
"} test;")
"} test;",
)
def test_multipleSubst_multipleIdenticalSubstitutionsForSameGlyph_info(self):
logger = logging.getLogger("fontTools.feaLib.builder")
@ -235,8 +271,11 @@ class BuilderTest(unittest.TestCase):
" sub f_f_i by f f i;"
" sub c_t by c t;"
" sub f_f_i by f f i;"
"} test;")
captor.assertRegex(r"Removing duplicate multiple substitution from glyph \"f_f_i\" to \('f', 'f', 'i'\)")
"} test;"
)
captor.assertRegex(
r"Removing duplicate multiple substitution from glyph \"f_f_i\" to \('f', 'f', 'i'\)"
)
def test_pairPos_redefinition_warning(self):
# https://github.com/fonttools/fonttools/issues/1147
@ -250,17 +289,18 @@ class BuilderTest(unittest.TestCase):
" pos yacute semicolon -70;"
" enum pos @Y_LC semicolon -80;"
" pos @Y_LC @SMALL_PUNC -100;"
"} kern;")
"} kern;"
)
captor.assertRegex("Already defined position for pair yacute semicolon")
# the first definition prevails: yacute semicolon -70
st = font["GPOS"].table.LookupList.Lookup[0].SubTable[0]
self.assertEqual(st.Coverage.glyphs[2], "yacute")
self.assertEqual(st.PairSet[2].PairValueRecord[0].SecondGlyph,
"semicolon")
self.assertEqual(vars(st.PairSet[2].PairValueRecord[0].Value1),
{"XAdvance": -70})
self.assertEqual(st.PairSet[2].PairValueRecord[0].SecondGlyph, "semicolon")
self.assertEqual(
vars(st.PairSet[2].PairValueRecord[0].Value1), {"XAdvance": -70}
)
def test_singleSubst_multipleSubstitutionsForSameGlyph(self):
self.assertRaisesRegex(
@ -270,127 +310,153 @@ class BuilderTest(unittest.TestCase):
"feature test {"
" sub [a-z] by [A.sc-Z.sc];"
" sub e by e.fina;"
"} test;")
"} test;",
)
def test_singlePos_redefinition(self):
self.assertRaisesRegex(
FeatureLibError,
"Already defined different position for glyph \"A\"",
self.build, "feature test { pos A 123; pos A 456; } test;")
'Already defined different position for glyph "A"',
self.build,
"feature test { pos A 123; pos A 456; } test;",
)
def test_feature_outside_aalt(self):
self.assertRaisesRegex(
FeatureLibError,
'Feature references are only allowed inside "feature aalt"',
self.build, "feature test { feature test; } test;")
self.build,
"feature test { feature test; } test;",
)
def test_feature_undefinedReference(self):
self.assertRaisesRegex(
FeatureLibError, 'Feature none has not been defined',
self.build, "feature aalt { feature none; } aalt;")
FeatureLibError,
"Feature none has not been defined",
self.build,
"feature aalt { feature none; } aalt;",
)
def test_GlyphClassDef_conflictingClasses(self):
self.assertRaisesRegex(
FeatureLibError, "Glyph X was assigned to a different class",
FeatureLibError,
"Glyph X was assigned to a different class",
self.build,
"table GDEF {"
" GlyphClassDef [a b], [X], , ;"
" GlyphClassDef [a b X], , , ;"
"} GDEF;")
"} GDEF;",
)
def test_languagesystem(self):
builder = Builder(makeTTFont(), (None, None))
builder.add_language_system(None, 'latn', 'FRA')
builder.add_language_system(None, 'cyrl', 'RUS')
builder.start_feature(location=None, name='test')
self.assertEqual(builder.language_systems,
{('latn', 'FRA'), ('cyrl', 'RUS')})
builder.add_language_system(None, "latn", "FRA")
builder.add_language_system(None, "cyrl", "RUS")
builder.start_feature(location=None, name="test")
self.assertEqual(builder.language_systems, {("latn", "FRA"), ("cyrl", "RUS")})
def test_languagesystem_duplicate(self):
self.assertRaisesRegex(
FeatureLibError,
'"languagesystem cyrl RUS" has already been specified',
self.build, "languagesystem cyrl RUS; languagesystem cyrl RUS;")
self.build,
"languagesystem cyrl RUS; languagesystem cyrl RUS;",
)
def test_languagesystem_none_specified(self):
builder = Builder(makeTTFont(), (None, None))
builder.start_feature(location=None, name='test')
self.assertEqual(builder.language_systems, {('DFLT', 'dflt')})
builder.start_feature(location=None, name="test")
self.assertEqual(builder.language_systems, {("DFLT", "dflt")})
def test_languagesystem_DFLT_dflt_not_first(self):
self.assertRaisesRegex(
FeatureLibError,
"If \"languagesystem DFLT dflt\" is present, "
'If "languagesystem DFLT dflt" is present, '
"it must be the first of the languagesystem statements",
self.build, "languagesystem latn TRK; languagesystem DFLT dflt;")
self.build,
"languagesystem latn TRK; languagesystem DFLT dflt;",
)
def test_languagesystem_DFLT_not_preceding(self):
self.assertRaisesRegex(
FeatureLibError,
"languagesystems using the \"DFLT\" script tag must "
'languagesystems using the "DFLT" script tag must '
"precede all other languagesystems",
self.build,
"languagesystem DFLT dflt; "
"languagesystem latn dflt; "
"languagesystem DFLT fooo; "
"languagesystem DFLT fooo; ",
)
def test_script(self):
builder = Builder(makeTTFont(), (None, None))
builder.start_feature(location=None, name='test')
builder.set_script(location=None, script='cyrl')
self.assertEqual(builder.language_systems, {('cyrl', 'dflt')})
builder.start_feature(location=None, name="test")
builder.set_script(location=None, script="cyrl")
self.assertEqual(builder.language_systems, {("cyrl", "dflt")})
def test_script_in_aalt_feature(self):
self.assertRaisesRegex(
FeatureLibError,
"Script statements are not allowed within \"feature aalt\"",
self.build, "feature aalt { script latn; } aalt;")
'Script statements are not allowed within "feature aalt"',
self.build,
"feature aalt { script latn; } aalt;",
)
def test_script_in_size_feature(self):
self.assertRaisesRegex(
FeatureLibError,
"Script statements are not allowed within \"feature size\"",
self.build, "feature size { script latn; } size;")
'Script statements are not allowed within "feature size"',
self.build,
"feature size { script latn; } size;",
)
def test_script_in_standalone_lookup(self):
self.assertRaisesRegex(
FeatureLibError,
"Script statements are not allowed within standalone lookup blocks",
self.build, "lookup test { script latn; } test;")
self.build,
"lookup test { script latn; } test;",
)
def test_language(self):
builder = Builder(makeTTFont(), (None, None))
builder.add_language_system(None, 'latn', 'FRA ')
builder.start_feature(location=None, name='test')
builder.set_script(location=None, script='cyrl')
builder.set_language(location=None, language='RUS ',
include_default=False, required=False)
self.assertEqual(builder.language_systems, {('cyrl', 'RUS ')})
builder.set_language(location=None, language='BGR ',
include_default=True, required=False)
self.assertEqual(builder.language_systems,
{('cyrl', 'BGR ')})
builder.start_feature(location=None, name='test2')
self.assertEqual(builder.language_systems, {('latn', 'FRA ')})
builder.add_language_system(None, "latn", "FRA ")
builder.start_feature(location=None, name="test")
builder.set_script(location=None, script="cyrl")
builder.set_language(
location=None, language="RUS ", include_default=False, required=False
)
self.assertEqual(builder.language_systems, {("cyrl", "RUS ")})
builder.set_language(
location=None, language="BGR ", include_default=True, required=False
)
self.assertEqual(builder.language_systems, {("cyrl", "BGR ")})
builder.start_feature(location=None, name="test2")
self.assertEqual(builder.language_systems, {("latn", "FRA ")})
def test_language_in_aalt_feature(self):
self.assertRaisesRegex(
FeatureLibError,
"Language statements are not allowed within \"feature aalt\"",
self.build, "feature aalt { language FRA; } aalt;")
'Language statements are not allowed within "feature aalt"',
self.build,
"feature aalt { language FRA; } aalt;",
)
def test_language_in_size_feature(self):
self.assertRaisesRegex(
FeatureLibError,
"Language statements are not allowed within \"feature size\"",
self.build, "feature size { language FRA; } size;")
'Language statements are not allowed within "feature size"',
self.build,
"feature size { language FRA; } size;",
)
def test_language_in_standalone_lookup(self):
self.assertRaisesRegex(
FeatureLibError,
"Language statements are not allowed within standalone lookup blocks",
self.build, "lookup test { language FRA; } test;")
self.build,
"lookup test { language FRA; } test;",
)
def test_language_required_duplicate(self):
self.assertRaisesRegex(
@ -408,13 +474,16 @@ class BuilderTest(unittest.TestCase):
" script latn;"
" language FRA required;"
" substitute [a-z] by [A.sc-Z.sc];"
"} test;")
"} test;",
)
def test_lookup_already_defined(self):
self.assertRaisesRegex(
FeatureLibError,
"Lookup \"foo\" has already been defined",
self.build, "lookup foo {} foo; lookup foo {} foo;")
'Lookup "foo" has already been defined',
self.build,
"lookup foo {} foo; lookup foo {} foo;",
)
def test_lookup_multiple_flags(self):
self.assertRaisesRegex(
@ -427,7 +496,8 @@ class BuilderTest(unittest.TestCase):
" sub f i by f_i;"
" lookupflag 2;"
" sub f f i by f_f_i;"
"} foo;")
"} foo;",
)
def test_lookup_multiple_types(self):
self.assertRaisesRegex(
@ -438,13 +508,16 @@ class BuilderTest(unittest.TestCase):
"lookup foo {"
" sub f f i by f_f_i;"
" sub A from [A.alt1 A.alt2];"
"} foo;")
"} foo;",
)
def test_lookup_inside_feature_aalt(self):
self.assertRaisesRegex(
FeatureLibError,
"Lookup blocks cannot be placed inside 'aalt' features",
self.build, "feature aalt {lookup L {} L;} aalt;")
self.build,
"feature aalt {lookup L {} L;} aalt;",
)
def test_chain_subst_refrences_GPOS_looup(self):
self.assertRaisesRegex(
@ -454,7 +527,7 @@ class BuilderTest(unittest.TestCase):
"lookup dummy { pos a 50; } dummy;"
"feature test {"
" sub a' lookup dummy b;"
"} test;"
"} test;",
)
def test_chain_pos_refrences_GSUB_looup(self):
@ -465,203 +538,215 @@ class BuilderTest(unittest.TestCase):
"lookup dummy { sub a by A; } dummy;"
"feature test {"
" pos a' lookup dummy b;"
"} test;"
"} test;",
)
def test_STAT_elidedfallbackname_already_defined(self):
self.assertRaisesRegex(
FeatureLibError,
'ElidedFallbackName is already set.',
"ElidedFallbackName is already set.",
self.build,
'table name {'
"table name {"
' nameid 256 "Roman"; '
'} name;'
'table STAT {'
"} name;"
"table STAT {"
' ElidedFallbackName { name "Roman"; };'
' ElidedFallbackNameID 256;'
'} STAT;')
" ElidedFallbackNameID 256;"
"} STAT;",
)
def test_STAT_elidedfallbackname_set_twice(self):
self.assertRaisesRegex(
FeatureLibError,
'ElidedFallbackName is already set.',
"ElidedFallbackName is already set.",
self.build,
'table name {'
"table name {"
' nameid 256 "Roman"; '
'} name;'
'table STAT {'
"} name;"
"table STAT {"
' ElidedFallbackName { name "Roman"; };'
' ElidedFallbackName { name "Italic"; };'
'} STAT;')
"} STAT;",
)
def test_STAT_elidedfallbacknameID_already_defined(self):
self.assertRaisesRegex(
FeatureLibError,
'ElidedFallbackNameID is already set.',
"ElidedFallbackNameID is already set.",
self.build,
'table name {'
"table name {"
' nameid 256 "Roman"; '
'} name;'
'table STAT {'
' ElidedFallbackNameID 256;'
"} name;"
"table STAT {"
" ElidedFallbackNameID 256;"
' ElidedFallbackName { name "Roman"; };'
'} STAT;')
"} STAT;",
)
def test_STAT_elidedfallbacknameID_not_in_name_table(self):
self.assertRaisesRegex(
FeatureLibError,
'ElidedFallbackNameID 256 points to a nameID that does not '
"ElidedFallbackNameID 256 points to a nameID that does not "
'exist in the "name" table',
self.build,
'table name {'
"table name {"
' nameid 257 "Roman"; '
'} name;'
'table STAT {'
' ElidedFallbackNameID 256;'
"} name;"
"table STAT {"
" ElidedFallbackNameID 256;"
' DesignAxis opsz 1 { name "Optical Size"; };'
'} STAT;')
"} STAT;",
)
def test_STAT_design_axis_name(self):
self.assertRaisesRegex(
FeatureLibError,
'Expected "name"',
self.build,
'table name {'
"table name {"
' nameid 256 "Roman"; '
'} name;'
'table STAT {'
"} name;"
"table STAT {"
' ElidedFallbackName { name "Roman"; };'
' DesignAxis opsz 0 { badtag "Optical Size"; };'
'} STAT;')
"} STAT;",
)
def test_STAT_duplicate_design_axis_name(self):
self.assertRaisesRegex(
FeatureLibError,
'DesignAxis already defined for tag "opsz".',
self.build,
'table name {'
"table name {"
' nameid 256 "Roman"; '
'} name;'
'table STAT {'
"} name;"
"table STAT {"
' ElidedFallbackName { name "Roman"; };'
' DesignAxis opsz 0 { name "Optical Size"; };'
' DesignAxis opsz 1 { name "Optical Size"; };'
'} STAT;')
"} STAT;",
)
def test_STAT_design_axis_duplicate_order(self):
self.assertRaisesRegex(
FeatureLibError,
"DesignAxis already defined for axis number 0.",
self.build,
'table name {'
"table name {"
' nameid 256 "Roman"; '
'} name;'
'table STAT {'
"} name;"
"table STAT {"
' ElidedFallbackName { name "Roman"; };'
' DesignAxis opsz 0 { name "Optical Size"; };'
' DesignAxis wdth 0 { name "Width"; };'
' AxisValue {'
' location opsz 8;'
' location wdth 400;'
" AxisValue {"
" location opsz 8;"
" location wdth 400;"
' name "Caption";'
' };'
'} STAT;')
" };"
"} STAT;",
)
def test_STAT_undefined_tag(self):
self.assertRaisesRegex(
FeatureLibError,
'DesignAxis not defined for wdth.',
"DesignAxis not defined for wdth.",
self.build,
'table name {'
"table name {"
' nameid 256 "Roman"; '
'} name;'
'table STAT {'
"} name;"
"table STAT {"
' ElidedFallbackName { name "Roman"; };'
' DesignAxis opsz 0 { name "Optical Size"; };'
' AxisValue { '
' location wdth 125; '
" AxisValue { "
" location wdth 125; "
' name "Wide"; '
' };'
'} STAT;')
" };"
"} STAT;",
)
def test_STAT_axis_value_format4(self):
self.assertRaisesRegex(
FeatureLibError,
'Axis tag wdth already defined.',
"Axis tag wdth already defined.",
self.build,
'table name {'
"table name {"
' nameid 256 "Roman"; '
'} name;'
'table STAT {'
"} name;"
"table STAT {"
' ElidedFallbackName { name "Roman"; };'
' DesignAxis opsz 0 { name "Optical Size"; };'
' DesignAxis wdth 1 { name "Width"; };'
' DesignAxis wght 2 { name "Weight"; };'
' AxisValue { '
' location opsz 8; '
' location wdth 125; '
' location wdth 125; '
' location wght 500; '
" AxisValue { "
" location opsz 8; "
" location wdth 125; "
" location wdth 125; "
" location wght 500; "
' name "Caption Medium Wide"; '
' };'
'} STAT;')
" };"
"} STAT;",
)
def test_STAT_duplicate_axis_value_record(self):
# Test for Duplicate AxisValueRecords even when the definition order
# is different.
self.assertRaisesRegex(
FeatureLibError,
'An AxisValueRecord with these values is already defined.',
"An AxisValueRecord with these values is already defined.",
self.build,
'table name {'
"table name {"
' nameid 256 "Roman"; '
'} name;'
'table STAT {'
"} name;"
"table STAT {"
' ElidedFallbackName { name "Roman"; };'
' DesignAxis opsz 0 { name "Optical Size"; };'
' DesignAxis wdth 1 { name "Width"; };'
' AxisValue {'
' location opsz 8;'
' location wdth 400;'
" AxisValue {"
" location opsz 8;"
" location wdth 400;"
' name "Caption";'
' };'
' AxisValue {'
' location wdth 400;'
' location opsz 8;'
" };"
" AxisValue {"
" location wdth 400;"
" location opsz 8;"
' name "Caption";'
' };'
'} STAT;')
" };"
"} STAT;",
)
def test_STAT_axis_value_missing_location(self):
self.assertRaisesRegex(
FeatureLibError,
'Expected "Axis location"',
self.build,
'table name {'
"table name {"
' nameid 256 "Roman"; '
'} name;'
'table STAT {'
"} name;"
"table STAT {"
' ElidedFallbackName { name "Roman"; '
'};'
"};"
' DesignAxis opsz 0 { name "Optical Size"; };'
' AxisValue { '
" AxisValue { "
' name "Wide"; '
' };'
'} STAT;')
" };"
"} STAT;",
)
def test_STAT_invalid_location_tag(self):
self.assertRaisesRegex(
FeatureLibError,
'Tags cannot be longer than 4 characters',
"Tags cannot be longer than 4 characters",
self.build,
'table name {'
"table name {"
' nameid 256 "Roman"; '
'} name;'
'table STAT {'
"} name;"
"table STAT {"
' ElidedFallbackName { name "Roman"; '
' name 3 1 0x0411 "ローマン"; }; '
' DesignAxis width 0 { name "Width"; };'
'} STAT;')
"} STAT;",
)
def test_extensions(self):
class ast_BaseClass(ast.MarkClass):
@ -679,7 +764,9 @@ class BuilderTest(unittest.TestCase):
for bcd in self.base.markClass.definitions:
if res != "":
res += "\n{}".format(indent)
res += "pos base {} {}".format(bcd.glyphs.asFea(), bcd.anchor.asFea())
res += "pos base {} {}".format(
bcd.glyphs.asFea(), bcd.anchor.asFea()
)
for m in self.marks:
res += " mark @{}".format(m.name)
res += ";"
@ -692,6 +779,7 @@ class BuilderTest(unittest.TestCase):
class testAst(object):
MarkBasePosStatement = ast_MarkBasePosStatement
def __getattr__(self, name):
return getattr(ast, name)
@ -702,8 +790,9 @@ class BuilderTest(unittest.TestCase):
if enumerated:
raise FeatureLibError(
'"enumerate" is not allowed with '
'mark-to-base attachment positioning',
location)
"mark-to-base attachment positioning",
location,
)
base = self.parse_glyphclass_(accept_glyphname=True)
if self.next_token_ == "<":
marks = self.parse_anchor_marks_()
@ -714,11 +803,10 @@ class BuilderTest(unittest.TestCase):
m = self.expect_markClass_reference_()
marks.append(m)
self.expect_symbol_(";")
return self.ast.MarkBasePosStatement(base, marks,
location=location)
return self.ast.MarkBasePosStatement(base, marks, location=location)
def parseBaseClass(self):
if not hasattr(self.doc_, 'baseClasses'):
if not hasattr(self.doc_, "baseClasses"):
self.doc_.baseClasses = {}
location = self.cur_token_location_
glyphs = self.parse_glyphclass_(accept_glyphname=True)
@ -730,37 +818,39 @@ class BuilderTest(unittest.TestCase):
baseClass = ast_BaseClass(name)
self.doc_.baseClasses[name] = baseClass
self.glyphclasses_.define(name, baseClass)
bcdef = ast_BaseClassDefinition(baseClass, anchor, glyphs,
location=location)
bcdef = ast_BaseClassDefinition(
baseClass, anchor, glyphs, location=location
)
baseClass.addDefinition(bcdef)
return bcdef
extensions = {
'baseClass' : lambda s : s.parseBaseClass()
}
extensions = {"baseClass": lambda s: s.parseBaseClass()}
ast = testAst()
self.check_fea2fea_file(
"baseClass.feax", base="baseClass.fea", parser=testParser)
"baseClass.feax", base="baseClass.fea", parser=testParser
)
def test_markClass_same_glyph_redefined(self):
self.assertRaisesRegex(
FeatureLibError,
"Glyph acute already defined",
self.build,
"markClass [acute] <anchor 350 0> @TOP_MARKS;"*2)
"markClass [acute] <anchor 350 0> @TOP_MARKS;" * 2,
)
def test_markClass_same_glyph_multiple_classes(self):
self.assertRaisesRegex(
FeatureLibError,
'Glyph uni0327 cannot be in both @ogonek and @cedilla',
"Glyph uni0327 cannot be in both @ogonek and @cedilla",
self.build,
"feature mark {"
" markClass [uni0327 uni0328] <anchor 0 0> @ogonek;"
" pos base [a] <anchor 399 0> mark @ogonek;"
" markClass [uni0327] <anchor 0 0> @cedilla;"
" pos base [a] <anchor 244 0> mark @cedilla;"
"} mark;")
"} mark;",
)
def test_build_specific_tables(self):
features = "feature liga {sub f i by f_i;} liga;"
@ -782,7 +872,7 @@ class BuilderTest(unittest.TestCase):
def test_unsupported_subtable_break(self):
logger = logging.getLogger("fontTools.otlLib.builder")
with CapturingLogHandler(logger, level='WARNING') as captor:
with CapturingLogHandler(logger, level="WARNING") as captor:
self.build(
"feature test {"
" pos a 10;"
@ -813,10 +903,8 @@ class BuilderTest(unittest.TestCase):
FeatureLibError,
"Already defined different position for glyph",
self.build,
"lookup foo {"
" pos A -45; "
" pos A 45; "
"} foo;")
"lookup foo {" " pos A -45; " " pos A 45; " "} foo;",
)
def test_pairPos_enumRuleOverridenBySinglePair_DEBUG(self):
logger = logging.getLogger("fontTools.otlLib.builder")
@ -825,14 +913,14 @@ class BuilderTest(unittest.TestCase):
"feature test {"
" enum pos A [V Y] -80;"
" pos A V -75;"
"} test;")
captor.assertRegex('Already defined position for pair A V at')
"} test;"
)
captor.assertRegex("Already defined position for pair A V at")
def test_ignore_empty_lookup_block(self):
# https://github.com/fonttools/fonttools/pull/2277
font = self.build(
"lookup EMPTY { ; } EMPTY;"
"feature ss01 { lookup EMPTY; } ss01;"
"lookup EMPTY { ; } EMPTY;" "feature ss01 { lookup EMPTY; } ss01;"
)
assert "GPOS" not in font
assert "GSUB" not in font
@ -843,8 +931,7 @@ def generate_feature_file_test(name):
for name in BuilderTest.TEST_FEATURE_FILES:
setattr(BuilderTest, "test_FeatureFile_%s" % name,
generate_feature_file_test(name))
setattr(BuilderTest, "test_FeatureFile_%s" % name, generate_feature_file_test(name))
def generate_fea2fea_file_test(name):
@ -852,8 +939,11 @@ def generate_fea2fea_file_test(name):
for name in BuilderTest.TEST_FEATURE_FILES:
setattr(BuilderTest, "test_Fea2feaFile_{}".format(name),
generate_fea2fea_file_test(name))
setattr(
BuilderTest,
"test_Fea2feaFile_{}".format(name),
generate_fea2fea_file_test(name),
)
if __name__ == "__main__":

View File

@ -0,0 +1,13 @@
languagesystem DFLT dflt;
lookup symbols_heavy {
sub a by b;
} symbols_heavy;
conditionset heavy {
wght 700 900;
} heavy;
variation rvrn heavy {
lookup symbols_heavy;
} rvrn;

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<ttFont>
<GSUB>
<Version value="0x00010001"/>
<ScriptList>
<!-- ScriptCount=1 -->
<ScriptRecord index="0">
<ScriptTag value="DFLT"/>
<Script>
<DefaultLangSys>
<ReqFeatureIndex value="65535"/>
<!-- FeatureCount=1 -->
<FeatureIndex index="0" value="0"/>
</DefaultLangSys>
<!-- LangSysCount=0 -->
</Script>
</ScriptRecord>
</ScriptList>
<FeatureList>
<!-- FeatureCount=1 -->
<FeatureRecord index="0">
<FeatureTag value="rvrn"/>
<Feature>
<!-- LookupCount=0 -->
</Feature>
</FeatureRecord>
</FeatureList>
<LookupList>
<!-- LookupCount=1 -->
<Lookup index="0">
<LookupType value="1"/>
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
<SingleSubst index="0">
<Substitution in="a" out="b"/>
</SingleSubst>
</Lookup>
</LookupList>
<FeatureVariations>
<Version value="0x00010000"/>
<!-- FeatureVariationCount=1 -->
<FeatureVariationRecord index="0">
<ConditionSet>
<!-- ConditionCount=1 -->
<ConditionTable index="0" Format="1">
<AxisIndex value="0"/>
<FilterRangeMinValue value="0.625"/>
<FilterRangeMaxValue value="0.875"/>
</ConditionTable>
</ConditionSet>
<FeatureTableSubstitution>
<Version value="0x00010000"/>
<!-- SubstitutionCount=1 -->
<SubstitutionRecord index="0">
<FeatureIndex value="0"/>
<Feature>
<!-- LookupCount=1 -->
<LookupListIndex index="0" value="0"/>
</Feature>
</SubstitutionRecord>
</FeatureTableSubstitution>
</FeatureVariationRecord>
</FeatureVariations>
</GSUB>
</ttFont>

View File

@ -0,0 +1,4 @@
languagesystem DFLT dflt;
feature kern {
pos cursive one <anchor 0 (wght=200:12 wght=900:22 wdth=150,wght=900:42)> <anchor NULL>;
} kern;

View File

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<ttFont>
<GDEF>
<Version value="0x00010003"/>
<VarStore Format="1">
<Format value="1"/>
<VarRegionList>
<!-- RegionAxisCount=2 -->
<!-- RegionCount=2 -->
<Region index="0">
<VarRegionAxis index="0">
<StartCoord value="0.0"/>
<PeakCoord value="0.875"/>
<EndCoord value="0.875"/>
</VarRegionAxis>
<VarRegionAxis index="1">
<StartCoord value="0.0"/>
<PeakCoord value="0.0"/>
<EndCoord value="0.0"/>
</VarRegionAxis>
</Region>
<Region index="1">
<VarRegionAxis index="0">
<StartCoord value="0.0"/>
<PeakCoord value="0.875"/>
<EndCoord value="0.875"/>
</VarRegionAxis>
<VarRegionAxis index="1">
<StartCoord value="0.0"/>
<PeakCoord value="0.5"/>
<EndCoord value="0.5"/>
</VarRegionAxis>
</Region>
</VarRegionList>
<!-- VarDataCount=1 -->
<VarData index="0">
<!-- ItemCount=1 -->
<NumShorts value="0"/>
<!-- VarRegionCount=2 -->
<VarRegionIndex index="0" value="0"/>
<VarRegionIndex index="1" value="1"/>
<Item index="0" value="[10, 20]"/>
</VarData>
</VarStore>
</GDEF>
<GPOS>
<Version value="0x00010000"/>
<ScriptList>
<!-- ScriptCount=1 -->
<ScriptRecord index="0">
<ScriptTag value="DFLT"/>
<Script>
<DefaultLangSys>
<ReqFeatureIndex value="65535"/>
<!-- FeatureCount=1 -->
<FeatureIndex index="0" value="0"/>
</DefaultLangSys>
<!-- LangSysCount=0 -->
</Script>
</ScriptRecord>
</ScriptList>
<FeatureList>
<!-- FeatureCount=1 -->
<FeatureRecord index="0">
<FeatureTag value="kern"/>
<Feature>
<!-- LookupCount=1 -->
<LookupListIndex index="0" value="0"/>
</Feature>
</FeatureRecord>
</FeatureList>
<LookupList>
<!-- LookupCount=1 -->
<Lookup index="0">
<LookupType value="3"/>
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
<CursivePos index="0" Format="1">
<Coverage>
<Glyph value="one"/>
</Coverage>
<!-- EntryExitCount=1 -->
<EntryExitRecord index="0">
<EntryAnchor Format="3">
<XCoordinate value="0"/>
<YCoordinate value="12"/>
<YDeviceTable>
<StartSize value="0"/>
<EndSize value="0"/>
<DeltaFormat value="32768"/>
</YDeviceTable>
</EntryAnchor>
</EntryExitRecord>
</CursivePos>
</Lookup>
</LookupList>
</GPOS>
</ttFont>

View File

@ -0,0 +1,5 @@
languagesystem DFLT dflt;
feature kern {
pos one 1;
pos two <0 (wght=200:12 wght=900:22 wdth=150,wght=900:42) 0 0>;
} kern;

View File

@ -0,0 +1,104 @@
<?xml version="1.0" encoding="UTF-8"?>
<ttFont>
<GDEF>
<Version value="0x00010003"/>
<VarStore Format="1">
<Format value="1"/>
<VarRegionList>
<!-- RegionAxisCount=2 -->
<!-- RegionCount=2 -->
<Region index="0">
<VarRegionAxis index="0">
<StartCoord value="0.0"/>
<PeakCoord value="0.875"/>
<EndCoord value="0.875"/>
</VarRegionAxis>
<VarRegionAxis index="1">
<StartCoord value="0.0"/>
<PeakCoord value="0.0"/>
<EndCoord value="0.0"/>
</VarRegionAxis>
</Region>
<Region index="1">
<VarRegionAxis index="0">
<StartCoord value="0.0"/>
<PeakCoord value="0.875"/>
<EndCoord value="0.875"/>
</VarRegionAxis>
<VarRegionAxis index="1">
<StartCoord value="0.0"/>
<PeakCoord value="0.5"/>
<EndCoord value="0.5"/>
</VarRegionAxis>
</Region>
</VarRegionList>
<!-- VarDataCount=1 -->
<VarData index="0">
<!-- ItemCount=1 -->
<NumShorts value="0"/>
<!-- VarRegionCount=2 -->
<VarRegionIndex index="0" value="0"/>
<VarRegionIndex index="1" value="1"/>
<Item index="0" value="[10, 20]"/>
</VarData>
</VarStore>
</GDEF>
<GPOS>
<Version value="0x00010000"/>
<ScriptList>
<!-- ScriptCount=1 -->
<ScriptRecord index="0">
<ScriptTag value="DFLT"/>
<Script>
<DefaultLangSys>
<ReqFeatureIndex value="65535"/>
<!-- FeatureCount=1 -->
<FeatureIndex index="0" value="0"/>
</DefaultLangSys>
<!-- LangSysCount=0 -->
</Script>
</ScriptRecord>
</ScriptList>
<FeatureList>
<!-- FeatureCount=1 -->
<FeatureRecord index="0">
<FeatureTag value="kern"/>
<Feature>
<!-- LookupCount=1 -->
<LookupListIndex index="0" value="0"/>
</Feature>
</FeatureRecord>
</FeatureList>
<LookupList>
<!-- LookupCount=1 -->
<Lookup index="0">
<LookupType value="1"/>
<LookupFlag value="0"/>
<!-- SubTableCount=2 -->
<SinglePos index="0" Format="1">
<Coverage>
<Glyph value="one"/>
</Coverage>
<ValueFormat value="4"/>
<Value XAdvance="1"/>
</SinglePos>
<SinglePos index="1" Format="1">
<Coverage>
<Glyph value="two"/>
</Coverage>
<ValueFormat value="34"/>
<Value YPlacement="12">
<YPlaDevice>
<StartSize value="0"/>
<EndSize value="0"/>
<DeltaFormat value="32768"/>
</YPlaDevice>
</Value>
</SinglePos>
</Lookup>
</LookupList>
</GPOS>
</ttFont>

File diff suppressed because it is too large Load Diff