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