Support variable feature syntax (#2432)
This commit is contained in:
parent
c194a18be7
commit
563730f8ce
@ -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
|
||||
|
@ -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,37 +1510,98 @@ 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):
|
||||
"""ast.Anchor --> otTables.Anchor"""
|
||||
if anchor is None:
|
||||
return None
|
||||
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)
|
||||
# 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))
|
||||
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 = {
|
||||
name[0].lower() + name[1:]: (name, isDevice)
|
||||
for _, name, isDevice, _ in otBase.valueRecordFormat
|
||||
if not name.startswith("Reserved")
|
||||
}
|
||||
def makeOpenTypeValueRecord(self, location, v, pairPosContext):
|
||||
"""ast.ValueRecord --> otBase.ValueRecord"""
|
||||
if not v:
|
||||
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):
|
||||
"""ast.ValueRecord --> otBase.ValueRecord"""
|
||||
if not v:
|
||||
return None
|
||||
|
||||
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
|
||||
if pairPosContext and not vr:
|
||||
vr = {"YAdvance": 0} if v.vertical else {"XAdvance": 0}
|
||||
valRec = otl.buildValue(vr)
|
||||
return valRec
|
||||
|
@ -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,8 +1692,11 @@ class Parser(object):
|
||||
self.expect_symbol_(";")
|
||||
return self.ast.LanguageSystemStatement(script, language, location=location)
|
||||
|
||||
def parse_feature_block_(self):
|
||||
assert self.cur_token_ == "feature"
|
||||
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_()
|
||||
vertical = tag in {"vkrn", "vpal", "vhal", "valt"}
|
||||
@ -1695,14 +1711,22 @@ 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
|
||||
|
||||
block = self.ast.FeatureBlock(
|
||||
tag, use_extension=use_extension, location=location
|
||||
)
|
||||
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
|
||||
)
|
||||
self.parse_block_(block, vertical, stylisticset, size_feature, cv_feature)
|
||||
return block
|
||||
|
||||
@ -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:
|
||||
|
97
Lib/fontTools/feaLib/variableScalar.py
Normal file
97
Lib/fontTools/feaLib/variableScalar.py
Normal 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
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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,
|
||||
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)))
|
||||
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))
|
||||
)
|
||||
|
||||
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__":
|
||||
|
13
Tests/feaLib/data/variable_conditionset.fea
Normal file
13
Tests/feaLib/data/variable_conditionset.fea
Normal 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;
|
67
Tests/feaLib/data/variable_conditionset.ttx
Normal file
67
Tests/feaLib/data/variable_conditionset.ttx
Normal 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>
|
4
Tests/feaLib/data/variable_scalar_anchor.fea
Normal file
4
Tests/feaLib/data/variable_scalar_anchor.fea
Normal 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;
|
101
Tests/feaLib/data/variable_scalar_anchor.ttx
Normal file
101
Tests/feaLib/data/variable_scalar_anchor.ttx
Normal 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>
|
5
Tests/feaLib/data/variable_scalar_valuerecord.fea
Normal file
5
Tests/feaLib/data/variable_scalar_valuerecord.fea
Normal 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;
|
104
Tests/feaLib/data/variable_scalar_valuerecord.ttx
Normal file
104
Tests/feaLib/data/variable_scalar_valuerecord.ttx
Normal 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
Loading…
x
Reference in New Issue
Block a user