Coverage for /usr/lib/python3/dist-packages/fontTools/varLib/stat.py: 23%
48 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
1"""Extra methods for DesignSpaceDocument to generate its STAT table data."""
3from __future__ import annotations
5from typing import Dict, List, Union
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
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.
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.
25 .. versionadded:: 5.0
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 )
40 region = getVFUserRegion(doc, vf)
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 )
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()`.
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 ]
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()`.
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 ]
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
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")