[otlLib.builder] Add function to build STAT table from high-level description (#1926)

* added a function to build a STAT table: `fontTools.otlLib.builder.buildStatTable()`
* make `varLib._add_stat()` a client of `buildStatTable()`
This commit is contained in:
Just van Rossum 2020-05-09 16:08:11 +02:00 committed by GitHub
parent 50c77c138e
commit d6bb38c7e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 585 additions and 23 deletions

View File

@ -803,6 +803,15 @@ class FontBuilder(object):
nameTable=self.font.get("name") nameTable=self.font.get("name")
) )
def setupStat(self, axes, locations=None, elidedFallbackName=2):
"""Build a new 'STAT' table.
See `fontTools.otlLib.builder.buildStatTable` for details about
the arguments.
"""
from .otlLib.builder import buildStatTable
buildStatTable(self.font, axes, locations, elidedFallbackName)
def buildCmapSubTable(cmapping, format, platformID, platEncID): def buildCmapSubTable(cmapping, format, platformID, platEncID):
subTable = cmap_classes[format](format) subTable = cmap_classes[format](format)

View File

@ -1,4 +1,5 @@
from collections import namedtuple from collections import namedtuple
from fontTools.misc.fixedTools import fixedToFloat
from fontTools import ttLib from fontTools import ttLib
from fontTools.ttLib.tables import otTables as ot from fontTools.ttLib.tables import otTables as ot
from fontTools.ttLib.tables.otBase import ValueRecord, valueRecordFormatDict from fontTools.ttLib.tables.otBase import ValueRecord, valueRecordFormatDict
@ -657,3 +658,193 @@ class ClassDefBuilder(object):
classDef = ot.ClassDef() classDef = ot.ClassDef()
classDef.classDefs = glyphClasses classDef.classDefs = glyphClasses
return classDef return classDef
AXIS_VALUE_NEGATIVE_INFINITY = fixedToFloat(-0x80000000, 16)
AXIS_VALUE_POSITIVE_INFINITY = fixedToFloat(0x7FFFFFFF, 16)
def buildStatTable(ttFont, axes, locations=None, elidedFallbackName=2):
"""Add a 'STAT' table to 'ttFont'.
'axes' is a list of dictionaries describing axes and their
values.
Example:
axes = [
dict(
tag="wght",
name="Weight",
ordering=0, # optional
values=[
dict(value=100, name='Thin'),
dict(value=300, name='Light'),
dict(value=400, name='Regular', flags=0x2),
dict(value=900, name='Black'),
],
)
]
Each axis dict must have 'tag' and 'name' items. 'tag' maps
to the 'AxisTag' field. 'name' can be a name ID (int), a string,
or a dictionary containing multilingual names (see the
addMultilingualName() name table method), and will translate to
the AxisNameID field.
An axis dict may contain an 'ordering' item that maps to the
AxisOrdering field. If omitted, the order of the axes list is
used to calculate AxisOrdering fields.
The axis dict may contain a 'values' item, which is a list of
dictionaries describing AxisValue records belonging to this axis.
Each value dict must have a 'name' item, which can be a name ID
(int), a string, or a dictionary containing multilingual names,
like the axis name. It translates to the ValueNameID field.
Optionally the value dict can contain a 'flags' item. It maps to
the AxisValue Flags field, and will be 0 when omitted.
The format of the AxisValue is determined by the remaining contents
of the value dictionary:
If the value dict contains a 'value' item, an AxisValue record
Format 1 is created. If in addition to the 'value' item it contains
a 'linkedValue' item, an AxisValue record Format 3 is built.
If the value dict contains a 'nominalValue' item, an AxisValue
record Format 2 is built. Optionally it may contain 'rangeMinValue'
and 'rangeMaxValue' items. These map to -Infinity and +Infinity
respectively if omitted.
You cannot specify Format 4 AxisValue tables this way, as they are
not tied to a single axis, and specify a name for a location that
is defined by multiple axes values. Instead, you need to supply the
'locations' argument.
The optional 'locations' argument specifies AxisValue Format 4
tables. It should be a list of dicts, where each dict has a 'name'
item, which works just like the value dicts above, an optional
'flags' item (defaulting to 0x0), and a 'location' dict. A
location dict key is an axis tag, and the associated value is the
location on the specified axis. They map to the AxisIndex and Value
fields of the AxisValueRecord.
Example:
locations = [
dict(name='Regular ABCD', location=dict(wght=300, ABCD=100)),
dict(name='Bold ABCD XYZ', location=dict(wght=600, ABCD=200)),
]
The optional 'elidedFallbackName' argument can be a name ID (int),
a string, or a dictionary containing multilingual names. 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
overwritten by the newly created one.
"""
ttFont["STAT"] = ttLib.newTable("STAT")
statTable = ttFont["STAT"].table = ot.STAT()
nameTable = ttFont["name"]
statTable.ElidedFallbackNameID = _addName(nameTable, elidedFallbackName)
# 'locations' contains data for AxisValue Format 4
axisRecords, axisValues = _buildAxisRecords(axes, nameTable)
if not locations:
statTable.Version = 0x00010001
else:
# We'll be adding Format 4 AxisValue records, which
# requires a higher table version
statTable.Version = 0x00010002
multiAxisValues = _buildAxisValuesFormat4(locations, axes, nameTable)
axisValues = multiAxisValues + axisValues
# Store AxisRecords
axisRecordArray = ot.AxisRecordArray()
axisRecordArray.Axis = axisRecords
# XXX these should not be hard-coded but computed automatically
statTable.DesignAxisRecordSize = 8
statTable.DesignAxisRecord = axisRecordArray
statTable.DesignAxisCount = len(axisRecords)
if axisValues:
# Store AxisValueRecords
axisValueArray = ot.AxisValueArray()
axisValueArray.AxisValue = axisValues
statTable.AxisValueArray = axisValueArray
statTable.AxisValueCount = len(axisValues)
def _buildAxisRecords(axes, nameTable):
axisRecords = []
axisValues = []
for axisRecordIndex, axisDict in enumerate(axes):
axis = ot.AxisRecord()
axis.AxisTag = axisDict["tag"]
axis.AxisNameID = _addName(nameTable, axisDict["name"])
axis.AxisOrdering = axisDict.get("ordering", axisRecordIndex)
axisRecords.append(axis)
for axisVal in axisDict.get("values", ()):
axisValRec = ot.AxisValue()
axisValRec.AxisIndex = axisRecordIndex
axisValRec.Flags = axisVal.get("flags", 0)
axisValRec.ValueNameID = _addName(nameTable, axisVal['name'])
if "value" in axisVal:
axisValRec.Value = axisVal["value"]
if "linkedValue" in axisVal:
axisValRec.Format = 3
axisValRec.LinkedValue = axisVal["linkedValue"]
else:
axisValRec.Format = 1
elif "nominalValue" in axisVal:
axisValRec.Format = 2
axisValRec.NominalValue = axisVal["nominalValue"]
axisValRec.RangeMinValue = axisVal.get("rangeMinValue", AXIS_VALUE_NEGATIVE_INFINITY)
axisValRec.RangeMaxValue = axisVal.get("rangeMaxValue", AXIS_VALUE_POSITIVE_INFINITY)
else:
raise ValueError("Can't determine format for AxisValue")
axisValues.append(axisValRec)
return axisRecords, axisValues
def _buildAxisValuesFormat4(locations, axes, nameTable):
axisTagToIndex = {}
for axisRecordIndex, axisDict in enumerate(axes):
axisTagToIndex[axisDict["tag"]] = axisRecordIndex
axisValues = []
for axisLocationDict in locations:
axisValRec = ot.AxisValue()
axisValRec.Format = 4
axisValRec.ValueNameID = _addName(nameTable, axisLocationDict['name'])
axisValRec.Flags = axisLocationDict.get("flags", 0)
axisValueRecords = []
for tag, value in axisLocationDict["location"].items():
avr = ot.AxisValueRecord()
avr.AxisIndex = axisTagToIndex[tag]
avr.Value = value
axisValueRecords.append(avr)
axisValueRecords.sort(key=lambda avr: avr.AxisIndex)
axisValRec.AxisCount = len(axisValueRecords)
axisValRec.AxisValueRecord = axisValueRecords
axisValues.append(axisValRec)
return axisValues
def _addName(nameTable, value):
if isinstance(value, int):
# Already a nameID
return value
if isinstance(value, str):
names = dict(en=value)
elif isinstance(value, dict):
names = value
else:
raise TypeError("value must be int, str or dict")
return nameTable.addMultilingualName(names)

View File

@ -202,30 +202,10 @@ def _add_stat(font, axes):
if "STAT" in font: if "STAT" in font:
return return
from ..otlLib.builder import buildStatTable
fvarTable = font['fvar'] fvarTable = font['fvar']
axes = [dict(tag=a.axisTag, name=a.axisNameID) for a in fvarTable.axes]
STAT = font["STAT"] = newTable('STAT') buildStatTable(font, axes)
stat = STAT.table = ot.STAT()
stat.Version = 0x00010001
axisRecords = []
for i, a in enumerate(fvarTable.axes):
axis = ot.AxisRecord()
axis.AxisTag = Tag(a.axisTag)
axis.AxisNameID = a.axisNameID
axis.AxisOrdering = i
axisRecords.append(axis)
axisRecordArray = ot.AxisRecordArray()
axisRecordArray.Axis = axisRecords
# XXX these should not be hard-coded but computed automatically
stat.DesignAxisRecordSize = 8
stat.DesignAxisCount = len(axisRecords)
stat.DesignAxisRecord = axisRecordArray
# for the elided fallback name, we default to the base style name.
# TODO make this user-configurable via designspace document
stat.ElidedFallbackNameID = 2
def _add_gvar(font, masterModel, master_ttfs, tolerance=0.5, optimize=True): def _add_gvar(font, masterModel, master_ttfs, tolerance=0.5, optimize=True):

View File

@ -204,6 +204,9 @@
<namerecord nameID="261" platformID="1" platEncID="0" langID="0x0" unicode="True"> <namerecord nameID="261" platformID="1" platEncID="0" langID="0x0" unicode="True">
Right Up Right Up
</namerecord> </namerecord>
<namerecord nameID="262" platformID="1" platEncID="0" langID="0x0" unicode="True">
Neutral
</namerecord>
<namerecord nameID="1" platformID="1" platEncID="0" langID="0x4" unicode="True"> <namerecord nameID="1" platformID="1" platEncID="0" langID="0x4" unicode="True">
HalloTestFont HalloTestFont
</namerecord> </namerecord>
@ -237,6 +240,9 @@
<namerecord nameID="261" platformID="3" platEncID="1" langID="0x409"> <namerecord nameID="261" platformID="3" platEncID="1" langID="0x409">
Right Up Right Up
</namerecord> </namerecord>
<namerecord nameID="262" platformID="3" platEncID="1" langID="0x409">
Neutral
</namerecord>
<namerecord nameID="1" platformID="3" platEncID="1" langID="0x413"> <namerecord nameID="1" platformID="3" platEncID="1" langID="0x413">
HalloTestFont HalloTestFont
</namerecord> </namerecord>
@ -363,6 +369,86 @@
</FeatureVariations> </FeatureVariations>
</GSUB> </GSUB>
<STAT>
<Version value="0x00010001"/>
<DesignAxisRecordSize value="8"/>
<!-- DesignAxisCount=4 -->
<DesignAxisRecord>
<Axis index="0">
<AxisTag value="LEFT"/>
<AxisNameID value="256"/> <!-- Left -->
<AxisOrdering value="0"/>
</Axis>
<Axis index="1">
<AxisTag value="RGHT"/>
<AxisNameID value="257"/> <!-- Right -->
<AxisOrdering value="1"/>
</Axis>
<Axis index="2">
<AxisTag value="UPPP"/>
<AxisNameID value="258"/> <!-- Up -->
<AxisOrdering value="2"/>
</Axis>
<Axis index="3">
<AxisTag value="DOWN"/>
<AxisNameID value="259"/> <!-- Down -->
<AxisOrdering value="3"/>
</Axis>
</DesignAxisRecord>
<!-- AxisValueCount=8 -->
<AxisValueArray>
<AxisValue index="0" Format="1">
<AxisIndex value="0"/>
<Flags value="2"/>
<ValueNameID value="262"/> <!-- Neutral -->
<Value value="0.0"/>
</AxisValue>
<AxisValue index="1" Format="1">
<AxisIndex value="0"/>
<Flags value="0"/>
<ValueNameID value="256"/> <!-- Left -->
<Value value="100.0"/>
</AxisValue>
<AxisValue index="2" Format="1">
<AxisIndex value="1"/>
<Flags value="2"/>
<ValueNameID value="262"/> <!-- Neutral -->
<Value value="0.0"/>
</AxisValue>
<AxisValue index="3" Format="1">
<AxisIndex value="1"/>
<Flags value="0"/>
<ValueNameID value="257"/> <!-- Right -->
<Value value="100.0"/>
</AxisValue>
<AxisValue index="4" Format="1">
<AxisIndex value="2"/>
<Flags value="2"/>
<ValueNameID value="262"/> <!-- Neutral -->
<Value value="0.0"/>
</AxisValue>
<AxisValue index="5" Format="1">
<AxisIndex value="2"/>
<Flags value="0"/>
<ValueNameID value="258"/> <!-- Up -->
<Value value="100.0"/>
</AxisValue>
<AxisValue index="6" Format="1">
<AxisIndex value="3"/>
<Flags value="2"/>
<ValueNameID value="262"/> <!-- Neutral -->
<Value value="0.0"/>
</AxisValue>
<AxisValue index="7" Format="1">
<AxisIndex value="3"/>
<Flags value="0"/>
<ValueNameID value="259"/> <!-- Down -->
<Value value="100.0"/>
</AxisValue>
</AxisValueArray>
<ElidedFallbackNameID value="2"/> <!-- TotallyNormal -->
</STAT>
<fvar> <fvar>
<!-- Left --> <!-- Left -->

View File

@ -226,6 +226,13 @@ def test_build_var(tmpdir):
featureTag="rclt", featureTag="rclt",
) )
statAxes = []
for tag, minVal, defaultVal, maxVal, name in axes:
values = [dict(name="Neutral", value=defaultVal, flags=0x2),
dict(name=name, value=maxVal)]
statAxes.append(dict(tag=tag, name=name, values=values))
fb.setupStat(statAxes)
fb.setupOS2() fb.setupOS2()
fb.setupPost() fb.setupPost()
fb.setupDummyDSIG() fb.setupDummyDSIG()

View File

@ -1,5 +1,9 @@
import io
import struct
from fontTools.misc.fixedTools import floatToFixed
from fontTools.misc.testTools import getXML from fontTools.misc.testTools import getXML
from fontTools.otlLib import builder from fontTools.otlLib import builder
from fontTools import ttLib
from fontTools.ttLib.tables import otTables from fontTools.ttLib.tables import otTables
import pytest import pytest
@ -1106,6 +1110,291 @@ class ClassDefBuilderTest(object):
assert not b.canAdd({"f"}) assert not b.canAdd({"f"})
buildStatTable_test_data = [
([
dict(
tag="wght",
name="Weight",
values=[
dict(value=100, name='Thin'),
dict(value=400, name='Regular', flags=0x2),
dict(value=900, name='Black')])], None, "Regular", [
' <STAT>',
' <Version value="0x00010001"/>',
' <DesignAxisRecordSize value="8"/>',
' <!-- DesignAxisCount=1 -->',
' <DesignAxisRecord>',
' <Axis index="0">',
' <AxisTag value="wght"/>',
' <AxisNameID value="257"/> <!-- Weight -->',
' <AxisOrdering value="0"/>',
' </Axis>',
' </DesignAxisRecord>',
' <!-- AxisValueCount=3 -->',
' <AxisValueArray>',
' <AxisValue index="0" Format="1">',
' <AxisIndex value="0"/>',
' <Flags value="0"/>',
' <ValueNameID value="258"/> <!-- Thin -->',
' <Value value="100.0"/>',
' </AxisValue>',
' <AxisValue index="1" Format="1">',
' <AxisIndex value="0"/>',
' <Flags value="2"/>',
' <ValueNameID value="256"/> <!-- Regular -->',
' <Value value="400.0"/>',
' </AxisValue>',
' <AxisValue index="2" Format="1">',
' <AxisIndex value="0"/>',
' <Flags value="0"/>',
' <ValueNameID value="259"/> <!-- Black -->',
' <Value value="900.0"/>',
' </AxisValue>',
' </AxisValueArray>',
' <ElidedFallbackNameID value="256"/> <!-- Regular -->',
' </STAT>']),
([
dict(
tag="wght",
name=dict(en="Weight", nl="Gewicht"),
values=[
dict(value=100, name=dict(en='Thin', nl='Dun')),
dict(value=400, name='Regular', flags=0x2),
dict(value=900, name='Black'),
]),
dict(
tag="wdth",
name="Width",
values=[
dict(value=50, name='Condensed'),
dict(value=100, name='Regular', flags=0x2),
dict(value=200, name='Extended')])], None, 2, [
' <STAT>',
' <Version value="0x00010001"/>',
' <DesignAxisRecordSize value="8"/>',
' <!-- DesignAxisCount=2 -->',
' <DesignAxisRecord>',
' <Axis index="0">',
' <AxisTag value="wght"/>',
' <AxisNameID value="256"/> <!-- Weight -->',
' <AxisOrdering value="0"/>',
' </Axis>',
' <Axis index="1">',
' <AxisTag value="wdth"/>',
' <AxisNameID value="260"/> <!-- Width -->',
' <AxisOrdering value="1"/>',
' </Axis>',
' </DesignAxisRecord>',
' <!-- AxisValueCount=6 -->',
' <AxisValueArray>',
' <AxisValue index="0" Format="1">',
' <AxisIndex value="0"/>',
' <Flags value="0"/>',
' <ValueNameID value="257"/> <!-- Thin -->',
' <Value value="100.0"/>',
' </AxisValue>',
' <AxisValue index="1" Format="1">',
' <AxisIndex value="0"/>',
' <Flags value="2"/>',
' <ValueNameID value="258"/> <!-- Regular -->',
' <Value value="400.0"/>',
' </AxisValue>',
' <AxisValue index="2" Format="1">',
' <AxisIndex value="0"/>',
' <Flags value="0"/>',
' <ValueNameID value="259"/> <!-- Black -->',
' <Value value="900.0"/>',
' </AxisValue>',
' <AxisValue index="3" Format="1">',
' <AxisIndex value="1"/>',
' <Flags value="0"/>',
' <ValueNameID value="261"/> <!-- Condensed -->',
' <Value value="50.0"/>',
' </AxisValue>',
' <AxisValue index="4" Format="1">',
' <AxisIndex value="1"/>',
' <Flags value="2"/>',
' <ValueNameID value="258"/> <!-- Regular -->',
' <Value value="100.0"/>',
' </AxisValue>',
' <AxisValue index="5" Format="1">',
' <AxisIndex value="1"/>',
' <Flags value="0"/>',
' <ValueNameID value="262"/> <!-- Extended -->',
' <Value value="200.0"/>',
' </AxisValue>',
' </AxisValueArray>',
' <ElidedFallbackNameID value="2"/> <!-- missing from name table -->',
' </STAT>']),
([
dict(
tag="wght",
name="Weight",
values=[
dict(value=400, name='Regular', flags=0x2),
dict(value=600, linkedValue=650, name='Bold')])], None, 18, [
' <STAT>',
' <Version value="0x00010001"/>',
' <DesignAxisRecordSize value="8"/>',
' <!-- DesignAxisCount=1 -->',
' <DesignAxisRecord>',
' <Axis index="0">',
' <AxisTag value="wght"/>',
' <AxisNameID value="256"/> <!-- Weight -->',
' <AxisOrdering value="0"/>',
' </Axis>',
' </DesignAxisRecord>',
' <!-- AxisValueCount=2 -->',
' <AxisValueArray>',
' <AxisValue index="0" Format="1">',
' <AxisIndex value="0"/>',
' <Flags value="2"/>',
' <ValueNameID value="257"/> <!-- Regular -->',
' <Value value="400.0"/>',
' </AxisValue>',
' <AxisValue index="1" Format="3">',
' <AxisIndex value="0"/>',
' <Flags value="0"/>',
' <ValueNameID value="258"/> <!-- Bold -->',
' <Value value="600.0"/>',
' <LinkedValue value="650.0"/>',
' </AxisValue>',
' </AxisValueArray>',
' <ElidedFallbackNameID value="18"/> <!-- missing from name table -->',
' </STAT>']),
([
dict(
tag="opsz",
name="Optical Size",
values=[
dict(nominalValue=6, rangeMaxValue=10, name='Small'),
dict(rangeMinValue=10, nominalValue=14, rangeMaxValue=24, name='Text', flags=0x2),
dict(rangeMinValue=24, nominalValue=600, name='Display')])], None, 2, [
' <STAT>',
' <Version value="0x00010001"/>',
' <DesignAxisRecordSize value="8"/>',
' <!-- DesignAxisCount=1 -->',
' <DesignAxisRecord>',
' <Axis index="0">',
' <AxisTag value="opsz"/>',
' <AxisNameID value="256"/> <!-- Optical Size -->',
' <AxisOrdering value="0"/>',
' </Axis>',
' </DesignAxisRecord>',
' <!-- AxisValueCount=3 -->',
' <AxisValueArray>',
' <AxisValue index="0" Format="2">',
' <AxisIndex value="0"/>',
' <Flags value="0"/>',
' <ValueNameID value="257"/> <!-- Small -->',
' <NominalValue value="6.0"/>',
' <RangeMinValue value="-32768.0"/>',
' <RangeMaxValue value="10.0"/>',
' </AxisValue>',
' <AxisValue index="1" Format="2">',
' <AxisIndex value="0"/>',
' <Flags value="2"/>',
' <ValueNameID value="258"/> <!-- Text -->',
' <NominalValue value="14.0"/>',
' <RangeMinValue value="10.0"/>',
' <RangeMaxValue value="24.0"/>',
' </AxisValue>',
' <AxisValue index="2" Format="2">',
' <AxisIndex value="0"/>',
' <Flags value="0"/>',
' <ValueNameID value="259"/> <!-- Display -->',
' <NominalValue value="600.0"/>',
' <RangeMinValue value="24.0"/>',
' <RangeMaxValue value="32767.99998"/>',
' </AxisValue>',
' </AxisValueArray>',
' <ElidedFallbackNameID value="2"/> <!-- missing from name table -->',
' </STAT>']),
([
dict(
tag="wght",
name="Weight",
ordering=1,
values=[]),
dict(
tag="ABCD",
name="ABCDTest",
ordering=0,
values=[
dict(value=100, name="Regular", flags=0x2)])],
[dict(location=dict(wght=300, ABCD=100), name='Regular ABCD')], 18, [
' <STAT>',
' <Version value="0x00010002"/>',
' <DesignAxisRecordSize value="8"/>',
' <!-- DesignAxisCount=2 -->',
' <DesignAxisRecord>',
' <Axis index="0">',
' <AxisTag value="wght"/>',
' <AxisNameID value="256"/> <!-- Weight -->',
' <AxisOrdering value="1"/>',
' </Axis>',
' <Axis index="1">',
' <AxisTag value="ABCD"/>',
' <AxisNameID value="257"/> <!-- ABCDTest -->',
' <AxisOrdering value="0"/>',
' </Axis>',
' </DesignAxisRecord>',
' <!-- AxisValueCount=2 -->',
' <AxisValueArray>',
' <AxisValue index="0" Format="4">',
' <!-- AxisCount=2 -->',
' <Flags value="0"/>',
' <ValueNameID value="259"/> <!-- Regular ABCD -->',
' <AxisValueRecord index="0">',
' <AxisIndex value="0"/>',
' <Value value="300.0"/>',
' </AxisValueRecord>',
' <AxisValueRecord index="1">',
' <AxisIndex value="1"/>',
' <Value value="100.0"/>',
' </AxisValueRecord>',
' </AxisValue>',
' <AxisValue index="1" Format="1">',
' <AxisIndex value="1"/>',
' <Flags value="2"/>',
' <ValueNameID value="258"/> <!-- Regular -->',
' <Value value="100.0"/>',
' </AxisValue>',
' </AxisValueArray>',
' <ElidedFallbackNameID value="18"/> <!-- missing from name table -->',
' </STAT>']),
]
@pytest.mark.parametrize("axes, axisValues, elidedFallbackName, expected_ttx", buildStatTable_test_data)
def test_buildStatTable(axes, axisValues, elidedFallbackName, expected_ttx):
font = ttLib.TTFont()
font["name"] = ttLib.newTable("name")
font["name"].names = []
builder.buildStatTable(font, axes, axisValues, elidedFallbackName)
f = io.StringIO()
font.saveXML(f, tables=["STAT"])
ttx = f.getvalue().splitlines()
ttx = ttx[3:-2] # strip XML header and <ttFont> element
assert expected_ttx == ttx
# Compile and round-trip
f = io.BytesIO()
font.save(f)
font = ttLib.TTFont(f)
f = io.StringIO()
font.saveXML(f, tables=["STAT"])
ttx = f.getvalue().splitlines()
ttx = ttx[3:-2] # strip XML header and <ttFont> element
assert expected_ttx == ttx
def test_stat_infinities():
negInf = floatToFixed(builder.AXIS_VALUE_NEGATIVE_INFINITY, 16)
assert struct.pack(">l", negInf) == b"\x80\x00\x00\x00"
posInf = floatToFixed(builder.AXIS_VALUE_POSITIVE_INFINITY, 16)
assert struct.pack(">l", posInf) == b"\x7f\xff\xff\xff"
if __name__ == "__main__": if __name__ == "__main__":
import sys import sys