123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252 |
- """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__)
- # TODO(Python 3.8): use Literal
- # RibbiStyleName = Union[Literal["regular"], Literal["bold"], Literal["italic"], Literal["bold italic"]]
- 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] = {}
- # If a free-standing label matches the location, use it for name generation.
- label = doc.labelForUserLocation(userLocation)
- if label is not None:
- styleNames = {"en": label.name, **label.labelNames}
- # Otherwise, scour the axis labels for matches.
- else:
- # Gather all languages in which at least one translation is provided
- # Then build names for all these languages, but fallback to English
- # whenever a translation is missing.
- 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:
- # Not enough information to compute PS names of styleMap names
- 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."""
- # First, get the axis labels with explicit ordering...
- 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
- }
- # ... then append the others in the order they appear.
- # NOTE: This relies on Python 3.7+ dict's preserved insertion order.
- 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]
- # In the "recursive" case where both the Regular has
- # linkedUserValue pointing the Bold, and the Bold has
- # linkedUserValue pointing to the Regular, only consider the
- # first case: Regular (e.g. 400) has linkedUserValue pointing to
- # Bold (e.g. 700, higher than Regular)
- 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]
- # In the "recursive" case where both the Upright has
- # linkedUserValue pointing the Italic, and the Italic has
- # linkedUserValue pointing to the Upright, only consider the
- # first case: Upright (e.g. ital=0, slant=0) has
- # linkedUserValue pointing to Italic (e.g ital=1, slant=-12 or
- # slant=12 for backwards italics, in any case higher than
- # Upright in absolute value, hence the abs() below.
- 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,
- }
|