xref: /aosp_15_r20/external/fonttools/Lib/fontTools/designspaceLib/statNames.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1*e1fe3e4aSElliott Hughes"""Compute name information for a given location in user-space coordinates
2*e1fe3e4aSElliott Hughesusing STAT data. This can be used to fill-in automatically the names of an
3*e1fe3e4aSElliott Hughesinstance:
4*e1fe3e4aSElliott Hughes
5*e1fe3e4aSElliott Hughes.. code:: python
6*e1fe3e4aSElliott Hughes
7*e1fe3e4aSElliott Hughes    instance = doc.instances[0]
8*e1fe3e4aSElliott Hughes    names = getStatNames(doc, instance.getFullUserLocation(doc))
9*e1fe3e4aSElliott Hughes    print(names.styleNames)
10*e1fe3e4aSElliott Hughes"""
11*e1fe3e4aSElliott Hughes
12*e1fe3e4aSElliott Hughesfrom __future__ import annotations
13*e1fe3e4aSElliott Hughes
14*e1fe3e4aSElliott Hughesfrom dataclasses import dataclass
15*e1fe3e4aSElliott Hughesfrom typing import Dict, Optional, Tuple, Union
16*e1fe3e4aSElliott Hughesimport logging
17*e1fe3e4aSElliott Hughes
18*e1fe3e4aSElliott Hughesfrom fontTools.designspaceLib import (
19*e1fe3e4aSElliott Hughes    AxisDescriptor,
20*e1fe3e4aSElliott Hughes    AxisLabelDescriptor,
21*e1fe3e4aSElliott Hughes    DesignSpaceDocument,
22*e1fe3e4aSElliott Hughes    DesignSpaceDocumentError,
23*e1fe3e4aSElliott Hughes    DiscreteAxisDescriptor,
24*e1fe3e4aSElliott Hughes    SimpleLocationDict,
25*e1fe3e4aSElliott Hughes    SourceDescriptor,
26*e1fe3e4aSElliott Hughes)
27*e1fe3e4aSElliott Hughes
28*e1fe3e4aSElliott HughesLOGGER = logging.getLogger(__name__)
29*e1fe3e4aSElliott Hughes
30*e1fe3e4aSElliott Hughes# TODO(Python 3.8): use Literal
31*e1fe3e4aSElliott Hughes# RibbiStyleName = Union[Literal["regular"], Literal["bold"], Literal["italic"], Literal["bold italic"]]
32*e1fe3e4aSElliott HughesRibbiStyle = str
33*e1fe3e4aSElliott HughesBOLD_ITALIC_TO_RIBBI_STYLE = {
34*e1fe3e4aSElliott Hughes    (False, False): "regular",
35*e1fe3e4aSElliott Hughes    (False, True): "italic",
36*e1fe3e4aSElliott Hughes    (True, False): "bold",
37*e1fe3e4aSElliott Hughes    (True, True): "bold italic",
38*e1fe3e4aSElliott Hughes}
39*e1fe3e4aSElliott Hughes
40*e1fe3e4aSElliott Hughes
41*e1fe3e4aSElliott Hughes@dataclass
42*e1fe3e4aSElliott Hughesclass StatNames:
43*e1fe3e4aSElliott Hughes    """Name data generated from the STAT table information."""
44*e1fe3e4aSElliott Hughes
45*e1fe3e4aSElliott Hughes    familyNames: Dict[str, str]
46*e1fe3e4aSElliott Hughes    styleNames: Dict[str, str]
47*e1fe3e4aSElliott Hughes    postScriptFontName: Optional[str]
48*e1fe3e4aSElliott Hughes    styleMapFamilyNames: Dict[str, str]
49*e1fe3e4aSElliott Hughes    styleMapStyleName: Optional[RibbiStyle]
50*e1fe3e4aSElliott Hughes
51*e1fe3e4aSElliott Hughes
52*e1fe3e4aSElliott Hughesdef getStatNames(
53*e1fe3e4aSElliott Hughes    doc: DesignSpaceDocument, userLocation: SimpleLocationDict
54*e1fe3e4aSElliott Hughes) -> StatNames:
55*e1fe3e4aSElliott Hughes    """Compute the family, style, PostScript names of the given ``userLocation``
56*e1fe3e4aSElliott Hughes    using the document's STAT information.
57*e1fe3e4aSElliott Hughes
58*e1fe3e4aSElliott Hughes    Also computes localizations.
59*e1fe3e4aSElliott Hughes
60*e1fe3e4aSElliott Hughes    If not enough STAT data is available for a given name, either its dict of
61*e1fe3e4aSElliott Hughes    localized names will be empty (family and style names), or the name will be
62*e1fe3e4aSElliott Hughes    None (PostScript name).
63*e1fe3e4aSElliott Hughes
64*e1fe3e4aSElliott Hughes    .. versionadded:: 5.0
65*e1fe3e4aSElliott Hughes    """
66*e1fe3e4aSElliott Hughes    familyNames: Dict[str, str] = {}
67*e1fe3e4aSElliott Hughes    defaultSource: Optional[SourceDescriptor] = doc.findDefault()
68*e1fe3e4aSElliott Hughes    if defaultSource is None:
69*e1fe3e4aSElliott Hughes        LOGGER.warning("Cannot determine default source to look up family name.")
70*e1fe3e4aSElliott Hughes    elif defaultSource.familyName is None:
71*e1fe3e4aSElliott Hughes        LOGGER.warning(
72*e1fe3e4aSElliott Hughes            "Cannot look up family name, assign the 'familyname' attribute to the default source."
73*e1fe3e4aSElliott Hughes        )
74*e1fe3e4aSElliott Hughes    else:
75*e1fe3e4aSElliott Hughes        familyNames = {
76*e1fe3e4aSElliott Hughes            "en": defaultSource.familyName,
77*e1fe3e4aSElliott Hughes            **defaultSource.localisedFamilyName,
78*e1fe3e4aSElliott Hughes        }
79*e1fe3e4aSElliott Hughes
80*e1fe3e4aSElliott Hughes    styleNames: Dict[str, str] = {}
81*e1fe3e4aSElliott Hughes    # If a free-standing label matches the location, use it for name generation.
82*e1fe3e4aSElliott Hughes    label = doc.labelForUserLocation(userLocation)
83*e1fe3e4aSElliott Hughes    if label is not None:
84*e1fe3e4aSElliott Hughes        styleNames = {"en": label.name, **label.labelNames}
85*e1fe3e4aSElliott Hughes    # Otherwise, scour the axis labels for matches.
86*e1fe3e4aSElliott Hughes    else:
87*e1fe3e4aSElliott Hughes        # Gather all languages in which at least one translation is provided
88*e1fe3e4aSElliott Hughes        # Then build names for all these languages, but fallback to English
89*e1fe3e4aSElliott Hughes        # whenever a translation is missing.
90*e1fe3e4aSElliott Hughes        labels = _getAxisLabelsForUserLocation(doc.axes, userLocation)
91*e1fe3e4aSElliott Hughes        if labels:
92*e1fe3e4aSElliott Hughes            languages = set(
93*e1fe3e4aSElliott Hughes                language for label in labels for language in label.labelNames
94*e1fe3e4aSElliott Hughes            )
95*e1fe3e4aSElliott Hughes            languages.add("en")
96*e1fe3e4aSElliott Hughes            for language in languages:
97*e1fe3e4aSElliott Hughes                styleName = " ".join(
98*e1fe3e4aSElliott Hughes                    label.labelNames.get(language, label.defaultName)
99*e1fe3e4aSElliott Hughes                    for label in labels
100*e1fe3e4aSElliott Hughes                    if not label.elidable
101*e1fe3e4aSElliott Hughes                )
102*e1fe3e4aSElliott Hughes                if not styleName and doc.elidedFallbackName is not None:
103*e1fe3e4aSElliott Hughes                    styleName = doc.elidedFallbackName
104*e1fe3e4aSElliott Hughes                styleNames[language] = styleName
105*e1fe3e4aSElliott Hughes
106*e1fe3e4aSElliott Hughes    if "en" not in familyNames or "en" not in styleNames:
107*e1fe3e4aSElliott Hughes        # Not enough information to compute PS names of styleMap names
108*e1fe3e4aSElliott Hughes        return StatNames(
109*e1fe3e4aSElliott Hughes            familyNames=familyNames,
110*e1fe3e4aSElliott Hughes            styleNames=styleNames,
111*e1fe3e4aSElliott Hughes            postScriptFontName=None,
112*e1fe3e4aSElliott Hughes            styleMapFamilyNames={},
113*e1fe3e4aSElliott Hughes            styleMapStyleName=None,
114*e1fe3e4aSElliott Hughes        )
115*e1fe3e4aSElliott Hughes
116*e1fe3e4aSElliott Hughes    postScriptFontName = f"{familyNames['en']}-{styleNames['en']}".replace(" ", "")
117*e1fe3e4aSElliott Hughes
118*e1fe3e4aSElliott Hughes    styleMapStyleName, regularUserLocation = _getRibbiStyle(doc, userLocation)
119*e1fe3e4aSElliott Hughes
120*e1fe3e4aSElliott Hughes    styleNamesForStyleMap = styleNames
121*e1fe3e4aSElliott Hughes    if regularUserLocation != userLocation:
122*e1fe3e4aSElliott Hughes        regularStatNames = getStatNames(doc, regularUserLocation)
123*e1fe3e4aSElliott Hughes        styleNamesForStyleMap = regularStatNames.styleNames
124*e1fe3e4aSElliott Hughes
125*e1fe3e4aSElliott Hughes    styleMapFamilyNames = {}
126*e1fe3e4aSElliott Hughes    for language in set(familyNames).union(styleNames.keys()):
127*e1fe3e4aSElliott Hughes        familyName = familyNames.get(language, familyNames["en"])
128*e1fe3e4aSElliott Hughes        styleName = styleNamesForStyleMap.get(language, styleNamesForStyleMap["en"])
129*e1fe3e4aSElliott Hughes        styleMapFamilyNames[language] = (familyName + " " + styleName).strip()
130*e1fe3e4aSElliott Hughes
131*e1fe3e4aSElliott Hughes    return StatNames(
132*e1fe3e4aSElliott Hughes        familyNames=familyNames,
133*e1fe3e4aSElliott Hughes        styleNames=styleNames,
134*e1fe3e4aSElliott Hughes        postScriptFontName=postScriptFontName,
135*e1fe3e4aSElliott Hughes        styleMapFamilyNames=styleMapFamilyNames,
136*e1fe3e4aSElliott Hughes        styleMapStyleName=styleMapStyleName,
137*e1fe3e4aSElliott Hughes    )
138*e1fe3e4aSElliott Hughes
139*e1fe3e4aSElliott Hughes
140*e1fe3e4aSElliott Hughesdef _getSortedAxisLabels(
141*e1fe3e4aSElliott Hughes    axes: list[Union[AxisDescriptor, DiscreteAxisDescriptor]],
142*e1fe3e4aSElliott Hughes) -> Dict[str, list[AxisLabelDescriptor]]:
143*e1fe3e4aSElliott Hughes    """Returns axis labels sorted by their ordering, with unordered ones appended as
144*e1fe3e4aSElliott Hughes    they are listed."""
145*e1fe3e4aSElliott Hughes
146*e1fe3e4aSElliott Hughes    # First, get the axis labels with explicit ordering...
147*e1fe3e4aSElliott Hughes    sortedAxes = sorted(
148*e1fe3e4aSElliott Hughes        (axis for axis in axes if axis.axisOrdering is not None),
149*e1fe3e4aSElliott Hughes        key=lambda a: a.axisOrdering,
150*e1fe3e4aSElliott Hughes    )
151*e1fe3e4aSElliott Hughes    sortedLabels: Dict[str, list[AxisLabelDescriptor]] = {
152*e1fe3e4aSElliott Hughes        axis.name: axis.axisLabels for axis in sortedAxes
153*e1fe3e4aSElliott Hughes    }
154*e1fe3e4aSElliott Hughes
155*e1fe3e4aSElliott Hughes    # ... then append the others in the order they appear.
156*e1fe3e4aSElliott Hughes    # NOTE: This relies on Python 3.7+ dict's preserved insertion order.
157*e1fe3e4aSElliott Hughes    for axis in axes:
158*e1fe3e4aSElliott Hughes        if axis.axisOrdering is None:
159*e1fe3e4aSElliott Hughes            sortedLabels[axis.name] = axis.axisLabels
160*e1fe3e4aSElliott Hughes
161*e1fe3e4aSElliott Hughes    return sortedLabels
162*e1fe3e4aSElliott Hughes
163*e1fe3e4aSElliott Hughes
164*e1fe3e4aSElliott Hughesdef _getAxisLabelsForUserLocation(
165*e1fe3e4aSElliott Hughes    axes: list[Union[AxisDescriptor, DiscreteAxisDescriptor]],
166*e1fe3e4aSElliott Hughes    userLocation: SimpleLocationDict,
167*e1fe3e4aSElliott Hughes) -> list[AxisLabelDescriptor]:
168*e1fe3e4aSElliott Hughes    labels: list[AxisLabelDescriptor] = []
169*e1fe3e4aSElliott Hughes
170*e1fe3e4aSElliott Hughes    allAxisLabels = _getSortedAxisLabels(axes)
171*e1fe3e4aSElliott Hughes    if allAxisLabels.keys() != userLocation.keys():
172*e1fe3e4aSElliott Hughes        LOGGER.warning(
173*e1fe3e4aSElliott Hughes            f"Mismatch between user location '{userLocation.keys()}' and available "
174*e1fe3e4aSElliott Hughes            f"labels for '{allAxisLabels.keys()}'."
175*e1fe3e4aSElliott Hughes        )
176*e1fe3e4aSElliott Hughes
177*e1fe3e4aSElliott Hughes    for axisName, axisLabels in allAxisLabels.items():
178*e1fe3e4aSElliott Hughes        userValue = userLocation[axisName]
179*e1fe3e4aSElliott Hughes        label: Optional[AxisLabelDescriptor] = next(
180*e1fe3e4aSElliott Hughes            (
181*e1fe3e4aSElliott Hughes                l
182*e1fe3e4aSElliott Hughes                for l in axisLabels
183*e1fe3e4aSElliott Hughes                if l.userValue == userValue
184*e1fe3e4aSElliott Hughes                or (
185*e1fe3e4aSElliott Hughes                    l.userMinimum is not None
186*e1fe3e4aSElliott Hughes                    and l.userMaximum is not None
187*e1fe3e4aSElliott Hughes                    and l.userMinimum <= userValue <= l.userMaximum
188*e1fe3e4aSElliott Hughes                )
189*e1fe3e4aSElliott Hughes            ),
190*e1fe3e4aSElliott Hughes            None,
191*e1fe3e4aSElliott Hughes        )
192*e1fe3e4aSElliott Hughes        if label is None:
193*e1fe3e4aSElliott Hughes            LOGGER.debug(
194*e1fe3e4aSElliott Hughes                f"Document needs a label for axis '{axisName}', user value '{userValue}'."
195*e1fe3e4aSElliott Hughes            )
196*e1fe3e4aSElliott Hughes        else:
197*e1fe3e4aSElliott Hughes            labels.append(label)
198*e1fe3e4aSElliott Hughes
199*e1fe3e4aSElliott Hughes    return labels
200*e1fe3e4aSElliott Hughes
201*e1fe3e4aSElliott Hughes
202*e1fe3e4aSElliott Hughesdef _getRibbiStyle(
203*e1fe3e4aSElliott Hughes    self: DesignSpaceDocument, userLocation: SimpleLocationDict
204*e1fe3e4aSElliott Hughes) -> Tuple[RibbiStyle, SimpleLocationDict]:
205*e1fe3e4aSElliott Hughes    """Compute the RIBBI style name of the given user location,
206*e1fe3e4aSElliott Hughes    return the location of the matching Regular in the RIBBI group.
207*e1fe3e4aSElliott Hughes
208*e1fe3e4aSElliott Hughes    .. versionadded:: 5.0
209*e1fe3e4aSElliott Hughes    """
210*e1fe3e4aSElliott Hughes    regularUserLocation = {}
211*e1fe3e4aSElliott Hughes    axes_by_tag = {axis.tag: axis for axis in self.axes}
212*e1fe3e4aSElliott Hughes
213*e1fe3e4aSElliott Hughes    bold: bool = False
214*e1fe3e4aSElliott Hughes    italic: bool = False
215*e1fe3e4aSElliott Hughes
216*e1fe3e4aSElliott Hughes    axis = axes_by_tag.get("wght")
217*e1fe3e4aSElliott Hughes    if axis is not None:
218*e1fe3e4aSElliott Hughes        for regular_label in axis.axisLabels:
219*e1fe3e4aSElliott Hughes            if (
220*e1fe3e4aSElliott Hughes                regular_label.linkedUserValue == userLocation[axis.name]
221*e1fe3e4aSElliott Hughes                # In the "recursive" case where both the Regular has
222*e1fe3e4aSElliott Hughes                # linkedUserValue pointing the Bold, and the Bold has
223*e1fe3e4aSElliott Hughes                # linkedUserValue pointing to the Regular, only consider the
224*e1fe3e4aSElliott Hughes                # first case: Regular (e.g. 400) has linkedUserValue pointing to
225*e1fe3e4aSElliott Hughes                # Bold (e.g. 700, higher than Regular)
226*e1fe3e4aSElliott Hughes                and regular_label.userValue < regular_label.linkedUserValue
227*e1fe3e4aSElliott Hughes            ):
228*e1fe3e4aSElliott Hughes                regularUserLocation[axis.name] = regular_label.userValue
229*e1fe3e4aSElliott Hughes                bold = True
230*e1fe3e4aSElliott Hughes                break
231*e1fe3e4aSElliott Hughes
232*e1fe3e4aSElliott Hughes    axis = axes_by_tag.get("ital") or axes_by_tag.get("slnt")
233*e1fe3e4aSElliott Hughes    if axis is not None:
234*e1fe3e4aSElliott Hughes        for upright_label in axis.axisLabels:
235*e1fe3e4aSElliott Hughes            if (
236*e1fe3e4aSElliott Hughes                upright_label.linkedUserValue == userLocation[axis.name]
237*e1fe3e4aSElliott Hughes                # In the "recursive" case where both the Upright has
238*e1fe3e4aSElliott Hughes                # linkedUserValue pointing the Italic, and the Italic has
239*e1fe3e4aSElliott Hughes                # linkedUserValue pointing to the Upright, only consider the
240*e1fe3e4aSElliott Hughes                # first case: Upright (e.g. ital=0, slant=0) has
241*e1fe3e4aSElliott Hughes                # linkedUserValue pointing to Italic (e.g ital=1, slant=-12 or
242*e1fe3e4aSElliott Hughes                # slant=12 for backwards italics, in any case higher than
243*e1fe3e4aSElliott Hughes                # Upright in absolute value, hence the abs() below.
244*e1fe3e4aSElliott Hughes                and abs(upright_label.userValue) < abs(upright_label.linkedUserValue)
245*e1fe3e4aSElliott Hughes            ):
246*e1fe3e4aSElliott Hughes                regularUserLocation[axis.name] = upright_label.userValue
247*e1fe3e4aSElliott Hughes                italic = True
248*e1fe3e4aSElliott Hughes                break
249*e1fe3e4aSElliott Hughes
250*e1fe3e4aSElliott Hughes    return BOLD_ITALIC_TO_RIBBI_STYLE[bold, italic], {
251*e1fe3e4aSElliott Hughes        **userLocation,
252*e1fe3e4aSElliott Hughes        **regularUserLocation,
253*e1fe3e4aSElliott Hughes    }
254