File size: 5,320 Bytes
fe41391 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
from __future__ import annotations
from dataclasses import dataclass
from typing import Dict, List, Optional, Union, cast
from fontTools.designspaceLib import (
AxisDescriptor,
DesignSpaceDocument,
DesignSpaceDocumentError,
RangeAxisSubsetDescriptor,
SimpleLocationDict,
ValueAxisSubsetDescriptor,
VariableFontDescriptor,
)
def clamp(value, minimum, maximum):
return min(max(value, minimum), maximum)
@dataclass
class Range:
minimum: float
"""Inclusive minimum of the range."""
maximum: float
"""Inclusive maximum of the range."""
default: float = 0
"""Default value"""
def __post_init__(self):
self.minimum, self.maximum = sorted((self.minimum, self.maximum))
self.default = clamp(self.default, self.minimum, self.maximum)
def __contains__(self, value: Union[float, Range]) -> bool:
if isinstance(value, Range):
return self.minimum <= value.minimum and value.maximum <= self.maximum
return self.minimum <= value <= self.maximum
def intersection(self, other: Range) -> Optional[Range]:
if self.maximum < other.minimum or self.minimum > other.maximum:
return None
else:
return Range(
max(self.minimum, other.minimum),
min(self.maximum, other.maximum),
self.default, # We don't care about the default in this use-case
)
# A region selection is either a range or a single value, as a Designspace v5
# axis-subset element only allows a single discrete value or a range for a
# variable-font element.
Region = Dict[str, Union[Range, float]]
# A conditionset is a set of named ranges.
ConditionSet = Dict[str, Range]
# A rule is a list of conditionsets where any has to be relevant for the whole rule to be relevant.
Rule = List[ConditionSet]
Rules = Dict[str, Rule]
def locationInRegion(location: SimpleLocationDict, region: Region) -> bool:
for name, value in location.items():
if name not in region:
return False
regionValue = region[name]
if isinstance(regionValue, (float, int)):
if value != regionValue:
return False
else:
if value not in regionValue:
return False
return True
def regionInRegion(region: Region, superRegion: Region) -> bool:
for name, value in region.items():
if not name in superRegion:
return False
superValue = superRegion[name]
if isinstance(superValue, (float, int)):
if value != superValue:
return False
else:
if value not in superValue:
return False
return True
def userRegionToDesignRegion(doc: DesignSpaceDocument, userRegion: Region) -> Region:
designRegion = {}
for name, value in userRegion.items():
axis = doc.getAxis(name)
if axis is None:
raise DesignSpaceDocumentError(
f"Cannot find axis named '{name}' for region."
)
if isinstance(value, (float, int)):
designRegion[name] = axis.map_forward(value)
else:
designRegion[name] = Range(
axis.map_forward(value.minimum),
axis.map_forward(value.maximum),
axis.map_forward(value.default),
)
return designRegion
def getVFUserRegion(doc: DesignSpaceDocument, vf: VariableFontDescriptor) -> Region:
vfUserRegion: Region = {}
# For each axis, 2 cases:
# - it has a range = it's an axis in the VF DS
# - it's a single location = use it to know which rules should apply in the VF
for axisSubset in vf.axisSubsets:
axis = doc.getAxis(axisSubset.name)
if axis is None:
raise DesignSpaceDocumentError(
f"Cannot find axis named '{axisSubset.name}' for variable font '{vf.name}'."
)
if hasattr(axisSubset, "userMinimum"):
# Mypy doesn't support narrowing union types via hasattr()
# TODO(Python 3.10): use TypeGuard
# https://mypy.readthedocs.io/en/stable/type_narrowing.html
axisSubset = cast(RangeAxisSubsetDescriptor, axisSubset)
if not hasattr(axis, "minimum"):
raise DesignSpaceDocumentError(
f"Cannot select a range over '{axis.name}' for variable font '{vf.name}' "
"because it's a discrete axis, use only 'userValue' instead."
)
axis = cast(AxisDescriptor, axis)
vfUserRegion[axis.name] = Range(
max(axisSubset.userMinimum, axis.minimum),
min(axisSubset.userMaximum, axis.maximum),
axisSubset.userDefault or axis.default,
)
else:
axisSubset = cast(ValueAxisSubsetDescriptor, axisSubset)
vfUserRegion[axis.name] = axisSubset.userValue
# Any axis not mentioned explicitly has a single location = default value
for axis in doc.axes:
if axis.name not in vfUserRegion:
assert isinstance(
axis.default, (int, float)
), f"Axis '{axis.name}' has no valid default value."
vfUserRegion[axis.name] = axis.default
return vfUserRegion
|