1"""Extra methods for DesignSpaceDocument to generate its STAT table data.""" 2 3from __future__ import annotations 4 5from typing import Dict, List, Union 6 7import fontTools.otlLib.builder 8from fontTools.designspaceLib import ( 9 AxisLabelDescriptor, 10 DesignSpaceDocument, 11 DesignSpaceDocumentError, 12 LocationLabelDescriptor, 13) 14from fontTools.designspaceLib.types import Region, getVFUserRegion, locationInRegion 15from fontTools.ttLib import TTFont 16 17 18def buildVFStatTable(ttFont: TTFont, doc: DesignSpaceDocument, vfName: str) -> None: 19 """Build the STAT table for the variable font identified by its name in 20 the given document. 21 22 Knowing which variable we're building STAT data for is needed to subset 23 the STAT locations to only include what the variable font actually ships. 24 25 .. versionadded:: 5.0 26 27 .. seealso:: 28 - :func:`getStatAxes()` 29 - :func:`getStatLocations()` 30 - :func:`fontTools.otlLib.builder.buildStatTable()` 31 """ 32 for vf in doc.getVariableFonts(): 33 if vf.name == vfName: 34 break 35 else: 36 raise DesignSpaceDocumentError( 37 f"Cannot find the variable font by name {vfName}" 38 ) 39 40 region = getVFUserRegion(doc, vf) 41 42 return fontTools.otlLib.builder.buildStatTable( 43 ttFont, 44 getStatAxes(doc, region), 45 getStatLocations(doc, region), 46 doc.elidedFallbackName if doc.elidedFallbackName is not None else 2, 47 ) 48 49 50def getStatAxes(doc: DesignSpaceDocument, userRegion: Region) -> List[Dict]: 51 """Return a list of axis dicts suitable for use as the ``axes`` 52 argument to :func:`fontTools.otlLib.builder.buildStatTable()`. 53 54 .. versionadded:: 5.0 55 """ 56 # First, get the axis labels with explicit ordering 57 # then append the others in the order they appear. 58 maxOrdering = max( 59 (axis.axisOrdering for axis in doc.axes if axis.axisOrdering is not None), 60 default=-1, 61 ) 62 axisOrderings = [] 63 for axis in doc.axes: 64 if axis.axisOrdering is not None: 65 axisOrderings.append(axis.axisOrdering) 66 else: 67 maxOrdering += 1 68 axisOrderings.append(maxOrdering) 69 return [ 70 dict( 71 tag=axis.tag, 72 name={"en": axis.name, **axis.labelNames}, 73 ordering=ordering, 74 values=[ 75 _axisLabelToStatLocation(label) 76 for label in axis.axisLabels 77 if locationInRegion({axis.name: label.userValue}, userRegion) 78 ], 79 ) 80 for axis, ordering in zip(doc.axes, axisOrderings) 81 ] 82 83 84def getStatLocations(doc: DesignSpaceDocument, userRegion: Region) -> List[Dict]: 85 """Return a list of location dicts suitable for use as the ``locations`` 86 argument to :func:`fontTools.otlLib.builder.buildStatTable()`. 87 88 .. versionadded:: 5.0 89 """ 90 axesByName = {axis.name: axis for axis in doc.axes} 91 return [ 92 dict( 93 name={"en": label.name, **label.labelNames}, 94 # Location in the designspace is keyed by axis name 95 # Location in buildStatTable by axis tag 96 location={ 97 axesByName[name].tag: value 98 for name, value in label.getFullUserLocation(doc).items() 99 }, 100 flags=_labelToFlags(label), 101 ) 102 for label in doc.locationLabels 103 if locationInRegion(label.getFullUserLocation(doc), userRegion) 104 ] 105 106 107def _labelToFlags(label: Union[AxisLabelDescriptor, LocationLabelDescriptor]) -> int: 108 flags = 0 109 if label.olderSibling: 110 flags |= 1 111 if label.elidable: 112 flags |= 2 113 return flags 114 115 116def _axisLabelToStatLocation( 117 label: AxisLabelDescriptor, 118) -> Dict: 119 label_format = label.getFormat() 120 name = {"en": label.name, **label.labelNames} 121 flags = _labelToFlags(label) 122 if label_format == 1: 123 return dict(name=name, value=label.userValue, flags=flags) 124 if label_format == 3: 125 return dict( 126 name=name, 127 value=label.userValue, 128 linkedValue=label.linkedUserValue, 129 flags=flags, 130 ) 131 if label_format == 2: 132 res = dict( 133 name=name, 134 nominalValue=label.userValue, 135 flags=flags, 136 ) 137 if label.userMinimum is not None: 138 res["rangeMinValue"] = label.userMinimum 139 if label.userMaximum is not None: 140 res["rangeMaxValue"] = label.userMaximum 141 return res 142 raise NotImplementedError("Unknown STAT label format") 143