Coverage for /usr/lib/python3/dist-packages/fontTools/varLib/stat.py: 23%

48 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-14 15:55 +0200

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")