Skip to content

Commit

Permalink
[otlLib.builder] Add function to build STAT table from high-level des…
Browse files Browse the repository at this point in the history
…cription (#1926)

* added a function to build a STAT table: `fontTools.otlLib.builder.buildStatTable()`
* make `varLib._add_stat()` a client of `buildStatTable()`
  • Loading branch information
justvanrossum committed May 9, 2020
1 parent 50c77c1 commit d6bb38c
Show file tree
Hide file tree
Showing 6 changed files with 585 additions and 23 deletions.
9 changes: 9 additions & 0 deletions Lib/fontTools/fontBuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,15 @@ def setupCPAL(
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):
subTable = cmap_classes[format](format)
Expand Down
191 changes: 191 additions & 0 deletions Lib/fontTools/otlLib/builder.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from collections import namedtuple
from fontTools.misc.fixedTools import fixedToFloat
from fontTools import ttLib
from fontTools.ttLib.tables import otTables as ot
from fontTools.ttLib.tables.otBase import ValueRecord, valueRecordFormatDict
Expand Down Expand Up @@ -657,3 +658,193 @@ def build(self):
classDef = ot.ClassDef()
classDef.classDefs = glyphClasses
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)
26 changes: 3 additions & 23 deletions Lib/fontTools/varLib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,30 +202,10 @@ def _add_stat(font, axes):
if "STAT" in font:
return

from ..otlLib.builder import buildStatTable
fvarTable = font['fvar']

STAT = font["STAT"] = newTable('STAT')
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
axes = [dict(tag=a.axisTag, name=a.axisNameID) for a in fvarTable.axes]
buildStatTable(font, axes)


def _add_gvar(font, masterModel, master_ttfs, tolerance=0.5, optimize=True):
Expand Down
86 changes: 86 additions & 0 deletions Tests/fontBuilder/data/test_var.ttf.ttx
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,9 @@
<namerecord nameID="261" platformID="1" platEncID="0" langID="0x0" unicode="True">
Right Up
</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">
HalloTestFont
</namerecord>
Expand Down Expand Up @@ -237,6 +240,9 @@
<namerecord nameID="261" platformID="3" platEncID="1" langID="0x409">
Right Up
</namerecord>
<namerecord nameID="262" platformID="3" platEncID="1" langID="0x409">
Neutral
</namerecord>
<namerecord nameID="1" platformID="3" platEncID="1" langID="0x413">
HalloTestFont
</namerecord>
Expand Down Expand Up @@ -363,6 +369,86 @@
</FeatureVariations>
</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>

<!-- Left -->
Expand Down
7 changes: 7 additions & 0 deletions Tests/fontBuilder/fontBuilder_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,13 @@ def test_build_var(tmpdir):
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.setupPost()
fb.setupDummyDSIG()
Expand Down

0 comments on commit d6bb38c

Please sign in to comment.