stat.py 4.4 KB

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