Coverage for /usr/lib/python3/dist-packages/fontTools/designspaceLib/types.py: 30%

80 statements  

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

1from __future__ import annotations 

2 

3from dataclasses import dataclass 

4from typing import Dict, List, Optional, Union, cast 

5 

6from fontTools.designspaceLib import ( 

7 AxisDescriptor, 

8 DesignSpaceDocument, 

9 DesignSpaceDocumentError, 

10 RangeAxisSubsetDescriptor, 

11 SimpleLocationDict, 

12 ValueAxisSubsetDescriptor, 

13 VariableFontDescriptor, 

14) 

15 

16 

17def clamp(value, minimum, maximum): 

18 return min(max(value, minimum), maximum) 

19 

20 

21@dataclass 

22class Range: 

23 minimum: float 

24 """Inclusive minimum of the range.""" 

25 maximum: float 

26 """Inclusive maximum of the range.""" 

27 default: float = 0 

28 """Default value""" 

29 

30 def __post_init__(self): 

31 self.minimum, self.maximum = sorted((self.minimum, self.maximum)) 

32 self.default = clamp(self.default, self.minimum, self.maximum) 

33 

34 def __contains__(self, value: Union[float, Range]) -> bool: 

35 if isinstance(value, Range): 

36 return self.minimum <= value.minimum and value.maximum <= self.maximum 

37 return self.minimum <= value <= self.maximum 

38 

39 def intersection(self, other: Range) -> Optional[Range]: 

40 if self.maximum < other.minimum or self.minimum > other.maximum: 

41 return None 

42 else: 

43 return Range( 

44 max(self.minimum, other.minimum), 

45 min(self.maximum, other.maximum), 

46 self.default, # We don't care about the default in this use-case 

47 ) 

48 

49 

50# A region selection is either a range or a single value, as a Designspace v5 

51# axis-subset element only allows a single discrete value or a range for a 

52# variable-font element. 

53Region = Dict[str, Union[Range, float]] 

54 

55# A conditionset is a set of named ranges. 

56ConditionSet = Dict[str, Range] 

57 

58# A rule is a list of conditionsets where any has to be relevant for the whole rule to be relevant. 

59Rule = List[ConditionSet] 

60Rules = Dict[str, Rule] 

61 

62 

63def locationInRegion(location: SimpleLocationDict, region: Region) -> bool: 

64 for name, value in location.items(): 

65 if name not in region: 

66 return False 

67 regionValue = region[name] 

68 if isinstance(regionValue, (float, int)): 

69 if value != regionValue: 

70 return False 

71 else: 

72 if value not in regionValue: 

73 return False 

74 return True 

75 

76 

77def regionInRegion(region: Region, superRegion: Region) -> bool: 

78 for name, value in region.items(): 

79 if not name in superRegion: 

80 return False 

81 superValue = superRegion[name] 

82 if isinstance(superValue, (float, int)): 

83 if value != superValue: 

84 return False 

85 else: 

86 if value not in superValue: 

87 return False 

88 return True 

89 

90 

91def userRegionToDesignRegion(doc: DesignSpaceDocument, userRegion: Region) -> Region: 

92 designRegion = {} 

93 for name, value in userRegion.items(): 

94 axis = doc.getAxis(name) 

95 if axis is None: 

96 raise DesignSpaceDocumentError( 

97 f"Cannot find axis named '{name}' for region." 

98 ) 

99 if isinstance(value, (float, int)): 

100 designRegion[name] = axis.map_forward(value) 

101 else: 

102 designRegion[name] = Range( 

103 axis.map_forward(value.minimum), 

104 axis.map_forward(value.maximum), 

105 axis.map_forward(value.default), 

106 ) 

107 return designRegion 

108 

109 

110def getVFUserRegion(doc: DesignSpaceDocument, vf: VariableFontDescriptor) -> Region: 

111 vfUserRegion: Region = {} 

112 # For each axis, 2 cases: 

113 # - it has a range = it's an axis in the VF DS 

114 # - it's a single location = use it to know which rules should apply in the VF 

115 for axisSubset in vf.axisSubsets: 

116 axis = doc.getAxis(axisSubset.name) 

117 if axis is None: 

118 raise DesignSpaceDocumentError( 

119 f"Cannot find axis named '{axisSubset.name}' for variable font '{vf.name}'." 

120 ) 

121 if hasattr(axisSubset, "userMinimum"): 

122 # Mypy doesn't support narrowing union types via hasattr() 

123 # TODO(Python 3.10): use TypeGuard 

124 # https://mypy.readthedocs.io/en/stable/type_narrowing.html 

125 axisSubset = cast(RangeAxisSubsetDescriptor, axisSubset) 

126 if not hasattr(axis, "minimum"): 

127 raise DesignSpaceDocumentError( 

128 f"Cannot select a range over '{axis.name}' for variable font '{vf.name}' " 

129 "because it's a discrete axis, use only 'userValue' instead." 

130 ) 

131 axis = cast(AxisDescriptor, axis) 

132 vfUserRegion[axis.name] = Range( 

133 max(axisSubset.userMinimum, axis.minimum), 

134 min(axisSubset.userMaximum, axis.maximum), 

135 axisSubset.userDefault or axis.default, 

136 ) 

137 else: 

138 axisSubset = cast(ValueAxisSubsetDescriptor, axisSubset) 

139 vfUserRegion[axis.name] = axisSubset.userValue 

140 # Any axis not mentioned explicitly has a single location = default value 

141 for axis in doc.axes: 

142 if axis.name not in vfUserRegion: 

143 assert isinstance( 

144 axis.default, (int, float) 

145 ), f"Axis '{axis.name}' has no valid default value." 

146 vfUserRegion[axis.name] = axis.default 

147 return vfUserRegion