Merge pull request #2039 from fonttools/feaLib-STAT

[feaLib] Add support for STAT table
This commit is contained in:
Cosimo Lupo 2021-02-25 17:32:11 +00:00 committed by GitHub
commit d4ec4fffd2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1615 additions and 38 deletions

View File

@ -4,6 +4,7 @@ from fontTools.feaLib.location import FeatureLibLocation
from fontTools.misc.encodingTools import getEncoding
from collections import OrderedDict
import itertools
from typing import NamedTuple
SHIFT = " " * 4
@ -28,12 +29,15 @@ __all__ = [
"Anchor",
"AnchorDefinition",
"AttachStatement",
"AxisValueLocationStatement",
"BaseAxis",
"CVParametersNameStatement",
"ChainContextPosStatement",
"ChainContextSubstStatement",
"CharacterStatement",
"CursivePosStatement",
"ElidedFallbackName",
"ElidedFallbackNameID",
"Expression",
"FeatureNameStatement",
"FeatureReferenceStatement",
@ -62,6 +66,9 @@ __all__ = [
"SingleSubstStatement",
"SizeParameters",
"Statement",
"STATAxisValueStatement",
"STATDesignAxisStatement",
"STATNameStatement",
"SubtableStatement",
"TableBlock",
"ValueRecord",
@ -252,7 +259,7 @@ class GlyphClass(Expression):
def add_range(self, start, end, glyphs):
"""Add a range (e.g. ``A-Z``) to the class. ``start`` and ``end``
are either :class:`GlyphName` objects or strings representing the
are either :class:`GlyphName` objects or strings representing the
start and end glyphs in the class, and ``glyphs`` is the full list of
:class:`GlyphName` objects in the range."""
if self.curr < len(self.glyphs):
@ -547,7 +554,7 @@ class MarkClass(object):
class MarkClassDefinition(Statement):
"""A single ``markClass`` statement. The ``markClass`` should be a
"""A single ``markClass`` statement. The ``markClass`` should be a
:class:`MarkClass` object, the ``anchor`` an :class:`Anchor` object,
and the ``glyphs`` parameter should be a `glyph-containing object`_ .
@ -849,7 +856,7 @@ class IgnorePosStatement(Statement):
"""An ``ignore pos`` statement, containing `one or more` contexts to ignore.
``chainContexts`` should be a list of ``(prefix, glyphs, suffix)`` tuples,
with each of ``prefix``, ``glyphs`` and ``suffix`` being
with each of ``prefix``, ``glyphs`` and ``suffix`` being
`glyph-containing objects`_ ."""
def __init__(self, chainContexts, location=None):
@ -1165,7 +1172,7 @@ class MarkLigPosStatement(Statement):
# ... add definitions to mark classes...
glyph = GlyphName("lam_meem_jeem")
marks = [
marks = [
[ (Anchor(625,1800), m1) ], # Attachments on 1st component (lam)
[ (Anchor(376,-378), m2) ], # Attachments on 2nd component (meem)
[ ] # No attachments on the jeem
@ -1704,6 +1711,16 @@ class FeatureNameStatement(NameRecord):
return '{} {}"{}";'.format(tag, plat, self.string)
class STATNameStatement(NameRecord):
"""Represents a STAT table ``name`` statement."""
def asFea(self, indent=""):
plat = simplify_name_attributes(self.platformID, self.platEncID, self.langID)
if plat != "":
plat += " "
return 'name {}"{}";'.format(plat, self.string)
class SizeParameters(Statement):
"""A ``parameters`` statement."""
@ -1882,3 +1899,132 @@ class VheaField(Statement):
fields = ("VertTypoAscender", "VertTypoDescender", "VertTypoLineGap")
keywords = dict([(x.lower(), x) for x in fields])
return "{} {};".format(keywords[self.key], self.value)
class STATDesignAxisStatement(Statement):
"""A STAT table Design Axis
Args:
tag (str): a 4 letter axis tag
axisOrder (int): an int
names (list): a list of :class:`STATNameStatement` objects
"""
def __init__(self, tag, axisOrder, names, location=None):
Statement.__init__(self, location)
self.tag = tag
self.axisOrder = axisOrder
self.names = names
self.location = location
def build(self, builder):
builder.addDesignAxis(self, self.location)
def asFea(self, indent=""):
indent += SHIFT
res = f"DesignAxis {self.tag} {self.axisOrder} {{ \n"
res += ("\n" + indent).join([s.asFea(indent=indent) for s in self.names]) + "\n"
res += "};"
return res
class ElidedFallbackName(Statement):
"""STAT table ElidedFallbackName
Args:
names: a list of :class:`STATNameStatement` objects
"""
def __init__(self, names, location=None):
Statement.__init__(self, location)
self.names = names
self.location = location
def build(self, builder):
builder.setElidedFallbackName(self.names, self.location)
def asFea(self, indent=""):
indent += SHIFT
res = "ElidedFallbackName { \n"
res += ("\n" + indent).join([s.asFea(indent=indent) for s in self.names]) + "\n"
res += "};"
return res
class ElidedFallbackNameID(Statement):
"""STAT table ElidedFallbackNameID
Args:
value: an int pointing to an existing name table name ID
"""
def __init__(self, value, location=None):
Statement.__init__(self, location)
self.value = value
self.location = location
def build(self, builder):
builder.setElidedFallbackName(self.value, self.location)
def asFea(self, indent=""):
return f"ElidedFallbackNameID {self.value};"
class STATAxisValueStatement(Statement):
"""A STAT table Axis Value Record
Args:
names (list): a list of :class:`STATNameStatement` objects
locations (list): a list of :class:`AxisValueLocationStatement` objects
flags (int): an int
"""
def __init__(self, names, locations, flags, location=None):
Statement.__init__(self, location)
self.names = names
self.locations = locations
self.flags = flags
def build(self, builder):
builder.addAxisValueRecord(self, self.location)
def asFea(self, indent=""):
res = "AxisValue {\n"
for location in self.locations:
res += location.asFea()
for nameRecord in self.names:
res += nameRecord.asFea()
res += "\n"
if self.flags:
flags = ["OlderSiblingFontAttribute", "ElidableAxisValueName"]
flagStrings = []
curr = 1
for i in range(len(flags)):
if self.flags & curr != 0:
flagStrings.append(flags[i])
curr = curr << 1
res += f"flag {' '.join(flagStrings)};\n"
res += "};"
return res
class AxisValueLocationStatement(Statement):
"""
A STAT table Axis Value Location
Args:
tag (str): a 4 letter axis tag
values (list): a list of ints and/or floats
"""
def __init__(self, tag, values, location=None):
Statement.__init__(self, location)
self.tag = tag
self.values = values
def asFea(self, res=""):
res += f"location {self.tag} "
res += f"{' '.join(str(i) for i in self.values)};\n"
return res

View File

@ -98,6 +98,7 @@ class Builder(object):
"hhea",
"name",
"vhea",
"STAT",
]
)
@ -159,6 +160,8 @@ class Builder(object):
self.hhea_ = {}
# for table 'vhea'
self.vhea_ = {}
# for table 'STAT'
self.stat_ = {}
def build(self, tables=None, debug=False):
if self.parseTree is None:
@ -188,6 +191,8 @@ class Builder(object):
self.build_name()
if "OS/2" in tables:
self.build_OS_2()
if "STAT" in tables:
self.build_STAT()
for tag in ("GPOS", "GSUB"):
if tag not in tables:
continue
@ -510,6 +515,140 @@ class Builder(object):
if version >= 5:
checkattr(table, ("usLowerOpticalPointSize", "usUpperOpticalPointSize"))
def setElidedFallbackName(self, value, location):
# ElidedFallbackName is a convenience method for setting
# ElidedFallbackNameID so only one can be allowed
for token in ("ElidedFallbackName", "ElidedFallbackNameID"):
if token in self.stat_:
raise FeatureLibError(
f"{token} is already set.",
location,
)
if isinstance(value, int):
self.stat_["ElidedFallbackNameID"] = value
elif isinstance(value, list):
self.stat_["ElidedFallbackName"] = value
else:
raise AssertionError(value)
def addDesignAxis(self, designAxis, location):
if "DesignAxes" not in self.stat_:
self.stat_["DesignAxes"] = []
if designAxis.tag in (r.tag for r in self.stat_["DesignAxes"]):
raise FeatureLibError(
f'DesignAxis already defined for tag "{designAxis.tag}".',
location,
)
if designAxis.axisOrder in (r.axisOrder for r in self.stat_["DesignAxes"]):
raise FeatureLibError(
f"DesignAxis already defined for axis number {designAxis.axisOrder}.",
location,
)
self.stat_["DesignAxes"].append(designAxis)
def addAxisValueRecord(self, axisValueRecord, location):
if "AxisValueRecords" not in self.stat_:
self.stat_["AxisValueRecords"] = []
# Check for duplicate AxisValueRecords
for record_ in self.stat_["AxisValueRecords"]:
if (
{n.asFea() for n in record_.names}
== {n.asFea() for n in axisValueRecord.names}
and {n.asFea() for n in record_.locations}
== {n.asFea() for n in axisValueRecord.locations}
and record_.flags == axisValueRecord.flags
):
raise FeatureLibError(
"An AxisValueRecord with these values is already defined.",
location,
)
self.stat_["AxisValueRecords"].append(axisValueRecord)
def build_STAT(self):
if not self.stat_:
return
axes = self.stat_.get("DesignAxes")
if not axes:
raise FeatureLibError("DesignAxes not defined", None)
axisValueRecords = self.stat_.get("AxisValueRecords")
axisValues = {}
format4_locations = []
for tag in axes:
axisValues[tag.tag] = []
if axisValueRecords is not None:
for avr in axisValueRecords:
valuesDict = {}
if avr.flags > 0:
valuesDict["flags"] = avr.flags
if len(avr.locations) == 1:
location = avr.locations[0]
values = location.values
if len(values) == 1: # format1
valuesDict.update({"value": values[0], "name": avr.names})
if len(values) == 2: # format3
valuesDict.update(
{
"value": values[0],
"linkedValue": values[1],
"name": avr.names,
}
)
if len(values) == 3: # format2
nominal, minVal, maxVal = values
valuesDict.update(
{
"nominalValue": nominal,
"rangeMinValue": minVal,
"rangeMaxValue": maxVal,
"name": avr.names,
}
)
axisValues[location.tag].append(valuesDict)
else:
valuesDict.update(
{
"location": {i.tag: i.values[0] for i in avr.locations},
"name": avr.names,
}
)
format4_locations.append(valuesDict)
designAxes = [
{
"ordering": a.axisOrder,
"tag": a.tag,
"name": a.names,
"values": axisValues[a.tag],
}
for a in axes
]
nameTable = self.font.get("name")
if not nameTable: # this only happens for unit tests
nameTable = self.font["name"] = newTable("name")
nameTable.names = []
if "ElidedFallbackNameID" in self.stat_:
nameID = self.stat_["ElidedFallbackNameID"]
name = nameTable.getDebugName(nameID)
if not name:
raise FeatureLibError(
f"ElidedFallbackNameID {nameID} points "
"to a nameID that does not exist in the "
'"name" table',
None,
)
elif "ElidedFallbackName" in self.stat_:
nameID = self.stat_["ElidedFallbackName"]
otl.buildStatTable(
self.font,
designAxes,
locations=format4_locations,
elidedFallbackName=nameID,
)
def build_codepages_(self, pages):
pages2bits = {
1252: 0,
@ -718,8 +857,10 @@ class Builder(object):
str(ix)
]._replace(feature=key)
except KeyError:
warnings.warn("feaLib.Builder subclass needs upgrading to "
"stash debug information. See fonttools#2065.")
warnings.warn(
"feaLib.Builder subclass needs upgrading to "
"stash debug information. See fonttools#2065."
)
feature_key = (feature_tag, lookup_indices)
feature_index = feature_indices.get(feature_key)

View File

@ -1003,6 +1003,7 @@ class Parser(object):
"name": self.parse_table_name_,
"BASE": self.parse_table_BASE_,
"OS/2": self.parse_table_OS_2_,
"STAT": self.parse_table_STAT_,
}.get(name)
if handler:
handler(table)
@ -1162,6 +1163,35 @@ class Parser(object):
unescaped = self.unescape_string_(string, encoding)
return platformID, platEncID, langID, unescaped
def parse_stat_name_(self):
platEncID = None
langID = None
if self.next_token_type_ in Lexer.NUMBERS:
platformID = self.expect_any_number_()
location = self.cur_token_location_
if platformID not in (1, 3):
raise FeatureLibError("Expected platform id 1 or 3", location)
if self.next_token_type_ in Lexer.NUMBERS:
platEncID = self.expect_any_number_()
langID = self.expect_any_number_()
else:
platformID = 3
location = self.cur_token_location_
if platformID == 1: # Macintosh
platEncID = platEncID or 0 # Roman
langID = langID or 0 # English
else: # 3, Windows
platEncID = platEncID or 1 # Unicode
langID = langID or 0x0409 # English
string = self.expect_string_()
encoding = getEncoding(platformID, platEncID, langID)
if encoding is None:
raise FeatureLibError("Unsupported encoding", location)
unescaped = self.unescape_string_(string, encoding)
return platformID, platEncID, langID, unescaped
def parse_nameid_(self):
assert self.cur_token_ == "nameid", self.cur_token_
location, nameID = self.cur_token_location_, self.expect_any_number_()
@ -1283,6 +1313,198 @@ class Parser(object):
elif self.cur_token_ == ";":
continue
def parse_STAT_ElidedFallbackName(self):
assert self.is_cur_keyword_("ElidedFallbackName")
self.expect_symbol_("{")
names = []
while self.next_token_ != "}" or self.cur_comments_:
self.advance_lexer_()
if self.is_cur_keyword_("name"):
platformID, platEncID, langID, string = self.parse_stat_name_()
nameRecord = self.ast.STATNameStatement(
"stat",
platformID,
platEncID,
langID,
string,
location=self.cur_token_location_,
)
names.append(nameRecord)
else:
if self.cur_token_ != ";":
raise FeatureLibError(
f"Unexpected token {self.cur_token_} " f"in ElidedFallbackName",
self.cur_token_location_,
)
self.expect_symbol_("}")
if not names:
raise FeatureLibError('Expected "name"', self.cur_token_location_)
return names
def parse_STAT_design_axis(self):
assert self.is_cur_keyword_("DesignAxis")
names = []
axisTag = self.expect_tag_()
if (
axisTag not in ("ital", "opsz", "slnt", "wdth", "wght")
and not axisTag.isupper()
):
log.warning(f"Unregistered axis tag {axisTag} should be uppercase.")
axisOrder = self.expect_number_()
self.expect_symbol_("{")
while self.next_token_ != "}" or self.cur_comments_:
self.advance_lexer_()
if self.cur_token_type_ is Lexer.COMMENT:
continue
elif self.is_cur_keyword_("name"):
location = self.cur_token_location_
platformID, platEncID, langID, string = self.parse_stat_name_()
name = self.ast.STATNameStatement(
"stat", platformID, platEncID, langID, string, location=location
)
names.append(name)
elif self.cur_token_ == ";":
continue
else:
raise FeatureLibError(
f'Expected "name", got {self.cur_token_}', self.cur_token_location_
)
self.expect_symbol_("}")
return self.ast.STATDesignAxisStatement(
axisTag, axisOrder, names, self.cur_token_location_
)
def parse_STAT_axis_value_(self):
assert self.is_cur_keyword_("AxisValue")
self.expect_symbol_("{")
locations = []
names = []
flags = 0
while self.next_token_ != "}" or self.cur_comments_:
self.advance_lexer_(comments=True)
if self.cur_token_type_ is Lexer.COMMENT:
continue
elif self.is_cur_keyword_("name"):
location = self.cur_token_location_
platformID, platEncID, langID, string = self.parse_stat_name_()
name = self.ast.STATNameStatement(
"stat", platformID, platEncID, langID, string, location=location
)
names.append(name)
elif self.is_cur_keyword_("location"):
location = self.parse_STAT_location()
locations.append(location)
elif self.is_cur_keyword_("flag"):
flags = self.expect_stat_flags()
elif self.cur_token_ == ";":
continue
else:
raise FeatureLibError(
f"Unexpected token {self.cur_token_} " f"in AxisValue",
self.cur_token_location_,
)
self.expect_symbol_("}")
if not names:
raise FeatureLibError('Expected "Axis Name"', self.cur_token_location_)
if not locations:
raise FeatureLibError('Expected "Axis location"', self.cur_token_location_)
if len(locations) > 1:
for location in locations:
if len(location.values) > 1:
raise FeatureLibError(
"Only one value is allowed in a "
"Format 4 Axis Value Record, but "
f"{len(location.values)} were found.",
self.cur_token_location_,
)
format4_tags = []
for location in locations:
tag = location.tag
if tag in format4_tags:
raise FeatureLibError(
f"Axis tag {tag} already " "defined.", self.cur_token_location_
)
format4_tags.append(tag)
return self.ast.STATAxisValueStatement(
names, locations, flags, self.cur_token_location_
)
def parse_STAT_location(self):
values = []
tag = self.expect_tag_()
if len(tag.strip()) != 4:
raise FeatureLibError(
f"Axis tag {self.cur_token_} must be 4 " "characters",
self.cur_token_location_,
)
while self.next_token_ != ";":
if self.next_token_type_ is Lexer.FLOAT:
value = self.expect_float_()
values.append(value)
elif self.next_token_type_ is Lexer.NUMBER:
value = self.expect_number_()
values.append(value)
else:
raise FeatureLibError(
f'Unexpected value "{self.next_token_}". '
"Expected integer or float.",
self.next_token_location_,
)
if len(values) == 3:
nominal, min_val, max_val = values
if nominal < min_val or nominal > max_val:
raise FeatureLibError(
f"Default value {nominal} is outside "
f"of specified range "
f"{min_val}-{max_val}.",
self.next_token_location_,
)
return self.ast.AxisValueLocationStatement(tag, values)
def parse_table_STAT_(self, table):
statements = table.statements
design_axes = []
while self.next_token_ != "}" or self.cur_comments_:
self.advance_lexer_(comments=True)
if self.cur_token_type_ is Lexer.COMMENT:
statements.append(
self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
)
elif self.cur_token_type_ is Lexer.NAME:
if self.is_cur_keyword_("ElidedFallbackName"):
names = self.parse_STAT_ElidedFallbackName()
statements.append(self.ast.ElidedFallbackName(names))
elif self.is_cur_keyword_("ElidedFallbackNameID"):
value = self.expect_number_()
statements.append(self.ast.ElidedFallbackNameID(value))
self.expect_symbol_(";")
elif self.is_cur_keyword_("DesignAxis"):
designAxis = self.parse_STAT_design_axis()
design_axes.append(designAxis.tag)
statements.append(designAxis)
self.expect_symbol_(";")
elif self.is_cur_keyword_("AxisValue"):
axisValueRecord = self.parse_STAT_axis_value_()
for location in axisValueRecord.locations:
if location.tag not in design_axes:
# Tag must be defined in a DesignAxis before it
# can be referenced
raise FeatureLibError(
"DesignAxis not defined for " f"{location.tag}.",
self.cur_token_location_,
)
statements.append(axisValueRecord)
self.expect_symbol_(";")
else:
raise FeatureLibError(
f"Unexpected token {self.cur_token_}", self.cur_token_location_
)
elif self.cur_token_ == ";":
continue
def parse_base_tag_list_(self):
# Parses BASE table entries. (See `section 9.a <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.a>`_)
assert self.cur_token_ in (
@ -1784,7 +2006,7 @@ class Parser(object):
raise FeatureLibError("Expected a tag", self.cur_token_location_)
if len(self.cur_token_) > 4:
raise FeatureLibError(
"Tags can not be longer than 4 characters", self.cur_token_location_
"Tags cannot be longer than 4 characters", self.cur_token_location_
)
return (self.cur_token_ + " ")[:4]
@ -1856,6 +2078,32 @@ class Parser(object):
"Expected an integer or floating-point number", self.cur_token_location_
)
def expect_stat_flags(self):
value = 0
flags = {
"OlderSiblingFontAttribute": 1,
"ElidableAxisValueName": 2,
}
while self.next_token_ != ";":
if self.next_token_ in flags:
name = self.expect_name_()
value = value | flags[name]
else:
raise FeatureLibError(
f"Unexpected STAT flag {self.cur_token_}", self.cur_token_location_
)
return value
def expect_stat_values_(self):
if self.next_token_type_ == Lexer.FLOAT:
return self.expect_float_()
elif self.next_token_type_ is Lexer.NUMBER:
return self.expect_number_()
else:
raise FeatureLibError(
"Expected an integer or floating-point number", self.cur_token_location_
)
def expect_string_(self):
self.advance_lexer_()
if self.cur_token_type_ is Lexer.STRING:

View File

@ -9,6 +9,7 @@ from fontTools.ttLib.tables.otBase import (
CountReference,
)
from fontTools.ttLib.tables import otBase
from fontTools.feaLib.ast import STATNameStatement
from fontTools.otlLib.error import OpenTypeLibError
import logging
import copy
@ -94,9 +95,10 @@ def buildLookup(subtables, flags=0, markFilterSet=None):
subtables = [st for st in subtables if st is not None]
if not subtables:
return None
assert all(t.LookupType == subtables[0].LookupType for t in subtables), (
"all subtables must have the same LookupType; got %s"
% repr([t.LookupType for t in subtables])
assert all(
t.LookupType == subtables[0].LookupType for t in subtables
), "all subtables must have the same LookupType; got %s" % repr(
[t.LookupType for t in subtables]
)
self = ot.Lookup()
self.LookupType = subtables[0].LookupType
@ -2575,7 +2577,9 @@ class ClassDefBuilder(object):
self.classes_.add(glyphs)
for glyph in glyphs:
if glyph in self.glyphs_:
raise OpenTypeLibError(f"Glyph {glyph} is already present in class.", None)
raise OpenTypeLibError(
f"Glyph {glyph} is already present in class.", None
)
self.glyphs_[glyph] = glyphs
def classes(self):
@ -2687,8 +2691,8 @@ def buildStatTable(ttFont, axes, locations=None, elidedFallbackName=2):
]
The optional 'elidedFallbackName' argument can be a name ID (int),
a string, or a dictionary containing multilingual names. It
translates to the ElidedFallbackNameID field.
a string, a dictionary containing multilingual names, or a list of
STATNameStatements. It translates to the ElidedFallbackNameID field.
The 'ttFont' argument must be a TTFont instance that already has a
'name' table. If a 'STAT' table already exists, it will be
@ -2797,6 +2801,20 @@ def _addName(nameTable, value, minNameID=0):
names = dict(en=value)
elif isinstance(value, dict):
names = value
elif isinstance(value, list):
nameID = nameTable._findUnusedNameID()
for nameRecord in value:
if isinstance(nameRecord, STATNameStatement):
nameTable.setName(
nameRecord.string,
nameID,
nameRecord.platformID,
nameRecord.platEncID,
nameRecord.langID,
)
else:
raise TypeError("value must be a list of STATNameStatements")
return nameID
else:
raise TypeError("value must be int, str or dict")
raise TypeError("value must be int, str, dict or list")
return nameTable.addMultilingualName(names, minNameID=minNameID)

View File

@ -338,6 +338,18 @@ class NameID(UShort):
log.warning("name id %d missing from name table" % value)
xmlWriter.newline()
class STATFlags(UShort):
def xmlWrite(self, xmlWriter, font, value, name, attrs):
xmlWriter.simpletag(name, attrs + [("value", value)])
flags = []
if value & 0x01:
flags.append("OlderSiblingFontAttribute")
if value & 0x02:
flags.append("ElidableAxisValueName")
if flags:
xmlWriter.write(" ")
xmlWriter.comment(" ".join(flags))
xmlWriter.newline()
class FloatValue(SimpleValue):
@staticmethod
@ -1745,7 +1757,6 @@ converterMapping = {
"int8": Int8,
"int16": Short,
"uint8": UInt8,
"uint8": UInt8,
"uint16": UShort,
"uint24": UInt24,
"uint32": ULong,
@ -1770,6 +1781,7 @@ converterMapping = {
"LookupFlag": LookupFlag,
"ExtendMode": ExtendMode,
"CompositeMode": CompositeMode,
"STATFlags": STATFlags,
# AAT
"CIDGlyphMap": CIDGlyphMap,

View File

@ -872,7 +872,7 @@ otData = [
('AxisValueFormat1', [
('uint16', 'Format', None, None, 'Format, = 1'),
('uint16', 'AxisIndex', None, None, 'Index into the axis record array identifying the axis of design variation to which the axis value record applies.'),
('uint16', 'Flags', None, None, 'Flags.'),
('STATFlags', 'Flags', None, None, 'Flags.'),
('NameID', 'ValueNameID', None, None, ''),
('Fixed', 'Value', None, None, ''),
]),
@ -880,7 +880,7 @@ otData = [
('AxisValueFormat2', [
('uint16', 'Format', None, None, 'Format, = 2'),
('uint16', 'AxisIndex', None, None, 'Index into the axis record array identifying the axis of design variation to which the axis value record applies.'),
('uint16', 'Flags', None, None, 'Flags.'),
('STATFlags', 'Flags', None, None, 'Flags.'),
('NameID', 'ValueNameID', None, None, ''),
('Fixed', 'NominalValue', None, None, ''),
('Fixed', 'RangeMinValue', None, None, ''),
@ -890,7 +890,7 @@ otData = [
('AxisValueFormat3', [
('uint16', 'Format', None, None, 'Format, = 3'),
('uint16', 'AxisIndex', None, None, 'Index into the axis record array identifying the axis of design variation to which the axis value record applies.'),
('uint16', 'Flags', None, None, 'Flags.'),
('STATFlags', 'Flags', None, None, 'Flags.'),
('NameID', 'ValueNameID', None, None, ''),
('Fixed', 'Value', None, None, ''),
('Fixed', 'LinkedValue', None, None, ''),
@ -899,7 +899,7 @@ otData = [
('AxisValueFormat4', [
('uint16', 'Format', None, None, 'Format, = 4'),
('uint16', 'AxisCount', None, None, 'The total number of axes contributing to this axis-values combination.'),
('uint16', 'Flags', None, None, 'Flags.'),
('STATFlags', 'Flags', None, None, 'Flags.'),
('NameID', 'ValueNameID', None, None, ''),
('struct', 'AxisValueRecord', 'AxisCount', 0, 'Array of AxisValue records that provide the combination of axis values, one for each contributing axis. '),
]),

4
Tests/feaLib/STAT2.fea Normal file
View File

@ -0,0 +1,4 @@
table STAT {
ElidedFallbackName { name "Roman"; };
DesignAxis zonk 0 { name "Zonkey"; };'
} STAT;

View File

@ -9,6 +9,7 @@ from fontTools.feaLib import ast
from fontTools.feaLib.lexer import Lexer
import difflib
import os
import re
import shutil
import sys
import tempfile
@ -73,7 +74,7 @@ class BuilderTest(unittest.TestCase):
LigatureSubtable AlternateSubtable MultipleSubstSubtable
SingleSubstSubtable aalt_chain_contextual_subst AlternateChained
MultipleLookupsPerGlyph MultipleLookupsPerGlyph2 GSUB_6_formats
GSUB_5_formats delete_glyph
GSUB_5_formats delete_glyph STAT_test STAT_test_elidedFallbackNameID
""".split()
def __init__(self, methodName):
@ -118,7 +119,7 @@ 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', 'hhea', 'vhea'])
'GPOS', 'OS/2', 'STAT', 'hhea', 'vhea'])
actual = self.read_ttx(path)
expected = self.read_ttx(expected_ttx)
if replace:
@ -463,6 +464,201 @@ class BuilderTest(unittest.TestCase):
"} test;"
)
def test_STAT_elidedfallbackname_already_defined(self):
self.assertRaisesRegex(
FeatureLibError,
'ElidedFallbackName is already set.',
self.build,
'table name {'
' nameid 256 "Roman"; '
'} name;'
'table STAT {'
' ElidedFallbackName { name "Roman"; };'
' ElidedFallbackNameID 256;'
'} STAT;')
def test_STAT_elidedfallbackname_set_twice(self):
self.assertRaisesRegex(
FeatureLibError,
'ElidedFallbackName is already set.',
self.build,
'table name {'
' nameid 256 "Roman"; '
'} name;'
'table STAT {'
' ElidedFallbackName { name "Roman"; };'
' ElidedFallbackName { name "Italic"; };'
'} STAT;')
def test_STAT_elidedfallbacknameID_already_defined(self):
self.assertRaisesRegex(
FeatureLibError,
'ElidedFallbackNameID is already set.',
self.build,
'table name {'
' nameid 256 "Roman"; '
'} name;'
'table STAT {'
' ElidedFallbackNameID 256;'
' ElidedFallbackName { name "Roman"; };'
'} STAT;')
def test_STAT_elidedfallbacknameID_not_in_name_table(self):
self.assertRaisesRegex(
FeatureLibError,
'ElidedFallbackNameID 256 points to a nameID that does not '
'exist in the "name" table',
self.build,
'table name {'
' nameid 257 "Roman"; '
'} name;'
'table STAT {'
' ElidedFallbackNameID 256;'
' DesignAxis opsz 1 { name "Optical Size"; };'
'} STAT;')
def test_STAT_design_axis_name(self):
self.assertRaisesRegex(
FeatureLibError,
'Expected "name"',
self.build,
'table name {'
' nameid 256 "Roman"; '
'} name;'
'table STAT {'
' ElidedFallbackName { name "Roman"; };'
' DesignAxis opsz 0 { badtag "Optical Size"; };'
'} STAT;')
def test_STAT_duplicate_design_axis_name(self):
self.assertRaisesRegex(
FeatureLibError,
'DesignAxis already defined for tag "opsz".',
self.build,
'table name {'
' nameid 256 "Roman"; '
'} name;'
'table STAT {'
' ElidedFallbackName { name "Roman"; };'
' DesignAxis opsz 0 { name "Optical Size"; };'
' DesignAxis opsz 1 { name "Optical Size"; };'
'} STAT;')
def test_STAT_design_axis_duplicate_order(self):
self.assertRaisesRegex(
FeatureLibError,
"DesignAxis already defined for axis number 0.",
self.build,
'table name {'
' nameid 256 "Roman"; '
'} name;'
'table STAT {'
' ElidedFallbackName { name "Roman"; };'
' DesignAxis opsz 0 { name "Optical Size"; };'
' DesignAxis wdth 0 { name "Width"; };'
' AxisValue {'
' location opsz 8;'
' location wdth 400;'
' name "Caption";'
' };'
'} STAT;')
def test_STAT_undefined_tag(self):
self.assertRaisesRegex(
FeatureLibError,
'DesignAxis not defined for wdth.',
self.build,
'table name {'
' nameid 256 "Roman"; '
'} name;'
'table STAT {'
' ElidedFallbackName { name "Roman"; };'
' DesignAxis opsz 0 { name "Optical Size"; };'
' AxisValue { '
' location wdth 125; '
' name "Wide"; '
' };'
'} STAT;')
def test_STAT_axis_value_format4(self):
self.assertRaisesRegex(
FeatureLibError,
'Axis tag wdth already defined.',
self.build,
'table name {'
' nameid 256 "Roman"; '
'} 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; '
' name "Caption Medium Wide"; '
' };'
'} 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.',
self.build,
'table name {'
' nameid 256 "Roman"; '
'} name;'
'table STAT {'
' ElidedFallbackName { name "Roman"; };'
' DesignAxis opsz 0 { name "Optical Size"; };'
' DesignAxis wdth 1 { name "Width"; };'
' AxisValue {'
' location opsz 8;'
' location wdth 400;'
' name "Caption";'
' };'
' AxisValue {'
' location wdth 400;'
' location opsz 8;'
' name "Caption";'
' };'
'} STAT;')
def test_STAT_axis_value_missing_location(self):
self.assertRaisesRegex(
FeatureLibError,
'Expected "Axis location"',
self.build,
'table name {'
' nameid 256 "Roman"; '
'} name;'
'table STAT {'
' ElidedFallbackName { name "Roman"; '
'};'
' DesignAxis opsz 0 { name "Optical Size"; };'
' AxisValue { '
' name "Wide"; '
' };'
'} STAT;')
def test_STAT_invalid_location_tag(self):
self.assertRaisesRegex(
FeatureLibError,
'Tags cannot be longer than 4 characters',
self.build,
'table name {'
' nameid 256 "Roman"; '
'} name;'
'table STAT {'
' ElidedFallbackName { name "Roman"; '
' name 3 1 0x0411 "ローマン"; }; '
' DesignAxis width 0 { name "Width"; };'
'} STAT;')
def test_extensions(self):
class ast_BaseClass(ast.MarkClass):
def asFea(self, indent=""):

View File

@ -0,0 +1,96 @@
# bad fea file: Testing DesignAxis tag with incorrect label
table name {
nameid 25 "TestFont";
} name;
table STAT {
ElidedFallbackName { name "Roman"; };
DesignAxis opsz 0 { badtag "Optical Size"; }; #'badtag' instead of 'name' is incorrect
DesignAxis wdth 1 { name "Width"; };
DesignAxis wght 2 { name "Weight"; };
DesignAxis ital 3 { name "Italic"; };
AxisValue {
location opsz 8 5 9;
location wdth 300 350 450;
name "Caption";
};
AxisValue {
location opsz 11 9 12;
name "Text";
flag OlderSiblingFontAttribute ElidableAxisValueName ;
};
AxisValue {
location opsz 16.7 12 24;
name "Subhead";
};
AxisValue {
location opsz 72 24 72;
name "Display";
};
AxisValue {
location wdth 80 80 89;
name "Condensed";
};
AxisValue {
location wdth 90 90 96;
name "Semicondensed";
};
AxisValue {
location wdth 100 97 101;
name "Normal";
flag ElidableAxisValueName;
};
AxisValue {
location wdth 125 102 125;
name "Extended";
};
AxisValue {
location wght 300 300 349;
name "Light";
};
AxisValue {
location wght 400 350 449;
name "Regular";
flag ElidableAxisValueName;
};
AxisValue {
location wght 500 450 549;
name "Medium";
};
AxisValue {
location wght 600 550 649;
name "Semibold";
};
AxisValue {
location wght 700 650 749;
name "Bold";
};
AxisValue {
location wght 900 750 900;
name "Black";
};
AxisValue {
location ital 0;
name "Roman";
flag ElidableAxisValueName;
};
} STAT;

View File

@ -0,0 +1,109 @@
table name {
nameid 25 "TestFont";
} name;
table STAT {
ElidedFallbackName {
name "Roman";
name 3 1 1041 "ローマン";
};
DesignAxis opsz 0 {
name "Optical Size";
};
DesignAxis wdth 1 {
name "Width";
};
DesignAxis wght 2 {
name "Weight";
};
DesignAxis ital 3 {
name "Italic";
}; # here comment
AxisValue {
location opsz 8; # comment here
location wdth 400; # another comment
name "Caption"; # more comments
};
AxisValue {
location opsz 11 9 12;
name "Text";
flag OlderSiblingFontAttribute ElidableAxisValueName;
};
AxisValue {
location opsz 16.7 12 24;
name "Subhead";
};
AxisValue {
location opsz 72 24 72;
name "Display";
};
AxisValue {
location wdth 80 80 89;
name "Condensed";
};
AxisValue {
location wdth 90 90 96;
name "Semicondensed";
};
AxisValue {
location wdth 100 97 101;
name "Normal";
flag ElidableAxisValueName;
};
AxisValue {
location wdth 125 102 125;
name "Extended";
};
AxisValue {
location wght 300 300 349;
name "Light";
};
AxisValue {
location wght 400 350 449;
name "Regular";
flag ElidableAxisValueName;
};
AxisValue {
location wght 500 450 549;
name "Medium";
};
AxisValue {
location wght 600 550 649;
name "Semibold";
};
AxisValue {
location wght 700 650 749;
name "Bold";
};
AxisValue {
location wght 900 750 900;
name "Black";
};
AxisValue {
location ital 0;
name "Roman";
flag ElidableAxisValueName; # flag comment
};
} STAT;

View File

@ -0,0 +1,228 @@
<?xml version="1.0" encoding="UTF-8"?>
<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="4.20">
<name>
<namerecord nameID="25" platformID="3" platEncID="1" langID="0x409">
TestFont
</namerecord>
<namerecord nameID="256" platformID="3" platEncID="1" langID="0x409">
Roman
</namerecord>
<namerecord nameID="256" platformID="3" platEncID="1" langID="0x411">
ローマン
</namerecord>
<namerecord nameID="257" platformID="3" platEncID="1" langID="0x409">
Optical Size
</namerecord>
<namerecord nameID="258" platformID="3" platEncID="1" langID="0x409">
Text
</namerecord>
<namerecord nameID="259" platformID="3" platEncID="1" langID="0x409">
Subhead
</namerecord>
<namerecord nameID="260" platformID="3" platEncID="1" langID="0x409">
Display
</namerecord>
<namerecord nameID="261" platformID="3" platEncID="1" langID="0x409">
Width
</namerecord>
<namerecord nameID="262" platformID="3" platEncID="1" langID="0x409">
Condensed
</namerecord>
<namerecord nameID="263" platformID="3" platEncID="1" langID="0x409">
Semicondensed
</namerecord>
<namerecord nameID="264" platformID="3" platEncID="1" langID="0x409">
Normal
</namerecord>
<namerecord nameID="265" platformID="3" platEncID="1" langID="0x409">
Extended
</namerecord>
<namerecord nameID="266" platformID="3" platEncID="1" langID="0x409">
Weight
</namerecord>
<namerecord nameID="267" platformID="3" platEncID="1" langID="0x409">
Light
</namerecord>
<namerecord nameID="268" platformID="3" platEncID="1" langID="0x409">
Regular
</namerecord>
<namerecord nameID="269" platformID="3" platEncID="1" langID="0x409">
Medium
</namerecord>
<namerecord nameID="270" platformID="3" platEncID="1" langID="0x409">
Semibold
</namerecord>
<namerecord nameID="271" platformID="3" platEncID="1" langID="0x409">
Bold
</namerecord>
<namerecord nameID="272" platformID="3" platEncID="1" langID="0x409">
Black
</namerecord>
<namerecord nameID="273" platformID="3" platEncID="1" langID="0x409">
Italic
</namerecord>
<namerecord nameID="274" platformID="3" platEncID="1" langID="0x409">
Roman
</namerecord>
<namerecord nameID="275" platformID="3" platEncID="1" langID="0x409">
Caption
</namerecord>
</name>
<STAT>
<Version value="0x00010002"/>
<DesignAxisRecordSize value="8"/>
<!-- DesignAxisCount=4 -->
<DesignAxisRecord>
<Axis index="0">
<AxisTag value="opsz"/>
<AxisNameID value="257"/> <!-- Optical Size -->
<AxisOrdering value="0"/>
</Axis>
<Axis index="1">
<AxisTag value="wdth"/>
<AxisNameID value="261"/> <!-- Width -->
<AxisOrdering value="1"/>
</Axis>
<Axis index="2">
<AxisTag value="wght"/>
<AxisNameID value="266"/> <!-- Weight -->
<AxisOrdering value="2"/>
</Axis>
<Axis index="3">
<AxisTag value="ital"/>
<AxisNameID value="273"/> <!-- Italic -->
<AxisOrdering value="3"/>
</Axis>
</DesignAxisRecord>
<!-- AxisValueCount=15 -->
<AxisValueArray>
<AxisValue index="0" Format="4">
<!-- AxisCount=2 -->
<Flags value="0"/>
<ValueNameID value="275"/> <!-- Caption -->
<AxisValueRecord index="0">
<AxisIndex value="0"/>
<Value value="8.0"/>
</AxisValueRecord>
<AxisValueRecord index="1">
<AxisIndex value="1"/>
<Value value="400.0"/>
</AxisValueRecord>
</AxisValue>
<AxisValue index="1" Format="2">
<AxisIndex value="0"/>
<Flags value="3"/> <!-- OlderSiblingFontAttribute ElidableAxisValueName -->
<ValueNameID value="258"/> <!-- Text -->
<NominalValue value="11.0"/>
<RangeMinValue value="9.0"/>
<RangeMaxValue value="12.0"/>
</AxisValue>
<AxisValue index="2" Format="2">
<AxisIndex value="0"/>
<Flags value="0"/>
<ValueNameID value="259"/> <!-- Subhead -->
<NominalValue value="16.7"/>
<RangeMinValue value="12.0"/>
<RangeMaxValue value="24.0"/>
</AxisValue>
<AxisValue index="3" Format="2">
<AxisIndex value="0"/>
<Flags value="0"/>
<ValueNameID value="260"/> <!-- Display -->
<NominalValue value="72.0"/>
<RangeMinValue value="24.0"/>
<RangeMaxValue value="72.0"/>
</AxisValue>
<AxisValue index="4" Format="2">
<AxisIndex value="1"/>
<Flags value="0"/>
<ValueNameID value="262"/> <!-- Condensed -->
<NominalValue value="80.0"/>
<RangeMinValue value="80.0"/>
<RangeMaxValue value="89.0"/>
</AxisValue>
<AxisValue index="5" Format="2">
<AxisIndex value="1"/>
<Flags value="0"/>
<ValueNameID value="263"/> <!-- Semicondensed -->
<NominalValue value="90.0"/>
<RangeMinValue value="90.0"/>
<RangeMaxValue value="96.0"/>
</AxisValue>
<AxisValue index="6" Format="2">
<AxisIndex value="1"/>
<Flags value="2"/> <!-- ElidableAxisValueName -->
<ValueNameID value="264"/> <!-- Normal -->
<NominalValue value="100.0"/>
<RangeMinValue value="97.0"/>
<RangeMaxValue value="101.0"/>
</AxisValue>
<AxisValue index="7" Format="2">
<AxisIndex value="1"/>
<Flags value="0"/>
<ValueNameID value="265"/> <!-- Extended -->
<NominalValue value="125.0"/>
<RangeMinValue value="102.0"/>
<RangeMaxValue value="125.0"/>
</AxisValue>
<AxisValue index="8" Format="2">
<AxisIndex value="2"/>
<Flags value="0"/>
<ValueNameID value="267"/> <!-- Light -->
<NominalValue value="300.0"/>
<RangeMinValue value="300.0"/>
<RangeMaxValue value="349.0"/>
</AxisValue>
<AxisValue index="9" Format="2">
<AxisIndex value="2"/>
<Flags value="2"/> <!-- ElidableAxisValueName -->
<ValueNameID value="268"/> <!-- Regular -->
<NominalValue value="400.0"/>
<RangeMinValue value="350.0"/>
<RangeMaxValue value="449.0"/>
</AxisValue>
<AxisValue index="10" Format="2">
<AxisIndex value="2"/>
<Flags value="0"/>
<ValueNameID value="269"/> <!-- Medium -->
<NominalValue value="500.0"/>
<RangeMinValue value="450.0"/>
<RangeMaxValue value="549.0"/>
</AxisValue>
<AxisValue index="11" Format="2">
<AxisIndex value="2"/>
<Flags value="0"/>
<ValueNameID value="270"/> <!-- Semibold -->
<NominalValue value="600.0"/>
<RangeMinValue value="550.0"/>
<RangeMaxValue value="649.0"/>
</AxisValue>
<AxisValue index="12" Format="2">
<AxisIndex value="2"/>
<Flags value="0"/>
<ValueNameID value="271"/> <!-- Bold -->
<NominalValue value="700.0"/>
<RangeMinValue value="650.0"/>
<RangeMaxValue value="749.0"/>
</AxisValue>
<AxisValue index="13" Format="2">
<AxisIndex value="2"/>
<Flags value="0"/>
<ValueNameID value="272"/> <!-- Black -->
<NominalValue value="900.0"/>
<RangeMinValue value="750.0"/>
<RangeMaxValue value="900.0"/>
</AxisValue>
<AxisValue index="14" Format="1">
<AxisIndex value="3"/>
<Flags value="2"/> <!-- ElidableAxisValueName -->
<ValueNameID value="274"/> <!-- Roman -->
<Value value="0.0"/>
</AxisValue>
</AxisValueArray>
<ElidedFallbackNameID value="256"/> <!-- Roman -->
</STAT>
</ttFont>

View File

@ -0,0 +1,84 @@
table name {
nameid 25 "TestFont";
nameid 256 "Roman";
} name;
table STAT {
ElidedFallbackNameID 256;
DesignAxis opsz 0 {
name "Optical Size";
};
DesignAxis wdth 1 {
name "Width";
};
DesignAxis wght 2 {
name "Weight";
};
DesignAxis ital 3 {
name "Italic";
}; # here comment
AxisValue {
location opsz 8; # comment here
location wdth 400; # another comment
name "Caption"; # more comments
};
AxisValue {
location opsz 11 9 12;
name "Text";
flag OlderSiblingFontAttribute ElidableAxisValueName;
};
AxisValue {
location opsz 16.7 12 24;
name "Subhead";
};
AxisValue {
location opsz 72 24 72;
name "Display";
};
AxisValue {
location wdth 80 80 89;
name "Condensed";
};
AxisValue {
location wdth 90 90 96;
name "Semicondensed";
};
AxisValue {
location wdth 100 97 101;
name "Normal";
flag ElidableAxisValueName;
};
AxisValue {
location wdth 125 102 125;
name "Extended";
};
AxisValue {
location wght 300 300 349;
name "Light";
};
AxisValue {
location wght 400 350 449;
name "Regular";
flag ElidableAxisValueName;
};
AxisValue {
location wght 500 450 549;
name "Medium";
};
AxisValue {
location wght 600 550 649;
name "Semibold";
};
AxisValue {
location wght 700 650 749;
name "Bold";
};
AxisValue {
location wght 900 750 900;
name "Black";
};
AxisValue {
location ital 0;
name "Roman";
flag ElidableAxisValueName; # flag comment
};
} STAT;

View File

@ -0,0 +1,225 @@
<?xml version="1.0" encoding="UTF-8"?>
<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="4.20">
<name>
<namerecord nameID="25" platformID="3" platEncID="1" langID="0x409">
TestFont
</namerecord>
<namerecord nameID="256" platformID="3" platEncID="1" langID="0x409">
Roman
</namerecord>
<namerecord nameID="257" platformID="3" platEncID="1" langID="0x409">
Optical Size
</namerecord>
<namerecord nameID="258" platformID="3" platEncID="1" langID="0x409">
Text
</namerecord>
<namerecord nameID="259" platformID="3" platEncID="1" langID="0x409">
Subhead
</namerecord>
<namerecord nameID="260" platformID="3" platEncID="1" langID="0x409">
Display
</namerecord>
<namerecord nameID="261" platformID="3" platEncID="1" langID="0x409">
Width
</namerecord>
<namerecord nameID="262" platformID="3" platEncID="1" langID="0x409">
Condensed
</namerecord>
<namerecord nameID="263" platformID="3" platEncID="1" langID="0x409">
Semicondensed
</namerecord>
<namerecord nameID="264" platformID="3" platEncID="1" langID="0x409">
Normal
</namerecord>
<namerecord nameID="265" platformID="3" platEncID="1" langID="0x409">
Extended
</namerecord>
<namerecord nameID="266" platformID="3" platEncID="1" langID="0x409">
Weight
</namerecord>
<namerecord nameID="267" platformID="3" platEncID="1" langID="0x409">
Light
</namerecord>
<namerecord nameID="268" platformID="3" platEncID="1" langID="0x409">
Regular
</namerecord>
<namerecord nameID="269" platformID="3" platEncID="1" langID="0x409">
Medium
</namerecord>
<namerecord nameID="270" platformID="3" platEncID="1" langID="0x409">
Semibold
</namerecord>
<namerecord nameID="271" platformID="3" platEncID="1" langID="0x409">
Bold
</namerecord>
<namerecord nameID="272" platformID="3" platEncID="1" langID="0x409">
Black
</namerecord>
<namerecord nameID="273" platformID="3" platEncID="1" langID="0x409">
Italic
</namerecord>
<namerecord nameID="274" platformID="3" platEncID="1" langID="0x409">
Roman
</namerecord>
<namerecord nameID="275" platformID="3" platEncID="1" langID="0x409">
Caption
</namerecord>
</name>
<STAT>
<Version value="0x00010002"/>
<DesignAxisRecordSize value="8"/>
<!-- DesignAxisCount=4 -->
<DesignAxisRecord>
<Axis index="0">
<AxisTag value="opsz"/>
<AxisNameID value="257"/> <!-- Optical Size -->
<AxisOrdering value="0"/>
</Axis>
<Axis index="1">
<AxisTag value="wdth"/>
<AxisNameID value="261"/> <!-- Width -->
<AxisOrdering value="1"/>
</Axis>
<Axis index="2">
<AxisTag value="wght"/>
<AxisNameID value="266"/> <!-- Weight -->
<AxisOrdering value="2"/>
</Axis>
<Axis index="3">
<AxisTag value="ital"/>
<AxisNameID value="273"/> <!-- Italic -->
<AxisOrdering value="3"/>
</Axis>
</DesignAxisRecord>
<!-- AxisValueCount=15 -->
<AxisValueArray>
<AxisValue index="0" Format="4">
<!-- AxisCount=2 -->
<Flags value="0"/>
<ValueNameID value="275"/> <!-- Caption -->
<AxisValueRecord index="0">
<AxisIndex value="0"/>
<Value value="8.0"/>
</AxisValueRecord>
<AxisValueRecord index="1">
<AxisIndex value="1"/>
<Value value="400.0"/>
</AxisValueRecord>
</AxisValue>
<AxisValue index="1" Format="2">
<AxisIndex value="0"/>
<Flags value="3"/> <!-- OlderSiblingFontAttribute ElidableAxisValueName -->
<ValueNameID value="258"/> <!-- Text -->
<NominalValue value="11.0"/>
<RangeMinValue value="9.0"/>
<RangeMaxValue value="12.0"/>
</AxisValue>
<AxisValue index="2" Format="2">
<AxisIndex value="0"/>
<Flags value="0"/>
<ValueNameID value="259"/> <!-- Subhead -->
<NominalValue value="16.7"/>
<RangeMinValue value="12.0"/>
<RangeMaxValue value="24.0"/>
</AxisValue>
<AxisValue index="3" Format="2">
<AxisIndex value="0"/>
<Flags value="0"/>
<ValueNameID value="260"/> <!-- Display -->
<NominalValue value="72.0"/>
<RangeMinValue value="24.0"/>
<RangeMaxValue value="72.0"/>
</AxisValue>
<AxisValue index="4" Format="2">
<AxisIndex value="1"/>
<Flags value="0"/>
<ValueNameID value="262"/> <!-- Condensed -->
<NominalValue value="80.0"/>
<RangeMinValue value="80.0"/>
<RangeMaxValue value="89.0"/>
</AxisValue>
<AxisValue index="5" Format="2">
<AxisIndex value="1"/>
<Flags value="0"/>
<ValueNameID value="263"/> <!-- Semicondensed -->
<NominalValue value="90.0"/>
<RangeMinValue value="90.0"/>
<RangeMaxValue value="96.0"/>
</AxisValue>
<AxisValue index="6" Format="2">
<AxisIndex value="1"/>
<Flags value="2"/> <!-- ElidableAxisValueName -->
<ValueNameID value="264"/> <!-- Normal -->
<NominalValue value="100.0"/>
<RangeMinValue value="97.0"/>
<RangeMaxValue value="101.0"/>
</AxisValue>
<AxisValue index="7" Format="2">
<AxisIndex value="1"/>
<Flags value="0"/>
<ValueNameID value="265"/> <!-- Extended -->
<NominalValue value="125.0"/>
<RangeMinValue value="102.0"/>
<RangeMaxValue value="125.0"/>
</AxisValue>
<AxisValue index="8" Format="2">
<AxisIndex value="2"/>
<Flags value="0"/>
<ValueNameID value="267"/> <!-- Light -->
<NominalValue value="300.0"/>
<RangeMinValue value="300.0"/>
<RangeMaxValue value="349.0"/>
</AxisValue>
<AxisValue index="9" Format="2">
<AxisIndex value="2"/>
<Flags value="2"/> <!-- ElidableAxisValueName -->
<ValueNameID value="268"/> <!-- Regular -->
<NominalValue value="400.0"/>
<RangeMinValue value="350.0"/>
<RangeMaxValue value="449.0"/>
</AxisValue>
<AxisValue index="10" Format="2">
<AxisIndex value="2"/>
<Flags value="0"/>
<ValueNameID value="269"/> <!-- Medium -->
<NominalValue value="500.0"/>
<RangeMinValue value="450.0"/>
<RangeMaxValue value="549.0"/>
</AxisValue>
<AxisValue index="11" Format="2">
<AxisIndex value="2"/>
<Flags value="0"/>
<ValueNameID value="270"/> <!-- Semibold -->
<NominalValue value="600.0"/>
<RangeMinValue value="550.0"/>
<RangeMaxValue value="649.0"/>
</AxisValue>
<AxisValue index="12" Format="2">
<AxisIndex value="2"/>
<Flags value="0"/>
<ValueNameID value="271"/> <!-- Bold -->
<NominalValue value="700.0"/>
<RangeMinValue value="650.0"/>
<RangeMaxValue value="749.0"/>
</AxisValue>
<AxisValue index="13" Format="2">
<AxisIndex value="2"/>
<Flags value="0"/>
<ValueNameID value="272"/> <!-- Black -->
<NominalValue value="900.0"/>
<RangeMinValue value="750.0"/>
<RangeMaxValue value="900.0"/>
</AxisValue>
<AxisValue index="14" Format="1">
<AxisIndex value="3"/>
<Flags value="2"/> <!-- ElidableAxisValueName -->
<ValueNameID value="274"/> <!-- Roman -->
<Value value="0.0"/>
</AxisValue>
</AxisValueArray>
<ElidedFallbackNameID value="256"/> <!-- Roman -->
</STAT>
</ttFont>

View File

@ -1280,6 +1280,76 @@ class ParserTest(unittest.TestCase):
'"dflt" is not a valid script tag; use "DFLT" instead',
self.parse, "feature test {script dflt;} test;")
def test_stat_design_axis(self): # STAT DesignAxis
doc = self.parse('table STAT { DesignAxis opsz 0 '
'{name "Optical Size";}; } STAT;')
da = doc.statements[0].statements[0]
self.assertIsInstance(da, ast.STATDesignAxisStatement)
self.assertEqual(da.tag, 'opsz')
self.assertEqual(da.axisOrder, 0)
self.assertEqual(da.names[0].string, 'Optical Size')
def test_stat_axis_value_format1(self): # STAT AxisValue
doc = self.parse('table STAT { DesignAxis opsz 0 '
'{name "Optical Size";}; '
'AxisValue {location opsz 8; name "Caption";}; } '
'STAT;')
avr = doc.statements[0].statements[1]
self.assertIsInstance(avr, ast.STATAxisValueStatement)
self.assertEqual(avr.locations[0].tag, 'opsz')
self.assertEqual(avr.locations[0].values[0], 8)
self.assertEqual(avr.names[0].string, 'Caption')
def test_stat_axis_value_format2(self): # STAT AxisValue
doc = self.parse('table STAT { DesignAxis opsz 0 '
'{name "Optical Size";}; '
'AxisValue {location opsz 8 6 10; name "Caption";}; } '
'STAT;')
avr = doc.statements[0].statements[1]
self.assertIsInstance(avr, ast.STATAxisValueStatement)
self.assertEqual(avr.locations[0].tag, 'opsz')
self.assertEqual(avr.locations[0].values, [8, 6, 10])
self.assertEqual(avr.names[0].string, 'Caption')
def test_stat_axis_value_format2_bad_range(self): # STAT AxisValue
self.assertRaisesRegex(
FeatureLibError,
'Default value 5 is outside of specified range 6-10.',
self.parse, 'table STAT { DesignAxis opsz 0 '
'{name "Optical Size";}; '
'AxisValue {location opsz 5 6 10; name "Caption";}; } '
'STAT;')
def test_stat_axis_value_format4(self): # STAT AxisValue
self.assertRaisesRegex(
FeatureLibError,
'Only one value is allowed in a Format 4 Axis Value Record, but 3 were found.',
self.parse, 'table STAT { '
'DesignAxis opsz 0 {name "Optical Size";}; '
'DesignAxis wdth 0 {name "Width";}; '
'AxisValue {'
'location opsz 8 6 10; '
'location wdth 400; '
'name "Caption";}; } '
'STAT;')
def test_stat_elidedfallbackname(self): # STAT ElidedFallbackName
doc = self.parse('table STAT { ElidedFallbackName {name "Roman"; '
'name 3 1 0x0411 "ローマン"; }; '
'} STAT;')
nameRecord = doc.statements[0].statements[0]
self.assertIsInstance(nameRecord, ast.ElidedFallbackName)
self.assertEqual(nameRecord.names[0].string, 'Roman')
self.assertEqual(nameRecord.names[1].string, 'ローマン')
def test_stat_elidedfallbacknameid(self): # STAT ElidedFallbackNameID
doc = self.parse('table name { nameid 278 "Roman"; } name; '
'table STAT { ElidedFallbackNameID 278; '
'} STAT;')
nameRecord = doc.statements[0].statements[0]
self.assertIsInstance(nameRecord, ast.NameRecord)
self.assertEqual(nameRecord.string, 'Roman')
def test_sub_single_format_a(self): # GSUB LookupType 1
doc = self.parse("feature smcp {substitute a by a.sc;} smcp;")
sub = doc.statements[0].statements[0]

View File

@ -393,7 +393,7 @@
<AxisValueArray>
<AxisValue index="0" Format="1">
<AxisIndex value="0"/>
<Flags value="2"/>
<Flags value="2"/> <!-- ElidableAxisValueName -->
<ValueNameID value="261"/> <!-- Neutral -->
<Value value="0.0"/>
</AxisValue>
@ -405,7 +405,7 @@
</AxisValue>
<AxisValue index="2" Format="1">
<AxisIndex value="1"/>
<Flags value="2"/>
<Flags value="2"/> <!-- ElidableAxisValueName -->
<ValueNameID value="261"/> <!-- Neutral -->
<Value value="0.0"/>
</AxisValue>
@ -417,7 +417,7 @@
</AxisValue>
<AxisValue index="4" Format="1">
<AxisIndex value="2"/>
<Flags value="2"/>
<Flags value="2"/> <!-- ElidableAxisValueName -->
<ValueNameID value="261"/> <!-- Neutral -->
<Value value="0.0"/>
</AxisValue>
@ -429,7 +429,7 @@
</AxisValue>
<AxisValue index="6" Format="1">
<AxisIndex value="3"/>
<Flags value="2"/>
<Flags value="2"/> <!-- ElidableAxisValueName -->
<ValueNameID value="261"/> <!-- Neutral -->
<Value value="0.0"/>
</AxisValue>

View File

@ -1138,7 +1138,7 @@ buildStatTable_test_data = [
' </AxisValue>',
' <AxisValue index="1" Format="1">',
' <AxisIndex value="0"/>',
' <Flags value="2"/>',
' <Flags value="2"/> <!-- ElidableAxisValueName -->',
' <ValueNameID value="256"/> <!-- Regular -->',
' <Value value="400.0"/>',
' </AxisValue>',
@ -1193,7 +1193,7 @@ buildStatTable_test_data = [
' </AxisValue>',
' <AxisValue index="1" Format="1">',
' <AxisIndex value="0"/>',
' <Flags value="2"/>',
' <Flags value="2"/> <!-- ElidableAxisValueName -->',
' <ValueNameID value="258"/> <!-- Regular -->',
' <Value value="400.0"/>',
' </AxisValue>',
@ -1211,7 +1211,7 @@ buildStatTable_test_data = [
' </AxisValue>',
' <AxisValue index="4" Format="1">',
' <AxisIndex value="1"/>',
' <Flags value="2"/>',
' <Flags value="2"/> <!-- ElidableAxisValueName -->',
' <ValueNameID value="258"/> <!-- Regular -->',
' <Value value="100.0"/>',
' </AxisValue>',
@ -1246,7 +1246,7 @@ buildStatTable_test_data = [
' <AxisValueArray>',
' <AxisValue index="0" Format="1">',
' <AxisIndex value="0"/>',
' <Flags value="2"/>',
' <Flags value="2"/> <!-- ElidableAxisValueName -->',
' <ValueNameID value="257"/> <!-- Regular -->',
' <Value value="400.0"/>',
' </AxisValue>',
@ -1291,7 +1291,7 @@ buildStatTable_test_data = [
' </AxisValue>',
' <AxisValue index="1" Format="2">',
' <AxisIndex value="0"/>',
' <Flags value="2"/>',
' <Flags value="2"/> <!-- ElidableAxisValueName -->',
' <ValueNameID value="258"/> <!-- Text -->',
' <NominalValue value="14.0"/>',
' <RangeMinValue value="10.0"/>',
@ -1354,7 +1354,7 @@ buildStatTable_test_data = [
' </AxisValue>',
' <AxisValue index="1" Format="1">',
' <AxisIndex value="1"/>',
' <Flags value="2"/>',
' <Flags value="2"/> <!-- ElidableAxisValueName -->',
' <ValueNameID value="258"/> <!-- Regular -->',
' <Value value="100.0"/>',
' </AxisValue>',

View File

@ -147,7 +147,7 @@ STAT_XML_AXIS_VALUE_FORMAT3 = [
'<AxisValueArray>',
' <AxisValue index="0" Format="3">',
' <AxisIndex value="0"/>',
' <Flags value="2"/>',
' <Flags value="2"/> <!-- ElidableAxisValueName -->',
' <ValueNameID value="2"/>',
' <Value value="400.0"/>',
' <LinkedValue value="700.0"/>',
@ -191,7 +191,7 @@ STAT_XML_VERSION_1_1 = [
'<AxisValueArray>',
' <AxisValue index="0" Format="3">',
' <AxisIndex value="0"/>',
' <Flags value="2"/>',
' <Flags value="2"/> <!-- ElidableAxisValueName -->',
' <ValueNameID value="2"/>',
' <Value value="400.0"/>',
' <LinkedValue value="700.0"/>',

View File

@ -531,7 +531,7 @@
</AxisValue>
<AxisValue index="1" Format="2">
<AxisIndex value="1"/>
<Flags value="2"/>
<Flags value="2"/> <!-- ElidableAxisValueName -->
<ValueNameID value="261"/> <!-- Regular -->
<NominalValue value="100.0"/>
<RangeMinValue value="93.75"/>

View File

@ -519,14 +519,14 @@
<AxisValueArray>
<AxisValue index="0" Format="3">
<AxisIndex value="0"/>
<Flags value="2"/>
<Flags value="2"/> <!-- ElidableAxisValueName -->
<ValueNameID value="261"/> <!-- Regular -->
<Value value="400.0"/>
<LinkedValue value="700.0"/>
</AxisValue>
<AxisValue index="1" Format="2">
<AxisIndex value="1"/>
<Flags value="2"/>
<Flags value="2"/> <!-- ElidableAxisValueName -->
<ValueNameID value="261"/> <!-- Regular -->
<NominalValue value="100.0"/>
<RangeMinValue value="93.75"/>

View File

@ -525,7 +525,7 @@
<AxisValueArray>
<AxisValue index="0" Format="3">
<AxisIndex value="0"/>
<Flags value="2"/>
<Flags value="2"/> <!-- ElidableAxisValueName -->
<ValueNameID value="261"/> <!-- Regular -->
<Value value="400.0"/>
<LinkedValue value="700.0"/>

View File

@ -531,7 +531,7 @@
</AxisValue>
<AxisValue index="1" Format="2">
<AxisIndex value="1"/>
<Flags value="2"/>
<Flags value="2"/> <!-- ElidableAxisValueName -->
<ValueNameID value="261"/> <!-- Regular -->
<NominalValue value="100.0"/>
<RangeMinValue value="93.75"/>