143 lines
4.4 KiB
Python
143 lines
4.4 KiB
Python
"""Extra methods for DesignSpaceDocument to generate its STAT table data."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Dict, List, Union
|
|
|
|
import fontTools.otlLib.builder
|
|
from fontTools.designspaceLib import (
|
|
AxisLabelDescriptor,
|
|
DesignSpaceDocument,
|
|
DesignSpaceDocumentError,
|
|
LocationLabelDescriptor,
|
|
)
|
|
from fontTools.designspaceLib.types import Region, getVFUserRegion, locationInRegion
|
|
from fontTools.ttLib import TTFont
|
|
|
|
|
|
def buildVFStatTable(ttFont: TTFont, doc: DesignSpaceDocument, vfName: str) -> None:
|
|
"""Build the STAT table for the variable font identified by its name in
|
|
the given document.
|
|
|
|
Knowing which variable we're building STAT data for is needed to subset
|
|
the STAT locations to only include what the variable font actually ships.
|
|
|
|
.. versionadded:: 5.0
|
|
|
|
.. seealso::
|
|
- :func:`getStatAxes()`
|
|
- :func:`getStatLocations()`
|
|
- :func:`fontTools.otlLib.builder.buildStatTable()`
|
|
"""
|
|
for vf in doc.getVariableFonts():
|
|
if vf.name == vfName:
|
|
break
|
|
else:
|
|
raise DesignSpaceDocumentError(
|
|
f"Cannot find the variable font by name {vfName}"
|
|
)
|
|
|
|
region = getVFUserRegion(doc, vf)
|
|
|
|
return fontTools.otlLib.builder.buildStatTable(
|
|
ttFont,
|
|
getStatAxes(doc, region),
|
|
getStatLocations(doc, region),
|
|
doc.elidedFallbackName if doc.elidedFallbackName is not None else 2,
|
|
)
|
|
|
|
|
|
def getStatAxes(doc: DesignSpaceDocument, userRegion: Region) -> List[Dict]:
|
|
"""Return a list of axis dicts suitable for use as the ``axes``
|
|
argument to :func:`fontTools.otlLib.builder.buildStatTable()`.
|
|
|
|
.. versionadded:: 5.0
|
|
"""
|
|
# First, get the axis labels with explicit ordering
|
|
# then append the others in the order they appear.
|
|
maxOrdering = max(
|
|
(axis.axisOrdering for axis in doc.axes if axis.axisOrdering is not None),
|
|
default=-1,
|
|
)
|
|
axisOrderings = []
|
|
for axis in doc.axes:
|
|
if axis.axisOrdering is not None:
|
|
axisOrderings.append(axis.axisOrdering)
|
|
else:
|
|
maxOrdering += 1
|
|
axisOrderings.append(maxOrdering)
|
|
return [
|
|
dict(
|
|
tag=axis.tag,
|
|
name={"en": axis.name, **axis.labelNames},
|
|
ordering=ordering,
|
|
values=[
|
|
_axisLabelToStatLocation(label)
|
|
for label in axis.axisLabels
|
|
if locationInRegion({axis.name: label.userValue}, userRegion)
|
|
],
|
|
)
|
|
for axis, ordering in zip(doc.axes, axisOrderings)
|
|
]
|
|
|
|
|
|
def getStatLocations(doc: DesignSpaceDocument, userRegion: Region) -> List[Dict]:
|
|
"""Return a list of location dicts suitable for use as the ``locations``
|
|
argument to :func:`fontTools.otlLib.builder.buildStatTable()`.
|
|
|
|
.. versionadded:: 5.0
|
|
"""
|
|
axesByName = {axis.name: axis for axis in doc.axes}
|
|
return [
|
|
dict(
|
|
name={"en": label.name, **label.labelNames},
|
|
# Location in the designspace is keyed by axis name
|
|
# Location in buildStatTable by axis tag
|
|
location={
|
|
axesByName[name].tag: value
|
|
for name, value in label.getFullUserLocation(doc).items()
|
|
},
|
|
flags=_labelToFlags(label),
|
|
)
|
|
for label in doc.locationLabels
|
|
if locationInRegion(label.getFullUserLocation(doc), userRegion)
|
|
]
|
|
|
|
|
|
def _labelToFlags(label: Union[AxisLabelDescriptor, LocationLabelDescriptor]) -> int:
|
|
flags = 0
|
|
if label.olderSibling:
|
|
flags |= 1
|
|
if label.elidable:
|
|
flags |= 2
|
|
return flags
|
|
|
|
|
|
def _axisLabelToStatLocation(
|
|
label: AxisLabelDescriptor,
|
|
) -> Dict:
|
|
label_format = label.getFormat()
|
|
name = {"en": label.name, **label.labelNames}
|
|
flags = _labelToFlags(label)
|
|
if label_format == 1:
|
|
return dict(name=name, value=label.userValue, flags=flags)
|
|
if label_format == 3:
|
|
return dict(
|
|
name=name,
|
|
value=label.userValue,
|
|
linkedValue=label.linkedUserValue,
|
|
flags=flags,
|
|
)
|
|
if label_format == 2:
|
|
res = dict(
|
|
name=name,
|
|
nominalValue=label.userValue,
|
|
flags=flags,
|
|
)
|
|
if label.userMinimum is not None:
|
|
res["rangeMinValue"] = label.userMinimum
|
|
if label.userMaximum is not None:
|
|
res["rangeMaxValue"] = label.userMaximum
|
|
return res
|
|
raise NotImplementedError("Unknown STAT label format")
|