xref: /aosp_15_r20/external/fonttools/Lib/fontTools/varLib/stat.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1"""Extra methods for DesignSpaceDocument to generate its STAT table data."""
2
3from __future__ import annotations
4
5from typing import Dict, List, Union
6
7import fontTools.otlLib.builder
8from fontTools.designspaceLib import (
9    AxisLabelDescriptor,
10    DesignSpaceDocument,
11    DesignSpaceDocumentError,
12    LocationLabelDescriptor,
13)
14from fontTools.designspaceLib.types import Region, getVFUserRegion, locationInRegion
15from fontTools.ttLib import TTFont
16
17
18def buildVFStatTable(ttFont: TTFont, doc: DesignSpaceDocument, vfName: str) -> None:
19    """Build the STAT table for the variable font identified by its name in
20    the given document.
21
22    Knowing which variable we're building STAT data for is needed to subset
23    the STAT locations to only include what the variable font actually ships.
24
25    .. versionadded:: 5.0
26
27    .. seealso::
28        - :func:`getStatAxes()`
29        - :func:`getStatLocations()`
30        - :func:`fontTools.otlLib.builder.buildStatTable()`
31    """
32    for vf in doc.getVariableFonts():
33        if vf.name == vfName:
34            break
35    else:
36        raise DesignSpaceDocumentError(
37            f"Cannot find the variable font by name {vfName}"
38        )
39
40    region = getVFUserRegion(doc, vf)
41
42    return fontTools.otlLib.builder.buildStatTable(
43        ttFont,
44        getStatAxes(doc, region),
45        getStatLocations(doc, region),
46        doc.elidedFallbackName if doc.elidedFallbackName is not None else 2,
47    )
48
49
50def getStatAxes(doc: DesignSpaceDocument, userRegion: Region) -> List[Dict]:
51    """Return a list of axis dicts suitable for use as the ``axes``
52    argument to :func:`fontTools.otlLib.builder.buildStatTable()`.
53
54    .. versionadded:: 5.0
55    """
56    # First, get the axis labels with explicit ordering
57    # then append the others in the order they appear.
58    maxOrdering = max(
59        (axis.axisOrdering for axis in doc.axes if axis.axisOrdering is not None),
60        default=-1,
61    )
62    axisOrderings = []
63    for axis in doc.axes:
64        if axis.axisOrdering is not None:
65            axisOrderings.append(axis.axisOrdering)
66        else:
67            maxOrdering += 1
68            axisOrderings.append(maxOrdering)
69    return [
70        dict(
71            tag=axis.tag,
72            name={"en": axis.name, **axis.labelNames},
73            ordering=ordering,
74            values=[
75                _axisLabelToStatLocation(label)
76                for label in axis.axisLabels
77                if locationInRegion({axis.name: label.userValue}, userRegion)
78            ],
79        )
80        for axis, ordering in zip(doc.axes, axisOrderings)
81    ]
82
83
84def getStatLocations(doc: DesignSpaceDocument, userRegion: Region) -> List[Dict]:
85    """Return a list of location dicts suitable for use as the ``locations``
86    argument to :func:`fontTools.otlLib.builder.buildStatTable()`.
87
88    .. versionadded:: 5.0
89    """
90    axesByName = {axis.name: axis for axis in doc.axes}
91    return [
92        dict(
93            name={"en": label.name, **label.labelNames},
94            # Location in the designspace is keyed by axis name
95            # Location in buildStatTable by axis tag
96            location={
97                axesByName[name].tag: value
98                for name, value in label.getFullUserLocation(doc).items()
99            },
100            flags=_labelToFlags(label),
101        )
102        for label in doc.locationLabels
103        if locationInRegion(label.getFullUserLocation(doc), userRegion)
104    ]
105
106
107def _labelToFlags(label: Union[AxisLabelDescriptor, LocationLabelDescriptor]) -> int:
108    flags = 0
109    if label.olderSibling:
110        flags |= 1
111    if label.elidable:
112        flags |= 2
113    return flags
114
115
116def _axisLabelToStatLocation(
117    label: AxisLabelDescriptor,
118) -> Dict:
119    label_format = label.getFormat()
120    name = {"en": label.name, **label.labelNames}
121    flags = _labelToFlags(label)
122    if label_format == 1:
123        return dict(name=name, value=label.userValue, flags=flags)
124    if label_format == 3:
125        return dict(
126            name=name,
127            value=label.userValue,
128            linkedValue=label.linkedUserValue,
129            flags=flags,
130        )
131    if label_format == 2:
132        res = dict(
133            name=name,
134            nominalValue=label.userValue,
135            flags=flags,
136        )
137        if label.userMinimum is not None:
138            res["rangeMinValue"] = label.userMinimum
139        if label.userMaximum is not None:
140            res["rangeMaxValue"] = label.userMaximum
141        return res
142    raise NotImplementedError("Unknown STAT label format")
143