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", "ChainContextPosStatement",
"ChainContextSubstStatement", "ChainContextSubstStatement",
"CharacterStatement", "CharacterStatement",
"ConditionsetStatement",
"CursivePosStatement", "CursivePosStatement",
"ElidedFallbackName", "ElidedFallbackName",
"ElidedFallbackNameID", "ElidedFallbackNameID",
@ -1261,11 +1262,21 @@ class MultipleSubstStatement(Statement):
if not self.replacement and hasattr(self.glyph, "glyphSet"): if not self.replacement and hasattr(self.glyph, "glyphSet"):
for glyph in self.glyph.glyphSet(): for glyph in self.glyph.glyphSet():
builder.add_multiple_subst( builder.add_multiple_subst(
self.location, prefix, glyph, suffix, self.replacement, self.forceChain self.location,
prefix,
glyph,
suffix,
self.replacement,
self.forceChain,
) )
else: else:
builder.add_multiple_subst( 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=""): def asFea(self, indent=""):
@ -2033,3 +2044,79 @@ class AxisValueLocationStatement(Statement):
res += f"location {self.tag} " res += f"location {self.tag} "
res += f"{' '.join(str(i) for i in self.values)};\n" res += f"{' '.join(str(i) for i in self.values)};\n"
return res 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.parser import Parser
from fontTools.feaLib.ast import FeatureFile from fontTools.feaLib.ast import FeatureFile
from fontTools.feaLib.variableScalar import VariableScalar
from fontTools.otlLib import builder as otl from fontTools.otlLib import builder as otl
from fontTools.otlLib.maxContextCalc import maxCtxFont from fontTools.otlLib.maxContextCalc import maxCtxFont
from fontTools.ttLib import newTable, getTableModule from fontTools.ttLib import newTable, getTableModule
@ -30,6 +31,10 @@ from fontTools.otlLib.builder import (
ChainContextualRule, ChainContextualRule,
) )
from fontTools.otlLib.error import OpenTypeLibError 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 from collections import defaultdict
import itertools import itertools
from io import StringIO from io import StringIO
@ -111,6 +116,12 @@ class Builder(object):
else: else:
self.parseTree, self.file = None, featurefile self.parseTree, self.file = None, featurefile
self.glyphMap = font.getReverseGlyphMap() 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.default_language_systems_ = set()
self.script_ = None self.script_ = None
self.lookupflag_ = 0 self.lookupflag_ = 0
@ -125,6 +136,7 @@ class Builder(object):
self.lookup_locations = {"GSUB": {}, "GPOS": {}} self.lookup_locations = {"GSUB": {}, "GPOS": {}}
self.features_ = {} # ('latn', 'DEU ', 'smcp') --> [LookupBuilder*] self.features_ = {} # ('latn', 'DEU ', 'smcp') --> [LookupBuilder*]
self.required_features_ = {} # ('latn', 'DEU ') --> 'scmp' self.required_features_ = {} # ('latn', 'DEU ') --> 'scmp'
self.feature_variations_ = {}
# for feature 'aalt' # for feature 'aalt'
self.aalt_features_ = [] # [(location, featureName)*], for 'aalt' self.aalt_features_ = [] # [(location, featureName)*], for 'aalt'
self.aalt_location_ = None self.aalt_location_ = None
@ -162,6 +174,8 @@ class Builder(object):
self.vhea_ = {} self.vhea_ = {}
# for table 'STAT' # for table 'STAT'
self.stat_ = {} self.stat_ = {}
# for conditionsets
self.conditionsets_ = {}
def build(self, tables=None, debug=False): def build(self, tables=None, debug=False):
if self.parseTree is None: if self.parseTree is None:
@ -197,6 +211,8 @@ class Builder(object):
if tag not in tables: if tag not in tables:
continue continue
table = self.makeTable(tag) table = self.makeTable(tag)
if self.feature_variations_:
self.makeFeatureVariations(table, tag)
if ( if (
table.ScriptList.ScriptCount > 0 table.ScriptList.ScriptCount > 0
or table.FeatureList.FeatureCount > 0 or table.FeatureList.FeatureCount > 0
@ -214,6 +230,8 @@ class Builder(object):
self.font["GDEF"] = gdef self.font["GDEF"] = gdef
elif "GDEF" in self.font: elif "GDEF" in self.font:
del self.font["GDEF"] del self.font["GDEF"]
elif self.varstorebuilder:
raise FeatureLibError("Must save GDEF when compiling a variable font")
if "BASE" in tables: if "BASE" in tables:
base = self.buildBASE() base = self.buildBASE()
if base: if base:
@ -744,6 +762,16 @@ class Builder(object):
gdef.MarkAttachClassDef = self.buildGDEFMarkAttachClassDef_() gdef.MarkAttachClassDef = self.buildGDEFMarkAttachClassDef_()
gdef.MarkGlyphSetsDef = self.buildGDEFMarkGlyphSetsDef_() gdef.MarkGlyphSetsDef = self.buildGDEFMarkGlyphSetsDef_()
gdef.Version = 0x00010002 if gdef.MarkGlyphSetsDef else 0x00010000 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( if any(
( (
gdef.GlyphClassDef, gdef.GlyphClassDef,
@ -752,7 +780,7 @@ class Builder(object):
gdef.MarkAttachClassDef, gdef.MarkAttachClassDef,
gdef.MarkGlyphSetsDef, gdef.MarkGlyphSetsDef,
) )
): ) or hasattr(gdef, "VarStore"):
result = newTable("GDEF") result = newTable("GDEF")
result.table = gdef result.table = gdef
return result return result
@ -848,7 +876,8 @@ class Builder(object):
) )
size_feature = tag == "GPOS" and feature_tag == "size" 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 continue
for ix in lookup_indices: for ix in lookup_indices:
@ -914,6 +943,42 @@ class Builder(object):
table.LookupList.LookupCount = len(table.LookupList.Lookup) table.LookupList.LookupCount = len(table.LookupList.Lookup)
return table 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): def get_lookup_name_(self, lookup):
rev = {v: k for k, v in self.named_lookups_.items()} rev = {v: k for k, v in self.named_lookups_.items()}
if lookup in rev: if lookup in rev:
@ -1298,8 +1363,8 @@ class Builder(object):
lookup.add_attachment( lookup.add_attachment(
location, location,
glyphclass, glyphclass,
makeOpenTypeAnchor(entryAnchor), self.makeOpenTypeAnchor(location, entryAnchor),
makeOpenTypeAnchor(exitAnchor), self.makeOpenTypeAnchor(location, exitAnchor),
) )
def add_marks_(self, location, lookupBuilder, marks): def add_marks_(self, location, lookupBuilder, marks):
@ -1308,7 +1373,7 @@ class Builder(object):
for markClassDef in markClass.definitions: for markClassDef in markClass.definitions:
for mark in markClassDef.glyphs.glyphSet(): for mark in markClassDef.glyphs.glyphSet():
if mark not in lookupBuilder.marks: if mark not in lookupBuilder.marks:
otMarkAnchor = makeOpenTypeAnchor(markClassDef.anchor) otMarkAnchor = self.makeOpenTypeAnchor(location, markClassDef.anchor)
lookupBuilder.marks[mark] = (markClass.name, otMarkAnchor) lookupBuilder.marks[mark] = (markClass.name, otMarkAnchor)
else: else:
existingMarkClass = lookupBuilder.marks[mark][0] existingMarkClass = lookupBuilder.marks[mark][0]
@ -1323,7 +1388,7 @@ class Builder(object):
builder = self.get_lookup_(location, MarkBasePosBuilder) builder = self.get_lookup_(location, MarkBasePosBuilder)
self.add_marks_(location, builder, marks) self.add_marks_(location, builder, marks)
for baseAnchor, markClass in marks: for baseAnchor, markClass in marks:
otBaseAnchor = makeOpenTypeAnchor(baseAnchor) otBaseAnchor = self.makeOpenTypeAnchor(location, baseAnchor)
for base in bases: for base in bases:
builder.bases.setdefault(base, {})[markClass.name] = otBaseAnchor builder.bases.setdefault(base, {})[markClass.name] = otBaseAnchor
@ -1334,7 +1399,7 @@ class Builder(object):
anchors = {} anchors = {}
self.add_marks_(location, builder, marks) self.add_marks_(location, builder, marks)
for ligAnchor, markClass in marks: for ligAnchor, markClass in marks:
anchors[markClass.name] = makeOpenTypeAnchor(ligAnchor) anchors[markClass.name] = self.makeOpenTypeAnchor(location, ligAnchor)
componentAnchors.append(anchors) componentAnchors.append(anchors)
for glyph in ligatures: for glyph in ligatures:
builder.ligatures[glyph] = componentAnchors builder.ligatures[glyph] = componentAnchors
@ -1343,7 +1408,7 @@ class Builder(object):
builder = self.get_lookup_(location, MarkMarkPosBuilder) builder = self.get_lookup_(location, MarkMarkPosBuilder)
self.add_marks_(location, builder, marks) self.add_marks_(location, builder, marks)
for baseAnchor, markClass in marks: for baseAnchor, markClass in marks:
otBaseAnchor = makeOpenTypeAnchor(baseAnchor) otBaseAnchor = self.makeOpenTypeAnchor(location, baseAnchor)
for baseMark in baseMarks: for baseMark in baseMarks:
builder.baseMarks.setdefault(baseMark, {})[ builder.baseMarks.setdefault(baseMark, {})[
markClass.name markClass.name
@ -1351,8 +1416,8 @@ class Builder(object):
def add_class_pair_pos(self, location, glyphclass1, value1, glyphclass2, value2): def add_class_pair_pos(self, location, glyphclass1, value1, glyphclass2, value2):
lookup = self.get_lookup_(location, PairPosBuilder) lookup = self.get_lookup_(location, PairPosBuilder)
v1 = makeOpenTypeValueRecord(value1, pairPosContext=True) v1 = self.makeOpenTypeValueRecord(location, value1, pairPosContext=True)
v2 = makeOpenTypeValueRecord(value2, pairPosContext=True) v2 = self.makeOpenTypeValueRecord(location, value2, pairPosContext=True)
lookup.addClassPair(location, glyphclass1, v1, glyphclass2, v2) lookup.addClassPair(location, glyphclass1, v1, glyphclass2, v2)
def add_subtable_break(self, location): def add_subtable_break(self, location):
@ -1360,8 +1425,8 @@ class Builder(object):
def add_specific_pair_pos(self, location, glyph1, value1, glyph2, value2): def add_specific_pair_pos(self, location, glyph1, value1, glyph2, value2):
lookup = self.get_lookup_(location, PairPosBuilder) lookup = self.get_lookup_(location, PairPosBuilder)
v1 = makeOpenTypeValueRecord(value1, pairPosContext=True) v1 = self.makeOpenTypeValueRecord(location, value1, pairPosContext=True)
v2 = makeOpenTypeValueRecord(value2, pairPosContext=True) v2 = self.makeOpenTypeValueRecord(location, value2, pairPosContext=True)
lookup.addGlyphPair(location, glyph1, v1, glyph2, v2) lookup.addGlyphPair(location, glyph1, v1, glyph2, v2)
def add_single_pos(self, location, prefix, suffix, pos, forceChain): def add_single_pos(self, location, prefix, suffix, pos, forceChain):
@ -1370,7 +1435,7 @@ class Builder(object):
else: else:
lookup = self.get_lookup_(location, SinglePosBuilder) lookup = self.get_lookup_(location, SinglePosBuilder)
for glyphs, value in pos: for glyphs, value in pos:
otValueRecord = makeOpenTypeValueRecord(value, pairPosContext=False) otValueRecord = self.makeOpenTypeValueRecord(location, value, pairPosContext=False)
for glyph in glyphs: for glyph in glyphs:
try: try:
lookup.add_pos(location, glyph, otValueRecord) lookup.add_pos(location, glyph, otValueRecord)
@ -1388,7 +1453,7 @@ class Builder(object):
if value is None: if value is None:
subs.append(None) subs.append(None)
continue continue
otValue = makeOpenTypeValueRecord(value, pairPosContext=False) otValue = self.makeOpenTypeValueRecord(location, value, pairPosContext=False)
sub = chain.find_chainable_single_pos(targets, glyphs, otValue) sub = chain.find_chainable_single_pos(targets, glyphs, otValue)
if sub is None: if sub is None:
sub = self.get_chained_lookup_(location, SinglePosBuilder) sub = self.get_chained_lookup_(location, SinglePosBuilder)
@ -1445,37 +1510,98 @@ class Builder(object):
def add_vhea_field(self, key, value): def add_vhea_field(self, key, value):
self.vhea_[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
"""ast.Anchor --> otTables.Anchor""" axisMap = {
if anchor is None: axis.axisTag: (axis.minValue, axis.defaultValue, axis.maxValue)
return None for axis in self.axes
deviceX, deviceY = None, None }
if anchor.xDeviceTable is not None:
deviceX = otl.buildDevice(dict(anchor.xDeviceTable)) value = {
if anchor.yDeviceTable is not None: tag: (
deviceY = otl.buildDevice(dict(anchor.yDeviceTable)) normalizeValue(bottom, axisMap[tag]),
return otl.buildAnchor(anchor.x, anchor.y, anchor.contourpoint, deviceX, deviceY) 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))
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 = {
name[0].lower() + name[1:]: (name, isDevice)
for _, name, isDevice, _ in otBase.valueRecordFormat
if not name.startswith("Reserved")
}
_VALUEREC_ATTRS = { def makeOpenTypeValueRecord(self, location, v, pairPosContext):
name[0].lower() + name[1:]: (name, isDevice) """ast.ValueRecord --> otBase.ValueRecord"""
for _, name, isDevice, _ in otBase.valueRecordFormat if not v:
if not name.startswith("Reserved") return None
}
vr = {}
variable = False
for astName, (otName, isDevice) in self._VALUEREC_ATTRS.items():
val = getattr(v, astName, None)
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
def makeOpenTypeValueRecord(v, pairPosContext): if pairPosContext and not vr:
"""ast.ValueRecord --> otBase.ValueRecord""" vr = {"YAdvance": 0} if v.vertical else {"XAdvance": 0}
if not v: valRec = otl.buildValue(vr)
return None return valRec
vr = {}
for astName, (otName, isDevice) in _VALUEREC_ATTRS.items():
val = getattr(v, astName, None)
if val:
vr[otName] = otl.buildDevice(dict(val)) if isDevice else val
if pairPosContext and not vr:
vr = {"YAdvance": 0} if v.vertical else {"XAdvance": 0}
valRec = otl.buildValue(vr)
return valRec

View File

@ -1,5 +1,6 @@
from fontTools.feaLib.error import FeatureLibError from fontTools.feaLib.error import FeatureLibError
from fontTools.feaLib.lexer import Lexer, IncludingLexer, NonIncludingLexer from fontTools.feaLib.lexer import Lexer, IncludingLexer, NonIncludingLexer
from fontTools.feaLib.variableScalar import VariableScalar
from fontTools.misc.encodingTools import getEncoding from fontTools.misc.encodingTools import getEncoding
from fontTools.misc.textTools import bytechr, tobytes, tostr from fontTools.misc.textTools import bytechr, tobytes, tostr
import fontTools.feaLib.ast as ast import fontTools.feaLib.ast as ast
@ -101,6 +102,10 @@ class Parser(object):
statements.append(self.parse_markClass_()) statements.append(self.parse_markClass_())
elif self.is_cur_keyword_("feature"): elif self.is_cur_keyword_("feature"):
statements.append(self.parse_feature_block_()) 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"): elif self.is_cur_keyword_("table"):
statements.append(self.parse_table_()) statements.append(self.parse_table_())
elif self.is_cur_keyword_("valueRecordDef"): elif self.is_cur_keyword_("valueRecordDef"):
@ -152,7 +157,7 @@ class Parser(object):
location=location, location=location,
) )
x, y = self.expect_number_(), self.expect_number_() x, y = self.expect_number_(variable=True), self.expect_number_(variable=True)
contourpoint = None contourpoint = None
if self.next_token_ == "contourpoint": # Format B if self.next_token_ == "contourpoint": # Format B
@ -380,8 +385,7 @@ class Parser(object):
self.expect_symbol_("-") self.expect_symbol_("-")
range_end = self.expect_cid_() range_end = self.expect_cid_()
self.check_glyph_name_in_glyph_set( self.check_glyph_name_in_glyph_set(
f"cid{range_start:05d}", f"cid{range_start:05d}", f"cid{range_end:05d}",
f"cid{range_end:05d}",
) )
glyphs.add_cid_range( glyphs.add_cid_range(
range_start, range_start,
@ -477,7 +481,7 @@ class Parser(object):
raise FeatureLibError( raise FeatureLibError(
"Positioning cannot be applied in the bactrack glyph sequence, " "Positioning cannot be applied in the bactrack glyph sequence, "
"before the marked glyph sequence.", "before the marked glyph sequence.",
self.cur_token_location_ self.cur_token_location_,
) )
marked_values = values[len(prefix) : len(prefix) + len(glyphs)] marked_values = values[len(prefix) : len(prefix) + len(glyphs)]
if any(marked_values): if any(marked_values):
@ -486,7 +490,7 @@ class Parser(object):
"Positioning values are allowed only in the marked glyph " "Positioning values are allowed only in the marked glyph "
"sequence, or after the final glyph node when only one glyph " "sequence, or after the final glyph node when only one glyph "
"node is marked.", "node is marked.",
self.cur_token_location_ self.cur_token_location_,
) )
values = marked_values values = marked_values
elif values and values[-1]: elif values and values[-1]:
@ -495,7 +499,7 @@ class Parser(object):
"Positioning values are allowed only in the marked glyph " "Positioning values are allowed only in the marked glyph "
"sequence, or after the final glyph node when only one glyph " "sequence, or after the final glyph node when only one glyph "
"node is marked.", "node is marked.",
self.cur_token_location_ self.cur_token_location_,
) )
values = values[-1:] values = values[-1:]
elif any(values): elif any(values):
@ -503,7 +507,7 @@ class Parser(object):
"Positioning values are allowed only in the marked glyph " "Positioning values are allowed only in the marked glyph "
"sequence, or after the final glyph node when only one glyph " "sequence, or after the final glyph node when only one glyph "
"node is marked.", "node is marked.",
self.cur_token_location_ self.cur_token_location_,
) )
return (prefix, glyphs, lookups, values, suffix, hasMarks) return (prefix, glyphs, lookups, values, suffix, hasMarks)
@ -1005,8 +1009,8 @@ class Parser(object):
location = self.cur_token_location_ location = self.cur_token_location_
DesignSize = self.expect_decipoint_() DesignSize = self.expect_decipoint_()
SubfamilyID = self.expect_number_() SubfamilyID = self.expect_number_()
RangeStart = 0. RangeStart = 0.0
RangeEnd = 0. RangeEnd = 0.0
if self.next_token_type_ in (Lexer.NUMBER, Lexer.FLOAT) or SubfamilyID != 0: if self.next_token_type_ in (Lexer.NUMBER, Lexer.FLOAT) or SubfamilyID != 0:
RangeStart = self.expect_decipoint_() RangeStart = self.expect_decipoint_()
RangeEnd = self.expect_decipoint_() RangeEnd = self.expect_decipoint_()
@ -1585,11 +1589,20 @@ class Parser(object):
return result return result
def is_next_value_(self): 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): def parse_valuerecord_(self, vertical):
if self.next_token_type_ is Lexer.NUMBER: if (
number, location = self.expect_number_(), self.cur_token_location_ 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: if vertical:
val = self.ast.ValueRecord( val = self.ast.ValueRecord(
yAdvance=number, vertical=vertical, location=location yAdvance=number, vertical=vertical, location=location
@ -1616,10 +1629,10 @@ class Parser(object):
xAdvance, yAdvance = (value.xAdvance, value.yAdvance) xAdvance, yAdvance = (value.xAdvance, value.yAdvance)
else: else:
xPlacement, yPlacement, xAdvance, yAdvance = ( xPlacement, yPlacement, xAdvance, yAdvance = (
self.expect_number_(), self.expect_number_(variable=True),
self.expect_number_(), self.expect_number_(variable=True),
self.expect_number_(), self.expect_number_(variable=True),
self.expect_number_(), self.expect_number_(variable=True),
) )
if self.next_token_ == "<": if self.next_token_ == "<":
@ -1679,8 +1692,11 @@ class Parser(object):
self.expect_symbol_(";") self.expect_symbol_(";")
return self.ast.LanguageSystemStatement(script, language, location=location) return self.ast.LanguageSystemStatement(script, language, location=location)
def parse_feature_block_(self): def parse_feature_block_(self, variation=False):
assert self.cur_token_ == "feature" if variation:
assert self.cur_token_ == "variation"
else:
assert self.cur_token_ == "feature"
location = self.cur_token_location_ location = self.cur_token_location_
tag = self.expect_tag_() tag = self.expect_tag_()
vertical = tag in {"vkrn", "vpal", "vhal", "valt"} vertical = tag in {"vkrn", "vpal", "vhal", "valt"}
@ -1695,14 +1711,22 @@ class Parser(object):
elif tag == "size": elif tag == "size":
size_feature = True size_feature = True
if variation:
conditionset = self.expect_name_()
use_extension = False use_extension = False
if self.next_token_ == "useExtension": if self.next_token_ == "useExtension":
self.expect_keyword_("useExtension") self.expect_keyword_("useExtension")
use_extension = True use_extension = True
block = self.ast.FeatureBlock( if variation:
tag, use_extension=use_extension, location=location block = self.ast.VariationBlock(
) tag, conditionset, use_extension=use_extension, location=location
)
else:
block = self.ast.FeatureBlock(
tag, use_extension=use_extension, location=location
)
self.parse_block_(block, vertical, stylisticset, size_feature, cv_feature) self.parse_block_(block, vertical, stylisticset, size_feature, cv_feature)
return block return block
@ -1850,6 +1874,43 @@ class Parser(object):
raise FeatureLibError("Font revision numbers must be positive", location) raise FeatureLibError("Font revision numbers must be positive", location)
return self.ast.FontRevisionStatement(version, location=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_( def parse_block_(
self, block, vertical, stylisticset=None, size_feature=False, cv_feature=None self, block, vertical, stylisticset=None, size_feature=False, cv_feature=None
): ):
@ -2080,12 +2141,51 @@ class Parser(object):
return self.cur_token_ return self.cur_token_
raise FeatureLibError("Expected a name", self.cur_token_location_) raise FeatureLibError("Expected a name", self.cur_token_location_)
def expect_number_(self): def expect_number_(self, variable=False):
self.advance_lexer_() self.advance_lexer_()
if self.cur_token_type_ is Lexer.NUMBER: if self.cur_token_type_ is Lexer.NUMBER:
return self.cur_token_ 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_) 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): def expect_any_number_(self):
self.advance_lexer_() self.advance_lexer_()
if self.cur_token_type_ in Lexer.NUMBERS: 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. positioning lookup.
""" """
markClasses = self.buildMarkClasses_(self.marks) markClasses = self.buildMarkClasses_(self.marks)
marks = { marks = {}
mark: (markClasses[mc], anchor) for mark, (mc, anchor) in self.marks.items() 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 = {} bases = {}
for glyph, anchors in self.bases.items(): 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) subtables = buildMarkBasePos(marks, bases, self.glyphMap)
return self.buildLookup_(subtables) return self.buildLookup_(subtables)

View File

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

View File

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