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
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
1from __future__ import annotations
3from dataclasses import dataclass
4from typing import Dict, List, Optional, Union, cast
6from fontTools.designspaceLib import (
7 AxisDescriptor,
8 DesignSpaceDocument,
9 DesignSpaceDocumentError,
10 RangeAxisSubsetDescriptor,
11 SimpleLocationDict,
12 ValueAxisSubsetDescriptor,
13 VariableFontDescriptor,
14)
17def clamp(value, minimum, maximum):
18 return min(max(value, minimum), maximum)
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"""
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)
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
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 )
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]]
55# A conditionset is a set of named ranges.
56ConditionSet = Dict[str, Range]
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]
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
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
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
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