Spaces:
Running
Running
openvoice2
/
path
/to
/venv
/lib
/python3.10
/site-packages
/fontTools
/designspaceLib
/statNames.py
"""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", | |
} | |
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, | |
} | |