|
"""Compute name information for a given location in user-space coordinates |
|
using STAT data. This can be used to fill-in automatically the names of an |
|
instance: |
|
|
|
.. code:: python |
|
|
|
instance = doc.instances[0] |
|
names = getStatNames(doc, instance.getFullUserLocation(doc)) |
|
print(names.styleNames) |
|
""" |
|
|
|
from __future__ import annotations |
|
|
|
from dataclasses import dataclass |
|
from typing import Dict, Optional, Tuple, Union |
|
import logging |
|
|
|
from fontTools.designspaceLib import ( |
|
AxisDescriptor, |
|
AxisLabelDescriptor, |
|
DesignSpaceDocument, |
|
DesignSpaceDocumentError, |
|
DiscreteAxisDescriptor, |
|
SimpleLocationDict, |
|
SourceDescriptor, |
|
) |
|
|
|
LOGGER = logging.getLogger(__name__) |
|
|
|
|
|
|
|
RibbiStyle = str |
|
BOLD_ITALIC_TO_RIBBI_STYLE = { |
|
(False, False): "regular", |
|
(False, True): "italic", |
|
(True, False): "bold", |
|
(True, True): "bold italic", |
|
} |
|
|
|
|
|
@dataclass |
|
class StatNames: |
|
"""Name data generated from the STAT table information.""" |
|
|
|
familyNames: Dict[str, str] |
|
styleNames: Dict[str, str] |
|
postScriptFontName: Optional[str] |
|
styleMapFamilyNames: Dict[str, str] |
|
styleMapStyleName: Optional[RibbiStyle] |
|
|
|
|
|
def getStatNames( |
|
doc: DesignSpaceDocument, userLocation: SimpleLocationDict |
|
) -> StatNames: |
|
"""Compute the family, style, PostScript names of the given ``userLocation`` |
|
using the document's STAT information. |
|
|
|
Also computes localizations. |
|
|
|
If not enough STAT data is available for a given name, either its dict of |
|
localized names will be empty (family and style names), or the name will be |
|
None (PostScript name). |
|
|
|
.. versionadded:: 5.0 |
|
""" |
|
familyNames: Dict[str, str] = {} |
|
defaultSource: Optional[SourceDescriptor] = doc.findDefault() |
|
if defaultSource is None: |
|
LOGGER.warning("Cannot determine default source to look up family name.") |
|
elif defaultSource.familyName is None: |
|
LOGGER.warning( |
|
"Cannot look up family name, assign the 'familyname' attribute to the default source." |
|
) |
|
else: |
|
familyNames = { |
|
"en": defaultSource.familyName, |
|
**defaultSource.localisedFamilyName, |
|
} |
|
|
|
styleNames: Dict[str, str] = {} |
|
|
|
label = doc.labelForUserLocation(userLocation) |
|
if label is not None: |
|
styleNames = {"en": label.name, **label.labelNames} |
|
|
|
else: |
|
|
|
|
|
|
|
labels = _getAxisLabelsForUserLocation(doc.axes, userLocation) |
|
if labels: |
|
languages = set( |
|
language for label in labels for language in label.labelNames |
|
) |
|
languages.add("en") |
|
for language in languages: |
|
styleName = " ".join( |
|
label.labelNames.get(language, label.defaultName) |
|
for label in labels |
|
if not label.elidable |
|
) |
|
if not styleName and doc.elidedFallbackName is not None: |
|
styleName = doc.elidedFallbackName |
|
styleNames[language] = styleName |
|
|
|
if "en" not in familyNames or "en" not in styleNames: |
|
|
|
return StatNames( |
|
familyNames=familyNames, |
|
styleNames=styleNames, |
|
postScriptFontName=None, |
|
styleMapFamilyNames={}, |
|
styleMapStyleName=None, |
|
) |
|
|
|
postScriptFontName = f"{familyNames['en']}-{styleNames['en']}".replace(" ", "") |
|
|
|
styleMapStyleName, regularUserLocation = _getRibbiStyle(doc, userLocation) |
|
|
|
styleNamesForStyleMap = styleNames |
|
if regularUserLocation != userLocation: |
|
regularStatNames = getStatNames(doc, regularUserLocation) |
|
styleNamesForStyleMap = regularStatNames.styleNames |
|
|
|
styleMapFamilyNames = {} |
|
for language in set(familyNames).union(styleNames.keys()): |
|
familyName = familyNames.get(language, familyNames["en"]) |
|
styleName = styleNamesForStyleMap.get(language, styleNamesForStyleMap["en"]) |
|
styleMapFamilyNames[language] = (familyName + " " + styleName).strip() |
|
|
|
return StatNames( |
|
familyNames=familyNames, |
|
styleNames=styleNames, |
|
postScriptFontName=postScriptFontName, |
|
styleMapFamilyNames=styleMapFamilyNames, |
|
styleMapStyleName=styleMapStyleName, |
|
) |
|
|
|
|
|
def _getSortedAxisLabels( |
|
axes: list[Union[AxisDescriptor, DiscreteAxisDescriptor]], |
|
) -> Dict[str, list[AxisLabelDescriptor]]: |
|
"""Returns axis labels sorted by their ordering, with unordered ones appended as |
|
they are listed.""" |
|
|
|
|
|
sortedAxes = sorted( |
|
(axis for axis in axes if axis.axisOrdering is not None), |
|
key=lambda a: a.axisOrdering, |
|
) |
|
sortedLabels: Dict[str, list[AxisLabelDescriptor]] = { |
|
axis.name: axis.axisLabels for axis in sortedAxes |
|
} |
|
|
|
|
|
|
|
for axis in axes: |
|
if axis.axisOrdering is None: |
|
sortedLabels[axis.name] = axis.axisLabels |
|
|
|
return sortedLabels |
|
|
|
|
|
def _getAxisLabelsForUserLocation( |
|
axes: list[Union[AxisDescriptor, DiscreteAxisDescriptor]], |
|
userLocation: SimpleLocationDict, |
|
) -> list[AxisLabelDescriptor]: |
|
labels: list[AxisLabelDescriptor] = [] |
|
|
|
allAxisLabels = _getSortedAxisLabels(axes) |
|
if allAxisLabels.keys() != userLocation.keys(): |
|
LOGGER.warning( |
|
f"Mismatch between user location '{userLocation.keys()}' and available " |
|
f"labels for '{allAxisLabels.keys()}'." |
|
) |
|
|
|
for axisName, axisLabels in allAxisLabels.items(): |
|
userValue = userLocation[axisName] |
|
label: Optional[AxisLabelDescriptor] = next( |
|
( |
|
l |
|
for l in axisLabels |
|
if l.userValue == userValue |
|
or ( |
|
l.userMinimum is not None |
|
and l.userMaximum is not None |
|
and l.userMinimum <= userValue <= l.userMaximum |
|
) |
|
), |
|
None, |
|
) |
|
if label is None: |
|
LOGGER.debug( |
|
f"Document needs a label for axis '{axisName}', user value '{userValue}'." |
|
) |
|
else: |
|
labels.append(label) |
|
|
|
return labels |
|
|
|
|
|
def _getRibbiStyle( |
|
self: DesignSpaceDocument, userLocation: SimpleLocationDict |
|
) -> Tuple[RibbiStyle, SimpleLocationDict]: |
|
"""Compute the RIBBI style name of the given user location, |
|
return the location of the matching Regular in the RIBBI group. |
|
|
|
.. versionadded:: 5.0 |
|
""" |
|
regularUserLocation = {} |
|
axes_by_tag = {axis.tag: axis for axis in self.axes} |
|
|
|
bold: bool = False |
|
italic: bool = False |
|
|
|
axis = axes_by_tag.get("wght") |
|
if axis is not None: |
|
for regular_label in axis.axisLabels: |
|
if ( |
|
regular_label.linkedUserValue == userLocation[axis.name] |
|
|
|
|
|
|
|
|
|
|
|
and regular_label.userValue < regular_label.linkedUserValue |
|
): |
|
regularUserLocation[axis.name] = regular_label.userValue |
|
bold = True |
|
break |
|
|
|
axis = axes_by_tag.get("ital") or axes_by_tag.get("slnt") |
|
if axis is not None: |
|
for upright_label in axis.axisLabels: |
|
if ( |
|
upright_label.linkedUserValue == userLocation[axis.name] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
and abs(upright_label.userValue) < abs(upright_label.linkedUserValue) |
|
): |
|
regularUserLocation[axis.name] = upright_label.userValue |
|
italic = True |
|
break |
|
|
|
return BOLD_ITALIC_TO_RIBBI_STYLE[bold, italic], { |
|
**userLocation, |
|
**regularUserLocation, |
|
} |
|
|