xref: /aosp_15_r20/external/fonttools/Lib/fontTools/designspaceLib/split.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1*e1fe3e4aSElliott Hughes"""Allows building all the variable fonts of a DesignSpace version 5 by
2*e1fe3e4aSElliott Hughessplitting the document into interpolable sub-space, then into each VF.
3*e1fe3e4aSElliott Hughes"""
4*e1fe3e4aSElliott Hughes
5*e1fe3e4aSElliott Hughesfrom __future__ import annotations
6*e1fe3e4aSElliott Hughes
7*e1fe3e4aSElliott Hughesimport itertools
8*e1fe3e4aSElliott Hughesimport logging
9*e1fe3e4aSElliott Hughesimport math
10*e1fe3e4aSElliott Hughesfrom typing import Any, Callable, Dict, Iterator, List, Tuple, cast
11*e1fe3e4aSElliott Hughes
12*e1fe3e4aSElliott Hughesfrom fontTools.designspaceLib import (
13*e1fe3e4aSElliott Hughes    AxisDescriptor,
14*e1fe3e4aSElliott Hughes    AxisMappingDescriptor,
15*e1fe3e4aSElliott Hughes    DesignSpaceDocument,
16*e1fe3e4aSElliott Hughes    DiscreteAxisDescriptor,
17*e1fe3e4aSElliott Hughes    InstanceDescriptor,
18*e1fe3e4aSElliott Hughes    RuleDescriptor,
19*e1fe3e4aSElliott Hughes    SimpleLocationDict,
20*e1fe3e4aSElliott Hughes    SourceDescriptor,
21*e1fe3e4aSElliott Hughes    VariableFontDescriptor,
22*e1fe3e4aSElliott Hughes)
23*e1fe3e4aSElliott Hughesfrom fontTools.designspaceLib.statNames import StatNames, getStatNames
24*e1fe3e4aSElliott Hughesfrom fontTools.designspaceLib.types import (
25*e1fe3e4aSElliott Hughes    ConditionSet,
26*e1fe3e4aSElliott Hughes    Range,
27*e1fe3e4aSElliott Hughes    Region,
28*e1fe3e4aSElliott Hughes    getVFUserRegion,
29*e1fe3e4aSElliott Hughes    locationInRegion,
30*e1fe3e4aSElliott Hughes    regionInRegion,
31*e1fe3e4aSElliott Hughes    userRegionToDesignRegion,
32*e1fe3e4aSElliott Hughes)
33*e1fe3e4aSElliott Hughes
34*e1fe3e4aSElliott HughesLOGGER = logging.getLogger(__name__)
35*e1fe3e4aSElliott Hughes
36*e1fe3e4aSElliott HughesMakeInstanceFilenameCallable = Callable[
37*e1fe3e4aSElliott Hughes    [DesignSpaceDocument, InstanceDescriptor, StatNames], str
38*e1fe3e4aSElliott Hughes]
39*e1fe3e4aSElliott Hughes
40*e1fe3e4aSElliott Hughes
41*e1fe3e4aSElliott Hughesdef defaultMakeInstanceFilename(
42*e1fe3e4aSElliott Hughes    doc: DesignSpaceDocument, instance: InstanceDescriptor, statNames: StatNames
43*e1fe3e4aSElliott Hughes) -> str:
44*e1fe3e4aSElliott Hughes    """Default callable to synthesize an instance filename
45*e1fe3e4aSElliott Hughes    when makeNames=True, for instances that don't specify an instance name
46*e1fe3e4aSElliott Hughes    in the designspace. This part of the name generation can be overriden
47*e1fe3e4aSElliott Hughes    because it's not specified by the STAT table.
48*e1fe3e4aSElliott Hughes    """
49*e1fe3e4aSElliott Hughes    familyName = instance.familyName or statNames.familyNames.get("en")
50*e1fe3e4aSElliott Hughes    styleName = instance.styleName or statNames.styleNames.get("en")
51*e1fe3e4aSElliott Hughes    return f"{familyName}-{styleName}.ttf"
52*e1fe3e4aSElliott Hughes
53*e1fe3e4aSElliott Hughes
54*e1fe3e4aSElliott Hughesdef splitInterpolable(
55*e1fe3e4aSElliott Hughes    doc: DesignSpaceDocument,
56*e1fe3e4aSElliott Hughes    makeNames: bool = True,
57*e1fe3e4aSElliott Hughes    expandLocations: bool = True,
58*e1fe3e4aSElliott Hughes    makeInstanceFilename: MakeInstanceFilenameCallable = defaultMakeInstanceFilename,
59*e1fe3e4aSElliott Hughes) -> Iterator[Tuple[SimpleLocationDict, DesignSpaceDocument]]:
60*e1fe3e4aSElliott Hughes    """Split the given DS5 into several interpolable sub-designspaces.
61*e1fe3e4aSElliott Hughes    There are as many interpolable sub-spaces as there are combinations of
62*e1fe3e4aSElliott Hughes    discrete axis values.
63*e1fe3e4aSElliott Hughes
64*e1fe3e4aSElliott Hughes    E.g. with axes:
65*e1fe3e4aSElliott Hughes        - italic (discrete) Upright or Italic
66*e1fe3e4aSElliott Hughes        - style (discrete) Sans or Serif
67*e1fe3e4aSElliott Hughes        - weight (continuous) 100 to 900
68*e1fe3e4aSElliott Hughes
69*e1fe3e4aSElliott Hughes    There are 4 sub-spaces in which the Weight axis should interpolate:
70*e1fe3e4aSElliott Hughes    (Upright, Sans), (Upright, Serif), (Italic, Sans) and (Italic, Serif).
71*e1fe3e4aSElliott Hughes
72*e1fe3e4aSElliott Hughes    The sub-designspaces still include the full axis definitions and STAT data,
73*e1fe3e4aSElliott Hughes    but the rules, sources, variable fonts, instances are trimmed down to only
74*e1fe3e4aSElliott Hughes    keep what falls within the interpolable sub-space.
75*e1fe3e4aSElliott Hughes
76*e1fe3e4aSElliott Hughes    Args:
77*e1fe3e4aSElliott Hughes      - ``makeNames``: Whether to compute the instance family and style
78*e1fe3e4aSElliott Hughes        names using the STAT data.
79*e1fe3e4aSElliott Hughes      - ``expandLocations``: Whether to turn all locations into "full"
80*e1fe3e4aSElliott Hughes        locations, including implicit default axis values where missing.
81*e1fe3e4aSElliott Hughes      - ``makeInstanceFilename``: Callable to synthesize an instance filename
82*e1fe3e4aSElliott Hughes        when makeNames=True, for instances that don't specify an instance name
83*e1fe3e4aSElliott Hughes        in the designspace. This part of the name generation can be overridden
84*e1fe3e4aSElliott Hughes        because it's not specified by the STAT table.
85*e1fe3e4aSElliott Hughes
86*e1fe3e4aSElliott Hughes    .. versionadded:: 5.0
87*e1fe3e4aSElliott Hughes    """
88*e1fe3e4aSElliott Hughes    discreteAxes = []
89*e1fe3e4aSElliott Hughes    interpolableUserRegion: Region = {}
90*e1fe3e4aSElliott Hughes    for axis in doc.axes:
91*e1fe3e4aSElliott Hughes        if hasattr(axis, "values"):
92*e1fe3e4aSElliott Hughes            # Mypy doesn't support narrowing union types via hasattr()
93*e1fe3e4aSElliott Hughes            # TODO(Python 3.10): use TypeGuard
94*e1fe3e4aSElliott Hughes            # https://mypy.readthedocs.io/en/stable/type_narrowing.html
95*e1fe3e4aSElliott Hughes            axis = cast(DiscreteAxisDescriptor, axis)
96*e1fe3e4aSElliott Hughes            discreteAxes.append(axis)
97*e1fe3e4aSElliott Hughes        else:
98*e1fe3e4aSElliott Hughes            axis = cast(AxisDescriptor, axis)
99*e1fe3e4aSElliott Hughes            interpolableUserRegion[axis.name] = Range(
100*e1fe3e4aSElliott Hughes                axis.minimum,
101*e1fe3e4aSElliott Hughes                axis.maximum,
102*e1fe3e4aSElliott Hughes                axis.default,
103*e1fe3e4aSElliott Hughes            )
104*e1fe3e4aSElliott Hughes    valueCombinations = itertools.product(*[axis.values for axis in discreteAxes])
105*e1fe3e4aSElliott Hughes    for values in valueCombinations:
106*e1fe3e4aSElliott Hughes        discreteUserLocation = {
107*e1fe3e4aSElliott Hughes            discreteAxis.name: value
108*e1fe3e4aSElliott Hughes            for discreteAxis, value in zip(discreteAxes, values)
109*e1fe3e4aSElliott Hughes        }
110*e1fe3e4aSElliott Hughes        subDoc = _extractSubSpace(
111*e1fe3e4aSElliott Hughes            doc,
112*e1fe3e4aSElliott Hughes            {**interpolableUserRegion, **discreteUserLocation},
113*e1fe3e4aSElliott Hughes            keepVFs=True,
114*e1fe3e4aSElliott Hughes            makeNames=makeNames,
115*e1fe3e4aSElliott Hughes            expandLocations=expandLocations,
116*e1fe3e4aSElliott Hughes            makeInstanceFilename=makeInstanceFilename,
117*e1fe3e4aSElliott Hughes        )
118*e1fe3e4aSElliott Hughes        yield discreteUserLocation, subDoc
119*e1fe3e4aSElliott Hughes
120*e1fe3e4aSElliott Hughes
121*e1fe3e4aSElliott Hughesdef splitVariableFonts(
122*e1fe3e4aSElliott Hughes    doc: DesignSpaceDocument,
123*e1fe3e4aSElliott Hughes    makeNames: bool = False,
124*e1fe3e4aSElliott Hughes    expandLocations: bool = False,
125*e1fe3e4aSElliott Hughes    makeInstanceFilename: MakeInstanceFilenameCallable = defaultMakeInstanceFilename,
126*e1fe3e4aSElliott Hughes) -> Iterator[Tuple[str, DesignSpaceDocument]]:
127*e1fe3e4aSElliott Hughes    """Convert each variable font listed in this document into a standalone
128*e1fe3e4aSElliott Hughes    designspace. This can be used to compile all the variable fonts from a
129*e1fe3e4aSElliott Hughes    format 5 designspace using tools that can only deal with 1 VF at a time.
130*e1fe3e4aSElliott Hughes
131*e1fe3e4aSElliott Hughes    Args:
132*e1fe3e4aSElliott Hughes      - ``makeNames``: Whether to compute the instance family and style
133*e1fe3e4aSElliott Hughes        names using the STAT data.
134*e1fe3e4aSElliott Hughes      - ``expandLocations``: Whether to turn all locations into "full"
135*e1fe3e4aSElliott Hughes        locations, including implicit default axis values where missing.
136*e1fe3e4aSElliott Hughes      - ``makeInstanceFilename``: Callable to synthesize an instance filename
137*e1fe3e4aSElliott Hughes        when makeNames=True, for instances that don't specify an instance name
138*e1fe3e4aSElliott Hughes        in the designspace. This part of the name generation can be overridden
139*e1fe3e4aSElliott Hughes        because it's not specified by the STAT table.
140*e1fe3e4aSElliott Hughes
141*e1fe3e4aSElliott Hughes    .. versionadded:: 5.0
142*e1fe3e4aSElliott Hughes    """
143*e1fe3e4aSElliott Hughes    # Make one DesignspaceDoc v5 for each variable font
144*e1fe3e4aSElliott Hughes    for vf in doc.getVariableFonts():
145*e1fe3e4aSElliott Hughes        vfUserRegion = getVFUserRegion(doc, vf)
146*e1fe3e4aSElliott Hughes        vfDoc = _extractSubSpace(
147*e1fe3e4aSElliott Hughes            doc,
148*e1fe3e4aSElliott Hughes            vfUserRegion,
149*e1fe3e4aSElliott Hughes            keepVFs=False,
150*e1fe3e4aSElliott Hughes            makeNames=makeNames,
151*e1fe3e4aSElliott Hughes            expandLocations=expandLocations,
152*e1fe3e4aSElliott Hughes            makeInstanceFilename=makeInstanceFilename,
153*e1fe3e4aSElliott Hughes        )
154*e1fe3e4aSElliott Hughes        vfDoc.lib = {**vfDoc.lib, **vf.lib}
155*e1fe3e4aSElliott Hughes        yield vf.name, vfDoc
156*e1fe3e4aSElliott Hughes
157*e1fe3e4aSElliott Hughes
158*e1fe3e4aSElliott Hughesdef convert5to4(
159*e1fe3e4aSElliott Hughes    doc: DesignSpaceDocument,
160*e1fe3e4aSElliott Hughes) -> Dict[str, DesignSpaceDocument]:
161*e1fe3e4aSElliott Hughes    """Convert each variable font listed in this document into a standalone
162*e1fe3e4aSElliott Hughes    format 4 designspace. This can be used to compile all the variable fonts
163*e1fe3e4aSElliott Hughes    from a format 5 designspace using tools that only know about format 4.
164*e1fe3e4aSElliott Hughes
165*e1fe3e4aSElliott Hughes    .. versionadded:: 5.0
166*e1fe3e4aSElliott Hughes    """
167*e1fe3e4aSElliott Hughes    vfs = {}
168*e1fe3e4aSElliott Hughes    for _location, subDoc in splitInterpolable(doc):
169*e1fe3e4aSElliott Hughes        for vfName, vfDoc in splitVariableFonts(subDoc):
170*e1fe3e4aSElliott Hughes            vfDoc.formatVersion = "4.1"
171*e1fe3e4aSElliott Hughes            vfs[vfName] = vfDoc
172*e1fe3e4aSElliott Hughes    return vfs
173*e1fe3e4aSElliott Hughes
174*e1fe3e4aSElliott Hughes
175*e1fe3e4aSElliott Hughesdef _extractSubSpace(
176*e1fe3e4aSElliott Hughes    doc: DesignSpaceDocument,
177*e1fe3e4aSElliott Hughes    userRegion: Region,
178*e1fe3e4aSElliott Hughes    *,
179*e1fe3e4aSElliott Hughes    keepVFs: bool,
180*e1fe3e4aSElliott Hughes    makeNames: bool,
181*e1fe3e4aSElliott Hughes    expandLocations: bool,
182*e1fe3e4aSElliott Hughes    makeInstanceFilename: MakeInstanceFilenameCallable,
183*e1fe3e4aSElliott Hughes) -> DesignSpaceDocument:
184*e1fe3e4aSElliott Hughes    subDoc = DesignSpaceDocument()
185*e1fe3e4aSElliott Hughes    # Don't include STAT info
186*e1fe3e4aSElliott Hughes    # FIXME: (Jany) let's think about it. Not include = OK because the point of
187*e1fe3e4aSElliott Hughes    # the splitting is to build VFs and we'll use the STAT data of the full
188*e1fe3e4aSElliott Hughes    # document to generate the STAT of the VFs, so "no need" to have STAT data
189*e1fe3e4aSElliott Hughes    # in sub-docs. Counterpoint: what if someone wants to split this DS for
190*e1fe3e4aSElliott Hughes    # other purposes?  Maybe for that it would be useful to also subset the STAT
191*e1fe3e4aSElliott Hughes    # data?
192*e1fe3e4aSElliott Hughes    # subDoc.elidedFallbackName = doc.elidedFallbackName
193*e1fe3e4aSElliott Hughes
194*e1fe3e4aSElliott Hughes    def maybeExpandDesignLocation(object):
195*e1fe3e4aSElliott Hughes        if expandLocations:
196*e1fe3e4aSElliott Hughes            return object.getFullDesignLocation(doc)
197*e1fe3e4aSElliott Hughes        else:
198*e1fe3e4aSElliott Hughes            return object.designLocation
199*e1fe3e4aSElliott Hughes
200*e1fe3e4aSElliott Hughes    for axis in doc.axes:
201*e1fe3e4aSElliott Hughes        range = userRegion[axis.name]
202*e1fe3e4aSElliott Hughes        if isinstance(range, Range) and hasattr(axis, "minimum"):
203*e1fe3e4aSElliott Hughes            # Mypy doesn't support narrowing union types via hasattr()
204*e1fe3e4aSElliott Hughes            # TODO(Python 3.10): use TypeGuard
205*e1fe3e4aSElliott Hughes            # https://mypy.readthedocs.io/en/stable/type_narrowing.html
206*e1fe3e4aSElliott Hughes            axis = cast(AxisDescriptor, axis)
207*e1fe3e4aSElliott Hughes            subDoc.addAxis(
208*e1fe3e4aSElliott Hughes                AxisDescriptor(
209*e1fe3e4aSElliott Hughes                    # Same info
210*e1fe3e4aSElliott Hughes                    tag=axis.tag,
211*e1fe3e4aSElliott Hughes                    name=axis.name,
212*e1fe3e4aSElliott Hughes                    labelNames=axis.labelNames,
213*e1fe3e4aSElliott Hughes                    hidden=axis.hidden,
214*e1fe3e4aSElliott Hughes                    # Subset range
215*e1fe3e4aSElliott Hughes                    minimum=max(range.minimum, axis.minimum),
216*e1fe3e4aSElliott Hughes                    default=range.default or axis.default,
217*e1fe3e4aSElliott Hughes                    maximum=min(range.maximum, axis.maximum),
218*e1fe3e4aSElliott Hughes                    map=[
219*e1fe3e4aSElliott Hughes                        (user, design)
220*e1fe3e4aSElliott Hughes                        for user, design in axis.map
221*e1fe3e4aSElliott Hughes                        if range.minimum <= user <= range.maximum
222*e1fe3e4aSElliott Hughes                    ],
223*e1fe3e4aSElliott Hughes                    # Don't include STAT info
224*e1fe3e4aSElliott Hughes                    axisOrdering=None,
225*e1fe3e4aSElliott Hughes                    axisLabels=None,
226*e1fe3e4aSElliott Hughes                )
227*e1fe3e4aSElliott Hughes            )
228*e1fe3e4aSElliott Hughes
229*e1fe3e4aSElliott Hughes    subDoc.axisMappings = mappings = []
230*e1fe3e4aSElliott Hughes    subDocAxes = {axis.name for axis in subDoc.axes}
231*e1fe3e4aSElliott Hughes    for mapping in doc.axisMappings:
232*e1fe3e4aSElliott Hughes        if not all(axis in subDocAxes for axis in mapping.inputLocation.keys()):
233*e1fe3e4aSElliott Hughes            continue
234*e1fe3e4aSElliott Hughes        if not all(axis in subDocAxes for axis in mapping.outputLocation.keys()):
235*e1fe3e4aSElliott Hughes            LOGGER.error(
236*e1fe3e4aSElliott Hughes                "In axis mapping from input %s, some output axes are not in the variable-font: %s",
237*e1fe3e4aSElliott Hughes                mapping.inputLocation,
238*e1fe3e4aSElliott Hughes                mapping.outputLocation,
239*e1fe3e4aSElliott Hughes            )
240*e1fe3e4aSElliott Hughes            continue
241*e1fe3e4aSElliott Hughes
242*e1fe3e4aSElliott Hughes        mappingAxes = set()
243*e1fe3e4aSElliott Hughes        mappingAxes.update(mapping.inputLocation.keys())
244*e1fe3e4aSElliott Hughes        mappingAxes.update(mapping.outputLocation.keys())
245*e1fe3e4aSElliott Hughes        for axis in doc.axes:
246*e1fe3e4aSElliott Hughes            if axis.name not in mappingAxes:
247*e1fe3e4aSElliott Hughes                continue
248*e1fe3e4aSElliott Hughes            range = userRegion[axis.name]
249*e1fe3e4aSElliott Hughes            if (
250*e1fe3e4aSElliott Hughes                range.minimum != axis.minimum
251*e1fe3e4aSElliott Hughes                or (range.default is not None and range.default != axis.default)
252*e1fe3e4aSElliott Hughes                or range.maximum != axis.maximum
253*e1fe3e4aSElliott Hughes            ):
254*e1fe3e4aSElliott Hughes                LOGGER.error(
255*e1fe3e4aSElliott Hughes                    "Limiting axis ranges used in <mapping> elements not supported: %s",
256*e1fe3e4aSElliott Hughes                    axis.name,
257*e1fe3e4aSElliott Hughes                )
258*e1fe3e4aSElliott Hughes                continue
259*e1fe3e4aSElliott Hughes
260*e1fe3e4aSElliott Hughes        mappings.append(
261*e1fe3e4aSElliott Hughes            AxisMappingDescriptor(
262*e1fe3e4aSElliott Hughes                inputLocation=mapping.inputLocation,
263*e1fe3e4aSElliott Hughes                outputLocation=mapping.outputLocation,
264*e1fe3e4aSElliott Hughes            )
265*e1fe3e4aSElliott Hughes        )
266*e1fe3e4aSElliott Hughes
267*e1fe3e4aSElliott Hughes    # Don't include STAT info
268*e1fe3e4aSElliott Hughes    # subDoc.locationLabels = doc.locationLabels
269*e1fe3e4aSElliott Hughes
270*e1fe3e4aSElliott Hughes    # Rules: subset them based on conditions
271*e1fe3e4aSElliott Hughes    designRegion = userRegionToDesignRegion(doc, userRegion)
272*e1fe3e4aSElliott Hughes    subDoc.rules = _subsetRulesBasedOnConditions(doc.rules, designRegion)
273*e1fe3e4aSElliott Hughes    subDoc.rulesProcessingLast = doc.rulesProcessingLast
274*e1fe3e4aSElliott Hughes
275*e1fe3e4aSElliott Hughes    # Sources: keep only the ones that fall within the kept axis ranges
276*e1fe3e4aSElliott Hughes    for source in doc.sources:
277*e1fe3e4aSElliott Hughes        if not locationInRegion(doc.map_backward(source.designLocation), userRegion):
278*e1fe3e4aSElliott Hughes            continue
279*e1fe3e4aSElliott Hughes
280*e1fe3e4aSElliott Hughes        subDoc.addSource(
281*e1fe3e4aSElliott Hughes            SourceDescriptor(
282*e1fe3e4aSElliott Hughes                filename=source.filename,
283*e1fe3e4aSElliott Hughes                path=source.path,
284*e1fe3e4aSElliott Hughes                font=source.font,
285*e1fe3e4aSElliott Hughes                name=source.name,
286*e1fe3e4aSElliott Hughes                designLocation=_filterLocation(
287*e1fe3e4aSElliott Hughes                    userRegion, maybeExpandDesignLocation(source)
288*e1fe3e4aSElliott Hughes                ),
289*e1fe3e4aSElliott Hughes                layerName=source.layerName,
290*e1fe3e4aSElliott Hughes                familyName=source.familyName,
291*e1fe3e4aSElliott Hughes                styleName=source.styleName,
292*e1fe3e4aSElliott Hughes                muteKerning=source.muteKerning,
293*e1fe3e4aSElliott Hughes                muteInfo=source.muteInfo,
294*e1fe3e4aSElliott Hughes                mutedGlyphNames=source.mutedGlyphNames,
295*e1fe3e4aSElliott Hughes            )
296*e1fe3e4aSElliott Hughes        )
297*e1fe3e4aSElliott Hughes
298*e1fe3e4aSElliott Hughes    # Copy family name translations from the old default source to the new default
299*e1fe3e4aSElliott Hughes    vfDefault = subDoc.findDefault()
300*e1fe3e4aSElliott Hughes    oldDefault = doc.findDefault()
301*e1fe3e4aSElliott Hughes    if vfDefault is not None and oldDefault is not None:
302*e1fe3e4aSElliott Hughes        vfDefault.localisedFamilyName = oldDefault.localisedFamilyName
303*e1fe3e4aSElliott Hughes
304*e1fe3e4aSElliott Hughes    # Variable fonts: keep only the ones that fall within the kept axis ranges
305*e1fe3e4aSElliott Hughes    if keepVFs:
306*e1fe3e4aSElliott Hughes        # Note: call getVariableFont() to make the implicit VFs explicit
307*e1fe3e4aSElliott Hughes        for vf in doc.getVariableFonts():
308*e1fe3e4aSElliott Hughes            vfUserRegion = getVFUserRegion(doc, vf)
309*e1fe3e4aSElliott Hughes            if regionInRegion(vfUserRegion, userRegion):
310*e1fe3e4aSElliott Hughes                subDoc.addVariableFont(
311*e1fe3e4aSElliott Hughes                    VariableFontDescriptor(
312*e1fe3e4aSElliott Hughes                        name=vf.name,
313*e1fe3e4aSElliott Hughes                        filename=vf.filename,
314*e1fe3e4aSElliott Hughes                        axisSubsets=[
315*e1fe3e4aSElliott Hughes                            axisSubset
316*e1fe3e4aSElliott Hughes                            for axisSubset in vf.axisSubsets
317*e1fe3e4aSElliott Hughes                            if isinstance(userRegion[axisSubset.name], Range)
318*e1fe3e4aSElliott Hughes                        ],
319*e1fe3e4aSElliott Hughes                        lib=vf.lib,
320*e1fe3e4aSElliott Hughes                    )
321*e1fe3e4aSElliott Hughes                )
322*e1fe3e4aSElliott Hughes
323*e1fe3e4aSElliott Hughes    # Instances: same as Sources + compute missing names
324*e1fe3e4aSElliott Hughes    for instance in doc.instances:
325*e1fe3e4aSElliott Hughes        if not locationInRegion(instance.getFullUserLocation(doc), userRegion):
326*e1fe3e4aSElliott Hughes            continue
327*e1fe3e4aSElliott Hughes
328*e1fe3e4aSElliott Hughes        if makeNames:
329*e1fe3e4aSElliott Hughes            statNames = getStatNames(doc, instance.getFullUserLocation(doc))
330*e1fe3e4aSElliott Hughes            familyName = instance.familyName or statNames.familyNames.get("en")
331*e1fe3e4aSElliott Hughes            styleName = instance.styleName or statNames.styleNames.get("en")
332*e1fe3e4aSElliott Hughes            subDoc.addInstance(
333*e1fe3e4aSElliott Hughes                InstanceDescriptor(
334*e1fe3e4aSElliott Hughes                    filename=instance.filename
335*e1fe3e4aSElliott Hughes                    or makeInstanceFilename(doc, instance, statNames),
336*e1fe3e4aSElliott Hughes                    path=instance.path,
337*e1fe3e4aSElliott Hughes                    font=instance.font,
338*e1fe3e4aSElliott Hughes                    name=instance.name or f"{familyName} {styleName}",
339*e1fe3e4aSElliott Hughes                    userLocation={} if expandLocations else instance.userLocation,
340*e1fe3e4aSElliott Hughes                    designLocation=_filterLocation(
341*e1fe3e4aSElliott Hughes                        userRegion, maybeExpandDesignLocation(instance)
342*e1fe3e4aSElliott Hughes                    ),
343*e1fe3e4aSElliott Hughes                    familyName=familyName,
344*e1fe3e4aSElliott Hughes                    styleName=styleName,
345*e1fe3e4aSElliott Hughes                    postScriptFontName=instance.postScriptFontName
346*e1fe3e4aSElliott Hughes                    or statNames.postScriptFontName,
347*e1fe3e4aSElliott Hughes                    styleMapFamilyName=instance.styleMapFamilyName
348*e1fe3e4aSElliott Hughes                    or statNames.styleMapFamilyNames.get("en"),
349*e1fe3e4aSElliott Hughes                    styleMapStyleName=instance.styleMapStyleName
350*e1fe3e4aSElliott Hughes                    or statNames.styleMapStyleName,
351*e1fe3e4aSElliott Hughes                    localisedFamilyName=instance.localisedFamilyName
352*e1fe3e4aSElliott Hughes                    or statNames.familyNames,
353*e1fe3e4aSElliott Hughes                    localisedStyleName=instance.localisedStyleName
354*e1fe3e4aSElliott Hughes                    or statNames.styleNames,
355*e1fe3e4aSElliott Hughes                    localisedStyleMapFamilyName=instance.localisedStyleMapFamilyName
356*e1fe3e4aSElliott Hughes                    or statNames.styleMapFamilyNames,
357*e1fe3e4aSElliott Hughes                    localisedStyleMapStyleName=instance.localisedStyleMapStyleName
358*e1fe3e4aSElliott Hughes                    or {},
359*e1fe3e4aSElliott Hughes                    lib=instance.lib,
360*e1fe3e4aSElliott Hughes                )
361*e1fe3e4aSElliott Hughes            )
362*e1fe3e4aSElliott Hughes        else:
363*e1fe3e4aSElliott Hughes            subDoc.addInstance(
364*e1fe3e4aSElliott Hughes                InstanceDescriptor(
365*e1fe3e4aSElliott Hughes                    filename=instance.filename,
366*e1fe3e4aSElliott Hughes                    path=instance.path,
367*e1fe3e4aSElliott Hughes                    font=instance.font,
368*e1fe3e4aSElliott Hughes                    name=instance.name,
369*e1fe3e4aSElliott Hughes                    userLocation={} if expandLocations else instance.userLocation,
370*e1fe3e4aSElliott Hughes                    designLocation=_filterLocation(
371*e1fe3e4aSElliott Hughes                        userRegion, maybeExpandDesignLocation(instance)
372*e1fe3e4aSElliott Hughes                    ),
373*e1fe3e4aSElliott Hughes                    familyName=instance.familyName,
374*e1fe3e4aSElliott Hughes                    styleName=instance.styleName,
375*e1fe3e4aSElliott Hughes                    postScriptFontName=instance.postScriptFontName,
376*e1fe3e4aSElliott Hughes                    styleMapFamilyName=instance.styleMapFamilyName,
377*e1fe3e4aSElliott Hughes                    styleMapStyleName=instance.styleMapStyleName,
378*e1fe3e4aSElliott Hughes                    localisedFamilyName=instance.localisedFamilyName,
379*e1fe3e4aSElliott Hughes                    localisedStyleName=instance.localisedStyleName,
380*e1fe3e4aSElliott Hughes                    localisedStyleMapFamilyName=instance.localisedStyleMapFamilyName,
381*e1fe3e4aSElliott Hughes                    localisedStyleMapStyleName=instance.localisedStyleMapStyleName,
382*e1fe3e4aSElliott Hughes                    lib=instance.lib,
383*e1fe3e4aSElliott Hughes                )
384*e1fe3e4aSElliott Hughes            )
385*e1fe3e4aSElliott Hughes
386*e1fe3e4aSElliott Hughes    subDoc.lib = doc.lib
387*e1fe3e4aSElliott Hughes
388*e1fe3e4aSElliott Hughes    return subDoc
389*e1fe3e4aSElliott Hughes
390*e1fe3e4aSElliott Hughes
391*e1fe3e4aSElliott Hughesdef _conditionSetFrom(conditionSet: List[Dict[str, Any]]) -> ConditionSet:
392*e1fe3e4aSElliott Hughes    c: Dict[str, Range] = {}
393*e1fe3e4aSElliott Hughes    for condition in conditionSet:
394*e1fe3e4aSElliott Hughes        minimum, maximum = condition.get("minimum"), condition.get("maximum")
395*e1fe3e4aSElliott Hughes        c[condition["name"]] = Range(
396*e1fe3e4aSElliott Hughes            minimum if minimum is not None else -math.inf,
397*e1fe3e4aSElliott Hughes            maximum if maximum is not None else math.inf,
398*e1fe3e4aSElliott Hughes        )
399*e1fe3e4aSElliott Hughes    return c
400*e1fe3e4aSElliott Hughes
401*e1fe3e4aSElliott Hughes
402*e1fe3e4aSElliott Hughesdef _subsetRulesBasedOnConditions(
403*e1fe3e4aSElliott Hughes    rules: List[RuleDescriptor], designRegion: Region
404*e1fe3e4aSElliott Hughes) -> List[RuleDescriptor]:
405*e1fe3e4aSElliott Hughes    # What rules to keep:
406*e1fe3e4aSElliott Hughes    #  - Keep the rule if any conditionset is relevant.
407*e1fe3e4aSElliott Hughes    #  - A conditionset is relevant if all conditions are relevant or it is empty.
408*e1fe3e4aSElliott Hughes    #  - A condition is relevant if
409*e1fe3e4aSElliott Hughes    #    - axis is point (C-AP),
410*e1fe3e4aSElliott Hughes    #       - and point in condition's range (C-AP-in)
411*e1fe3e4aSElliott Hughes    #            (in this case remove the condition because it's always true)
412*e1fe3e4aSElliott Hughes    #       - else (C-AP-out) whole conditionset can be discarded (condition false
413*e1fe3e4aSElliott Hughes    #         => conditionset false)
414*e1fe3e4aSElliott Hughes    #    - axis is range (C-AR),
415*e1fe3e4aSElliott Hughes    #       - (C-AR-all) and axis range fully contained in condition range: we can
416*e1fe3e4aSElliott Hughes    #         scrap the condition because it's always true
417*e1fe3e4aSElliott Hughes    #       - (C-AR-inter) and intersection(axis range, condition range) not empty:
418*e1fe3e4aSElliott Hughes    #         keep the condition with the smaller range (= intersection)
419*e1fe3e4aSElliott Hughes    #       - (C-AR-none) else, whole conditionset can be discarded
420*e1fe3e4aSElliott Hughes    newRules: List[RuleDescriptor] = []
421*e1fe3e4aSElliott Hughes    for rule in rules:
422*e1fe3e4aSElliott Hughes        newRule: RuleDescriptor = RuleDescriptor(
423*e1fe3e4aSElliott Hughes            name=rule.name, conditionSets=[], subs=rule.subs
424*e1fe3e4aSElliott Hughes        )
425*e1fe3e4aSElliott Hughes        for conditionset in rule.conditionSets:
426*e1fe3e4aSElliott Hughes            cs = _conditionSetFrom(conditionset)
427*e1fe3e4aSElliott Hughes            newConditionset: List[Dict[str, Any]] = []
428*e1fe3e4aSElliott Hughes            discardConditionset = False
429*e1fe3e4aSElliott Hughes            for selectionName, selectionValue in designRegion.items():
430*e1fe3e4aSElliott Hughes                # TODO: Ensure that all(key in conditionset for key in region.keys())?
431*e1fe3e4aSElliott Hughes                if selectionName not in cs:
432*e1fe3e4aSElliott Hughes                    # raise Exception("Selection has different axes than the rules")
433*e1fe3e4aSElliott Hughes                    continue
434*e1fe3e4aSElliott Hughes                if isinstance(selectionValue, (float, int)):  # is point
435*e1fe3e4aSElliott Hughes                    # Case C-AP-in
436*e1fe3e4aSElliott Hughes                    if selectionValue in cs[selectionName]:
437*e1fe3e4aSElliott Hughes                        pass  # always matches, conditionset can stay empty for this one.
438*e1fe3e4aSElliott Hughes                    # Case C-AP-out
439*e1fe3e4aSElliott Hughes                    else:
440*e1fe3e4aSElliott Hughes                        discardConditionset = True
441*e1fe3e4aSElliott Hughes                else:  # is range
442*e1fe3e4aSElliott Hughes                    # Case C-AR-all
443*e1fe3e4aSElliott Hughes                    if selectionValue in cs[selectionName]:
444*e1fe3e4aSElliott Hughes                        pass  # always matches, conditionset can stay empty for this one.
445*e1fe3e4aSElliott Hughes                    else:
446*e1fe3e4aSElliott Hughes                        intersection = cs[selectionName].intersection(selectionValue)
447*e1fe3e4aSElliott Hughes                        # Case C-AR-inter
448*e1fe3e4aSElliott Hughes                        if intersection is not None:
449*e1fe3e4aSElliott Hughes                            newConditionset.append(
450*e1fe3e4aSElliott Hughes                                {
451*e1fe3e4aSElliott Hughes                                    "name": selectionName,
452*e1fe3e4aSElliott Hughes                                    "minimum": intersection.minimum,
453*e1fe3e4aSElliott Hughes                                    "maximum": intersection.maximum,
454*e1fe3e4aSElliott Hughes                                }
455*e1fe3e4aSElliott Hughes                            )
456*e1fe3e4aSElliott Hughes                        # Case C-AR-none
457*e1fe3e4aSElliott Hughes                        else:
458*e1fe3e4aSElliott Hughes                            discardConditionset = True
459*e1fe3e4aSElliott Hughes            if not discardConditionset:
460*e1fe3e4aSElliott Hughes                newRule.conditionSets.append(newConditionset)
461*e1fe3e4aSElliott Hughes        if newRule.conditionSets:
462*e1fe3e4aSElliott Hughes            newRules.append(newRule)
463*e1fe3e4aSElliott Hughes
464*e1fe3e4aSElliott Hughes    return newRules
465*e1fe3e4aSElliott Hughes
466*e1fe3e4aSElliott Hughes
467*e1fe3e4aSElliott Hughesdef _filterLocation(
468*e1fe3e4aSElliott Hughes    userRegion: Region,
469*e1fe3e4aSElliott Hughes    location: Dict[str, float],
470*e1fe3e4aSElliott Hughes) -> Dict[str, float]:
471*e1fe3e4aSElliott Hughes    return {
472*e1fe3e4aSElliott Hughes        name: value
473*e1fe3e4aSElliott Hughes        for name, value in location.items()
474*e1fe3e4aSElliott Hughes        if name in userRegion and isinstance(userRegion[name], Range)
475*e1fe3e4aSElliott Hughes    }
476