| """Extra methods for DesignSpaceDocument to generate its STAT table data.""" |
| |
| from __future__ import annotations |
| |
| from typing import Dict, List, Union |
| |
| import fontTools.otlLib.builder |
| from fontTools.designspaceLib import ( |
| AxisLabelDescriptor, |
| DesignSpaceDocument, |
| DesignSpaceDocumentError, |
| LocationLabelDescriptor, |
| ) |
| from fontTools.designspaceLib.types import Region, getVFUserRegion, locationInRegion |
| from fontTools.ttLib import TTFont |
| |
| |
| def buildVFStatTable(ttFont: TTFont, doc: DesignSpaceDocument, vfName: str) -> None: |
| """Build the STAT table for the variable font identified by its name in |
| the given document. |
| |
| Knowing which variable we're building STAT data for is needed to subset |
| the STAT locations to only include what the variable font actually ships. |
| |
| .. versionadded:: 5.0 |
| |
| .. seealso:: |
| - :func:`getStatAxes()` |
| - :func:`getStatLocations()` |
| - :func:`fontTools.otlLib.builder.buildStatTable()` |
| """ |
| for vf in doc.getVariableFonts(): |
| if vf.name == vfName: |
| break |
| else: |
| raise DesignSpaceDocumentError( |
| f"Cannot find the variable font by name {vfName}" |
| ) |
| |
| region = getVFUserRegion(doc, vf) |
| |
| return fontTools.otlLib.builder.buildStatTable( |
| ttFont, |
| getStatAxes(doc, region), |
| getStatLocations(doc, region), |
| doc.elidedFallbackName if doc.elidedFallbackName is not None else 2, |
| ) |
| |
| |
| def getStatAxes(doc: DesignSpaceDocument, userRegion: Region) -> List[Dict]: |
| """Return a list of axis dicts suitable for use as the ``axes`` |
| argument to :func:`fontTools.otlLib.builder.buildStatTable()`. |
| |
| .. versionadded:: 5.0 |
| """ |
| # First, get the axis labels with explicit ordering |
| # then append the others in the order they appear. |
| maxOrdering = max( |
| (axis.axisOrdering for axis in doc.axes if axis.axisOrdering is not None), |
| default=-1, |
| ) |
| axisOrderings = [] |
| for axis in doc.axes: |
| if axis.axisOrdering is not None: |
| axisOrderings.append(axis.axisOrdering) |
| else: |
| maxOrdering += 1 |
| axisOrderings.append(maxOrdering) |
| return [ |
| dict( |
| tag=axis.tag, |
| name={"en": axis.name, **axis.labelNames}, |
| ordering=ordering, |
| values=[ |
| _axisLabelToStatLocation(label) |
| for label in axis.axisLabels |
| if locationInRegion({axis.name: label.userValue}, userRegion) |
| ], |
| ) |
| for axis, ordering in zip(doc.axes, axisOrderings) |
| ] |
| |
| |
| def getStatLocations(doc: DesignSpaceDocument, userRegion: Region) -> List[Dict]: |
| """Return a list of location dicts suitable for use as the ``locations`` |
| argument to :func:`fontTools.otlLib.builder.buildStatTable()`. |
| |
| .. versionadded:: 5.0 |
| """ |
| axesByName = {axis.name: axis for axis in doc.axes} |
| return [ |
| dict( |
| name={"en": label.name, **label.labelNames}, |
| # Location in the designspace is keyed by axis name |
| # Location in buildStatTable by axis tag |
| location={ |
| axesByName[name].tag: value |
| for name, value in label.getFullUserLocation(doc).items() |
| }, |
| flags=_labelToFlags(label), |
| ) |
| for label in doc.locationLabels |
| if locationInRegion(label.getFullUserLocation(doc), userRegion) |
| ] |
| |
| |
| def _labelToFlags(label: Union[AxisLabelDescriptor, LocationLabelDescriptor]) -> int: |
| flags = 0 |
| if label.olderSibling: |
| flags |= 1 |
| if label.elidable: |
| flags |= 2 |
| return flags |
| |
| |
| def _axisLabelToStatLocation( |
| label: AxisLabelDescriptor, |
| ) -> Dict: |
| label_format = label.getFormat() |
| name = {"en": label.name, **label.labelNames} |
| flags = _labelToFlags(label) |
| if label_format == 1: |
| return dict(name=name, value=label.userValue, flags=flags) |
| if label_format == 3: |
| return dict( |
| name=name, |
| value=label.userValue, |
| linkedValue=label.linkedUserValue, |
| flags=flags, |
| ) |
| if label_format == 2: |
| res = dict( |
| name=name, |
| nominalValue=label.userValue, |
| flags=flags, |
| ) |
| if label.userMinimum is not None: |
| res["rangeMinValue"] = label.userMinimum |
| if label.userMaximum is not None: |
| res["rangeMaxValue"] = label.userMaximum |
| return res |
| raise NotImplementedError("Unknown STAT label format") |