xref: /aosp_15_r20/external/fonttools/Lib/fontTools/varLib/featureVars.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1*e1fe3e4aSElliott Hughes"""Module to build FeatureVariation tables:
2*e1fe3e4aSElliott Hugheshttps://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#featurevariations-table
3*e1fe3e4aSElliott Hughes
4*e1fe3e4aSElliott HughesNOTE: The API is experimental and subject to change.
5*e1fe3e4aSElliott Hughes"""
6*e1fe3e4aSElliott Hughes
7*e1fe3e4aSElliott Hughesfrom fontTools.misc.dictTools import hashdict
8*e1fe3e4aSElliott Hughesfrom fontTools.misc.intTools import bit_count
9*e1fe3e4aSElliott Hughesfrom fontTools.ttLib import newTable
10*e1fe3e4aSElliott Hughesfrom fontTools.ttLib.tables import otTables as ot
11*e1fe3e4aSElliott Hughesfrom fontTools.ttLib.ttVisitor import TTVisitor
12*e1fe3e4aSElliott Hughesfrom fontTools.otlLib.builder import buildLookup, buildSingleSubstSubtable
13*e1fe3e4aSElliott Hughesfrom collections import OrderedDict
14*e1fe3e4aSElliott Hughes
15*e1fe3e4aSElliott Hughesfrom .errors import VarLibError, VarLibValidationError
16*e1fe3e4aSElliott Hughes
17*e1fe3e4aSElliott Hughes
18*e1fe3e4aSElliott Hughesdef addFeatureVariations(font, conditionalSubstitutions, featureTag="rvrn"):
19*e1fe3e4aSElliott Hughes    """Add conditional substitutions to a Variable Font.
20*e1fe3e4aSElliott Hughes
21*e1fe3e4aSElliott Hughes    The `conditionalSubstitutions` argument is a list of (Region, Substitutions)
22*e1fe3e4aSElliott Hughes    tuples.
23*e1fe3e4aSElliott Hughes
24*e1fe3e4aSElliott Hughes    A Region is a list of Boxes. A Box is a dict mapping axisTags to
25*e1fe3e4aSElliott Hughes    (minValue, maxValue) tuples. Irrelevant axes may be omitted and they are
26*e1fe3e4aSElliott Hughes    interpretted as extending to end of axis in each direction.  A Box represents
27*e1fe3e4aSElliott Hughes    an orthogonal 'rectangular' subset of an N-dimensional design space.
28*e1fe3e4aSElliott Hughes    A Region represents a more complex subset of an N-dimensional design space,
29*e1fe3e4aSElliott Hughes    ie. the union of all the Boxes in the Region.
30*e1fe3e4aSElliott Hughes    For efficiency, Boxes within a Region should ideally not overlap, but
31*e1fe3e4aSElliott Hughes    functionality is not compromised if they do.
32*e1fe3e4aSElliott Hughes
33*e1fe3e4aSElliott Hughes    The minimum and maximum values are expressed in normalized coordinates.
34*e1fe3e4aSElliott Hughes
35*e1fe3e4aSElliott Hughes    A Substitution is a dict mapping source glyph names to substitute glyph names.
36*e1fe3e4aSElliott Hughes
37*e1fe3e4aSElliott Hughes    Example:
38*e1fe3e4aSElliott Hughes
39*e1fe3e4aSElliott Hughes    # >>> f = TTFont(srcPath)
40*e1fe3e4aSElliott Hughes    # >>> condSubst = [
41*e1fe3e4aSElliott Hughes    # ...     # A list of (Region, Substitution) tuples.
42*e1fe3e4aSElliott Hughes    # ...     ([{"wdth": (0.5, 1.0)}], {"cent": "cent.rvrn"}),
43*e1fe3e4aSElliott Hughes    # ...     ([{"wght": (0.5, 1.0)}], {"dollar": "dollar.rvrn"}),
44*e1fe3e4aSElliott Hughes    # ... ]
45*e1fe3e4aSElliott Hughes    # >>> addFeatureVariations(f, condSubst)
46*e1fe3e4aSElliott Hughes    # >>> f.save(dstPath)
47*e1fe3e4aSElliott Hughes
48*e1fe3e4aSElliott Hughes    The `featureTag` parameter takes either a str or a iterable of str (the single str
49*e1fe3e4aSElliott Hughes    is kept for backwards compatibility), and defines which feature(s) will be
50*e1fe3e4aSElliott Hughes    associated with the feature variations.
51*e1fe3e4aSElliott Hughes    Note, if this is "rvrn", then the substitution lookup will be inserted at the
52*e1fe3e4aSElliott Hughes    beginning of the lookup list so that it is processed before others, otherwise
53*e1fe3e4aSElliott Hughes    for any other feature tags it will be appended last.
54*e1fe3e4aSElliott Hughes    """
55*e1fe3e4aSElliott Hughes
56*e1fe3e4aSElliott Hughes    # process first when "rvrn" is the only listed tag
57*e1fe3e4aSElliott Hughes    featureTags = [featureTag] if isinstance(featureTag, str) else sorted(featureTag)
58*e1fe3e4aSElliott Hughes    processLast = "rvrn" not in featureTags or len(featureTags) > 1
59*e1fe3e4aSElliott Hughes
60*e1fe3e4aSElliott Hughes    _checkSubstitutionGlyphsExist(
61*e1fe3e4aSElliott Hughes        glyphNames=set(font.getGlyphOrder()),
62*e1fe3e4aSElliott Hughes        substitutions=conditionalSubstitutions,
63*e1fe3e4aSElliott Hughes    )
64*e1fe3e4aSElliott Hughes
65*e1fe3e4aSElliott Hughes    substitutions = overlayFeatureVariations(conditionalSubstitutions)
66*e1fe3e4aSElliott Hughes
67*e1fe3e4aSElliott Hughes    # turn substitution dicts into tuples of tuples, so they are hashable
68*e1fe3e4aSElliott Hughes    conditionalSubstitutions, allSubstitutions = makeSubstitutionsHashable(
69*e1fe3e4aSElliott Hughes        substitutions
70*e1fe3e4aSElliott Hughes    )
71*e1fe3e4aSElliott Hughes    if "GSUB" not in font:
72*e1fe3e4aSElliott Hughes        font["GSUB"] = buildGSUB()
73*e1fe3e4aSElliott Hughes    else:
74*e1fe3e4aSElliott Hughes        existingTags = _existingVariableFeatures(font["GSUB"].table).intersection(
75*e1fe3e4aSElliott Hughes            featureTags
76*e1fe3e4aSElliott Hughes        )
77*e1fe3e4aSElliott Hughes        if existingTags:
78*e1fe3e4aSElliott Hughes            raise VarLibError(
79*e1fe3e4aSElliott Hughes                f"FeatureVariations already exist for feature tag(s): {existingTags}"
80*e1fe3e4aSElliott Hughes            )
81*e1fe3e4aSElliott Hughes
82*e1fe3e4aSElliott Hughes    # setup lookups
83*e1fe3e4aSElliott Hughes    lookupMap = buildSubstitutionLookups(
84*e1fe3e4aSElliott Hughes        font["GSUB"].table, allSubstitutions, processLast
85*e1fe3e4aSElliott Hughes    )
86*e1fe3e4aSElliott Hughes
87*e1fe3e4aSElliott Hughes    # addFeatureVariationsRaw takes a list of
88*e1fe3e4aSElliott Hughes    #  ( {condition}, [ lookup indices ] )
89*e1fe3e4aSElliott Hughes    # so rearrange our lookups to match
90*e1fe3e4aSElliott Hughes    conditionsAndLookups = []
91*e1fe3e4aSElliott Hughes    for conditionSet, substitutions in conditionalSubstitutions:
92*e1fe3e4aSElliott Hughes        conditionsAndLookups.append(
93*e1fe3e4aSElliott Hughes            (conditionSet, [lookupMap[s] for s in substitutions])
94*e1fe3e4aSElliott Hughes        )
95*e1fe3e4aSElliott Hughes
96*e1fe3e4aSElliott Hughes    addFeatureVariationsRaw(font, font["GSUB"].table, conditionsAndLookups, featureTags)
97*e1fe3e4aSElliott Hughes
98*e1fe3e4aSElliott Hughes
99*e1fe3e4aSElliott Hughesdef _existingVariableFeatures(table):
100*e1fe3e4aSElliott Hughes    existingFeatureVarsTags = set()
101*e1fe3e4aSElliott Hughes    if hasattr(table, "FeatureVariations") and table.FeatureVariations is not None:
102*e1fe3e4aSElliott Hughes        features = table.FeatureList.FeatureRecord
103*e1fe3e4aSElliott Hughes        for fvr in table.FeatureVariations.FeatureVariationRecord:
104*e1fe3e4aSElliott Hughes            for ftsr in fvr.FeatureTableSubstitution.SubstitutionRecord:
105*e1fe3e4aSElliott Hughes                existingFeatureVarsTags.add(features[ftsr.FeatureIndex].FeatureTag)
106*e1fe3e4aSElliott Hughes    return existingFeatureVarsTags
107*e1fe3e4aSElliott Hughes
108*e1fe3e4aSElliott Hughes
109*e1fe3e4aSElliott Hughesdef _checkSubstitutionGlyphsExist(glyphNames, substitutions):
110*e1fe3e4aSElliott Hughes    referencedGlyphNames = set()
111*e1fe3e4aSElliott Hughes    for _, substitution in substitutions:
112*e1fe3e4aSElliott Hughes        referencedGlyphNames |= substitution.keys()
113*e1fe3e4aSElliott Hughes        referencedGlyphNames |= set(substitution.values())
114*e1fe3e4aSElliott Hughes    missing = referencedGlyphNames - glyphNames
115*e1fe3e4aSElliott Hughes    if missing:
116*e1fe3e4aSElliott Hughes        raise VarLibValidationError(
117*e1fe3e4aSElliott Hughes            "Missing glyphs are referenced in conditional substitution rules:"
118*e1fe3e4aSElliott Hughes            f" {', '.join(missing)}"
119*e1fe3e4aSElliott Hughes        )
120*e1fe3e4aSElliott Hughes
121*e1fe3e4aSElliott Hughes
122*e1fe3e4aSElliott Hughesdef overlayFeatureVariations(conditionalSubstitutions):
123*e1fe3e4aSElliott Hughes    """Compute overlaps between all conditional substitutions.
124*e1fe3e4aSElliott Hughes
125*e1fe3e4aSElliott Hughes    The `conditionalSubstitutions` argument is a list of (Region, Substitutions)
126*e1fe3e4aSElliott Hughes    tuples.
127*e1fe3e4aSElliott Hughes
128*e1fe3e4aSElliott Hughes    A Region is a list of Boxes. A Box is a dict mapping axisTags to
129*e1fe3e4aSElliott Hughes    (minValue, maxValue) tuples. Irrelevant axes may be omitted and they are
130*e1fe3e4aSElliott Hughes    interpretted as extending to end of axis in each direction.  A Box represents
131*e1fe3e4aSElliott Hughes    an orthogonal 'rectangular' subset of an N-dimensional design space.
132*e1fe3e4aSElliott Hughes    A Region represents a more complex subset of an N-dimensional design space,
133*e1fe3e4aSElliott Hughes    ie. the union of all the Boxes in the Region.
134*e1fe3e4aSElliott Hughes    For efficiency, Boxes within a Region should ideally not overlap, but
135*e1fe3e4aSElliott Hughes    functionality is not compromised if they do.
136*e1fe3e4aSElliott Hughes
137*e1fe3e4aSElliott Hughes    The minimum and maximum values are expressed in normalized coordinates.
138*e1fe3e4aSElliott Hughes
139*e1fe3e4aSElliott Hughes    A Substitution is a dict mapping source glyph names to substitute glyph names.
140*e1fe3e4aSElliott Hughes
141*e1fe3e4aSElliott Hughes    Returns data is in similar but different format.  Overlaps of distinct
142*e1fe3e4aSElliott Hughes    substitution Boxes (*not* Regions) are explicitly listed as distinct rules,
143*e1fe3e4aSElliott Hughes    and rules with the same Box merged.  The more specific rules appear earlier
144*e1fe3e4aSElliott Hughes    in the resulting list.  Moreover, instead of just a dictionary of substitutions,
145*e1fe3e4aSElliott Hughes    a list of dictionaries is returned for substitutions corresponding to each
146*e1fe3e4aSElliott Hughes    unique space, with each dictionary being identical to one of the input
147*e1fe3e4aSElliott Hughes    substitution dictionaries.  These dictionaries are not merged to allow data
148*e1fe3e4aSElliott Hughes    sharing when they are converted into font tables.
149*e1fe3e4aSElliott Hughes
150*e1fe3e4aSElliott Hughes    Example::
151*e1fe3e4aSElliott Hughes
152*e1fe3e4aSElliott Hughes        >>> condSubst = [
153*e1fe3e4aSElliott Hughes        ...     # A list of (Region, Substitution) tuples.
154*e1fe3e4aSElliott Hughes        ...     ([{"wght": (0.5, 1.0)}], {"dollar": "dollar.rvrn"}),
155*e1fe3e4aSElliott Hughes        ...     ([{"wght": (0.5, 1.0)}], {"dollar": "dollar.rvrn"}),
156*e1fe3e4aSElliott Hughes        ...     ([{"wdth": (0.5, 1.0)}], {"cent": "cent.rvrn"}),
157*e1fe3e4aSElliott Hughes        ...     ([{"wght": (0.5, 1.0), "wdth": (-1, 1.0)}], {"dollar": "dollar.rvrn"}),
158*e1fe3e4aSElliott Hughes        ... ]
159*e1fe3e4aSElliott Hughes        >>> from pprint import pprint
160*e1fe3e4aSElliott Hughes        >>> pprint(overlayFeatureVariations(condSubst))
161*e1fe3e4aSElliott Hughes        [({'wdth': (0.5, 1.0), 'wght': (0.5, 1.0)},
162*e1fe3e4aSElliott Hughes          [{'dollar': 'dollar.rvrn'}, {'cent': 'cent.rvrn'}]),
163*e1fe3e4aSElliott Hughes         ({'wdth': (0.5, 1.0)}, [{'cent': 'cent.rvrn'}]),
164*e1fe3e4aSElliott Hughes         ({'wght': (0.5, 1.0)}, [{'dollar': 'dollar.rvrn'}])]
165*e1fe3e4aSElliott Hughes
166*e1fe3e4aSElliott Hughes    """
167*e1fe3e4aSElliott Hughes
168*e1fe3e4aSElliott Hughes    # Merge same-substitutions rules, as this creates fewer number oflookups.
169*e1fe3e4aSElliott Hughes    merged = OrderedDict()
170*e1fe3e4aSElliott Hughes    for value, key in conditionalSubstitutions:
171*e1fe3e4aSElliott Hughes        key = hashdict(key)
172*e1fe3e4aSElliott Hughes        if key in merged:
173*e1fe3e4aSElliott Hughes            merged[key].extend(value)
174*e1fe3e4aSElliott Hughes        else:
175*e1fe3e4aSElliott Hughes            merged[key] = value
176*e1fe3e4aSElliott Hughes    conditionalSubstitutions = [(v, dict(k)) for k, v in merged.items()]
177*e1fe3e4aSElliott Hughes    del merged
178*e1fe3e4aSElliott Hughes
179*e1fe3e4aSElliott Hughes    # Merge same-region rules, as this is cheaper.
180*e1fe3e4aSElliott Hughes    # Also convert boxes to hashdict()
181*e1fe3e4aSElliott Hughes    #
182*e1fe3e4aSElliott Hughes    # Reversing is such that earlier entries win in case of conflicting substitution
183*e1fe3e4aSElliott Hughes    # rules for the same region.
184*e1fe3e4aSElliott Hughes    merged = OrderedDict()
185*e1fe3e4aSElliott Hughes    for key, value in reversed(conditionalSubstitutions):
186*e1fe3e4aSElliott Hughes        key = tuple(
187*e1fe3e4aSElliott Hughes            sorted(
188*e1fe3e4aSElliott Hughes                (hashdict(cleanupBox(k)) for k in key),
189*e1fe3e4aSElliott Hughes                key=lambda d: tuple(sorted(d.items())),
190*e1fe3e4aSElliott Hughes            )
191*e1fe3e4aSElliott Hughes        )
192*e1fe3e4aSElliott Hughes        if key in merged:
193*e1fe3e4aSElliott Hughes            merged[key].update(value)
194*e1fe3e4aSElliott Hughes        else:
195*e1fe3e4aSElliott Hughes            merged[key] = dict(value)
196*e1fe3e4aSElliott Hughes    conditionalSubstitutions = list(reversed(merged.items()))
197*e1fe3e4aSElliott Hughes    del merged
198*e1fe3e4aSElliott Hughes
199*e1fe3e4aSElliott Hughes    # Overlay
200*e1fe3e4aSElliott Hughes    #
201*e1fe3e4aSElliott Hughes    # Rank is the bit-set of the index of all contributing layers.
202*e1fe3e4aSElliott Hughes    initMapInit = ((hashdict(), 0),)  # Initializer representing the entire space
203*e1fe3e4aSElliott Hughes    boxMap = OrderedDict(initMapInit)  # Map from Box to Rank
204*e1fe3e4aSElliott Hughes    for i, (currRegion, _) in enumerate(conditionalSubstitutions):
205*e1fe3e4aSElliott Hughes        newMap = OrderedDict(initMapInit)
206*e1fe3e4aSElliott Hughes        currRank = 1 << i
207*e1fe3e4aSElliott Hughes        for box, rank in boxMap.items():
208*e1fe3e4aSElliott Hughes            for currBox in currRegion:
209*e1fe3e4aSElliott Hughes                intersection, remainder = overlayBox(currBox, box)
210*e1fe3e4aSElliott Hughes                if intersection is not None:
211*e1fe3e4aSElliott Hughes                    intersection = hashdict(intersection)
212*e1fe3e4aSElliott Hughes                    newMap[intersection] = newMap.get(intersection, 0) | rank | currRank
213*e1fe3e4aSElliott Hughes                if remainder is not None:
214*e1fe3e4aSElliott Hughes                    remainder = hashdict(remainder)
215*e1fe3e4aSElliott Hughes                    newMap[remainder] = newMap.get(remainder, 0) | rank
216*e1fe3e4aSElliott Hughes        boxMap = newMap
217*e1fe3e4aSElliott Hughes
218*e1fe3e4aSElliott Hughes    # Generate output
219*e1fe3e4aSElliott Hughes    items = []
220*e1fe3e4aSElliott Hughes    for box, rank in sorted(
221*e1fe3e4aSElliott Hughes        boxMap.items(), key=(lambda BoxAndRank: -bit_count(BoxAndRank[1]))
222*e1fe3e4aSElliott Hughes    ):
223*e1fe3e4aSElliott Hughes        # Skip any box that doesn't have any substitution.
224*e1fe3e4aSElliott Hughes        if rank == 0:
225*e1fe3e4aSElliott Hughes            continue
226*e1fe3e4aSElliott Hughes        substsList = []
227*e1fe3e4aSElliott Hughes        i = 0
228*e1fe3e4aSElliott Hughes        while rank:
229*e1fe3e4aSElliott Hughes            if rank & 1:
230*e1fe3e4aSElliott Hughes                substsList.append(conditionalSubstitutions[i][1])
231*e1fe3e4aSElliott Hughes            rank >>= 1
232*e1fe3e4aSElliott Hughes            i += 1
233*e1fe3e4aSElliott Hughes        items.append((dict(box), substsList))
234*e1fe3e4aSElliott Hughes    return items
235*e1fe3e4aSElliott Hughes
236*e1fe3e4aSElliott Hughes
237*e1fe3e4aSElliott Hughes#
238*e1fe3e4aSElliott Hughes# Terminology:
239*e1fe3e4aSElliott Hughes#
240*e1fe3e4aSElliott Hughes# A 'Box' is a dict representing an orthogonal "rectangular" bit of N-dimensional space.
241*e1fe3e4aSElliott Hughes# The keys in the dict are axis tags, the values are (minValue, maxValue) tuples.
242*e1fe3e4aSElliott Hughes# Missing dimensions (keys) are substituted by the default min and max values
243*e1fe3e4aSElliott Hughes# from the corresponding axes.
244*e1fe3e4aSElliott Hughes#
245*e1fe3e4aSElliott Hughes
246*e1fe3e4aSElliott Hughes
247*e1fe3e4aSElliott Hughesdef overlayBox(top, bot):
248*e1fe3e4aSElliott Hughes    """Overlays ``top`` box on top of ``bot`` box.
249*e1fe3e4aSElliott Hughes
250*e1fe3e4aSElliott Hughes    Returns two items:
251*e1fe3e4aSElliott Hughes
252*e1fe3e4aSElliott Hughes    * Box for intersection of ``top`` and ``bot``, or None if they don't intersect.
253*e1fe3e4aSElliott Hughes    * Box for remainder of ``bot``.  Remainder box might not be exact (since the
254*e1fe3e4aSElliott Hughes      remainder might not be a simple box), but is inclusive of the exact
255*e1fe3e4aSElliott Hughes      remainder.
256*e1fe3e4aSElliott Hughes    """
257*e1fe3e4aSElliott Hughes
258*e1fe3e4aSElliott Hughes    # Intersection
259*e1fe3e4aSElliott Hughes    intersection = {}
260*e1fe3e4aSElliott Hughes    intersection.update(top)
261*e1fe3e4aSElliott Hughes    intersection.update(bot)
262*e1fe3e4aSElliott Hughes    for axisTag in set(top) & set(bot):
263*e1fe3e4aSElliott Hughes        min1, max1 = top[axisTag]
264*e1fe3e4aSElliott Hughes        min2, max2 = bot[axisTag]
265*e1fe3e4aSElliott Hughes        minimum = max(min1, min2)
266*e1fe3e4aSElliott Hughes        maximum = min(max1, max2)
267*e1fe3e4aSElliott Hughes        if not minimum < maximum:
268*e1fe3e4aSElliott Hughes            return None, bot  # Do not intersect
269*e1fe3e4aSElliott Hughes        intersection[axisTag] = minimum, maximum
270*e1fe3e4aSElliott Hughes
271*e1fe3e4aSElliott Hughes    # Remainder
272*e1fe3e4aSElliott Hughes    #
273*e1fe3e4aSElliott Hughes    # Remainder is empty if bot's each axis range lies within that of intersection.
274*e1fe3e4aSElliott Hughes    #
275*e1fe3e4aSElliott Hughes    # Remainder is shrank if bot's each, except for exactly one, axis range lies
276*e1fe3e4aSElliott Hughes    # within that of intersection, and that one axis, it extrudes out of the
277*e1fe3e4aSElliott Hughes    # intersection only on one side.
278*e1fe3e4aSElliott Hughes    #
279*e1fe3e4aSElliott Hughes    # Bot is returned in full as remainder otherwise, as true remainder is not
280*e1fe3e4aSElliott Hughes    # representable as a single box.
281*e1fe3e4aSElliott Hughes
282*e1fe3e4aSElliott Hughes    remainder = dict(bot)
283*e1fe3e4aSElliott Hughes    extruding = False
284*e1fe3e4aSElliott Hughes    fullyInside = True
285*e1fe3e4aSElliott Hughes    for axisTag in top:
286*e1fe3e4aSElliott Hughes        if axisTag in bot:
287*e1fe3e4aSElliott Hughes            continue
288*e1fe3e4aSElliott Hughes        extruding = True
289*e1fe3e4aSElliott Hughes        fullyInside = False
290*e1fe3e4aSElliott Hughes        break
291*e1fe3e4aSElliott Hughes    for axisTag in bot:
292*e1fe3e4aSElliott Hughes        if axisTag not in top:
293*e1fe3e4aSElliott Hughes            continue  # Axis range lies fully within
294*e1fe3e4aSElliott Hughes        min1, max1 = intersection[axisTag]
295*e1fe3e4aSElliott Hughes        min2, max2 = bot[axisTag]
296*e1fe3e4aSElliott Hughes        if min1 <= min2 and max2 <= max1:
297*e1fe3e4aSElliott Hughes            continue  # Axis range lies fully within
298*e1fe3e4aSElliott Hughes
299*e1fe3e4aSElliott Hughes        # Bot's range doesn't fully lie within that of top's for this axis.
300*e1fe3e4aSElliott Hughes        # We know they intersect, so it cannot lie fully without either; so they
301*e1fe3e4aSElliott Hughes        # overlap.
302*e1fe3e4aSElliott Hughes
303*e1fe3e4aSElliott Hughes        # If we have had an overlapping axis before, remainder is not
304*e1fe3e4aSElliott Hughes        # representable as a box, so return full bottom and go home.
305*e1fe3e4aSElliott Hughes        if extruding:
306*e1fe3e4aSElliott Hughes            return intersection, bot
307*e1fe3e4aSElliott Hughes        extruding = True
308*e1fe3e4aSElliott Hughes        fullyInside = False
309*e1fe3e4aSElliott Hughes
310*e1fe3e4aSElliott Hughes        # Otherwise, cut remainder on this axis and continue.
311*e1fe3e4aSElliott Hughes        if min1 <= min2:
312*e1fe3e4aSElliott Hughes            # Right side survives.
313*e1fe3e4aSElliott Hughes            minimum = max(max1, min2)
314*e1fe3e4aSElliott Hughes            maximum = max2
315*e1fe3e4aSElliott Hughes        elif max2 <= max1:
316*e1fe3e4aSElliott Hughes            # Left side survives.
317*e1fe3e4aSElliott Hughes            minimum = min2
318*e1fe3e4aSElliott Hughes            maximum = min(min1, max2)
319*e1fe3e4aSElliott Hughes        else:
320*e1fe3e4aSElliott Hughes            # Remainder leaks out from both sides.  Can't cut either.
321*e1fe3e4aSElliott Hughes            return intersection, bot
322*e1fe3e4aSElliott Hughes
323*e1fe3e4aSElliott Hughes        remainder[axisTag] = minimum, maximum
324*e1fe3e4aSElliott Hughes
325*e1fe3e4aSElliott Hughes    if fullyInside:
326*e1fe3e4aSElliott Hughes        # bot is fully within intersection.  Remainder is empty.
327*e1fe3e4aSElliott Hughes        return intersection, None
328*e1fe3e4aSElliott Hughes
329*e1fe3e4aSElliott Hughes    return intersection, remainder
330*e1fe3e4aSElliott Hughes
331*e1fe3e4aSElliott Hughes
332*e1fe3e4aSElliott Hughesdef cleanupBox(box):
333*e1fe3e4aSElliott Hughes    """Return a sparse copy of `box`, without redundant (default) values.
334*e1fe3e4aSElliott Hughes
335*e1fe3e4aSElliott Hughes    >>> cleanupBox({})
336*e1fe3e4aSElliott Hughes    {}
337*e1fe3e4aSElliott Hughes    >>> cleanupBox({'wdth': (0.0, 1.0)})
338*e1fe3e4aSElliott Hughes    {'wdth': (0.0, 1.0)}
339*e1fe3e4aSElliott Hughes    >>> cleanupBox({'wdth': (-1.0, 1.0)})
340*e1fe3e4aSElliott Hughes    {}
341*e1fe3e4aSElliott Hughes
342*e1fe3e4aSElliott Hughes    """
343*e1fe3e4aSElliott Hughes    return {tag: limit for tag, limit in box.items() if limit != (-1.0, 1.0)}
344*e1fe3e4aSElliott Hughes
345*e1fe3e4aSElliott Hughes
346*e1fe3e4aSElliott Hughes#
347*e1fe3e4aSElliott Hughes# Low level implementation
348*e1fe3e4aSElliott Hughes#
349*e1fe3e4aSElliott Hughes
350*e1fe3e4aSElliott Hughes
351*e1fe3e4aSElliott Hughesdef addFeatureVariationsRaw(font, table, conditionalSubstitutions, featureTag="rvrn"):
352*e1fe3e4aSElliott Hughes    """Low level implementation of addFeatureVariations that directly
353*e1fe3e4aSElliott Hughes    models the possibilities of the FeatureVariations table."""
354*e1fe3e4aSElliott Hughes
355*e1fe3e4aSElliott Hughes    featureTags = [featureTag] if isinstance(featureTag, str) else sorted(featureTag)
356*e1fe3e4aSElliott Hughes    processLast = "rvrn" not in featureTags or len(featureTags) > 1
357*e1fe3e4aSElliott Hughes
358*e1fe3e4aSElliott Hughes    #
359*e1fe3e4aSElliott Hughes    # if a <featureTag> feature is not present:
360*e1fe3e4aSElliott Hughes    #     make empty <featureTag> feature
361*e1fe3e4aSElliott Hughes    #     sort features, get <featureTag> feature index
362*e1fe3e4aSElliott Hughes    #     add <featureTag> feature to all scripts
363*e1fe3e4aSElliott Hughes    # if a <featureTag> feature is present:
364*e1fe3e4aSElliott Hughes    #     reuse <featureTag> feature index
365*e1fe3e4aSElliott Hughes    # make lookups
366*e1fe3e4aSElliott Hughes    # add feature variations
367*e1fe3e4aSElliott Hughes    #
368*e1fe3e4aSElliott Hughes    if table.Version < 0x00010001:
369*e1fe3e4aSElliott Hughes        table.Version = 0x00010001  # allow table.FeatureVariations
370*e1fe3e4aSElliott Hughes
371*e1fe3e4aSElliott Hughes    varFeatureIndices = set()
372*e1fe3e4aSElliott Hughes
373*e1fe3e4aSElliott Hughes    existingTags = {
374*e1fe3e4aSElliott Hughes        feature.FeatureTag
375*e1fe3e4aSElliott Hughes        for feature in table.FeatureList.FeatureRecord
376*e1fe3e4aSElliott Hughes        if feature.FeatureTag in featureTags
377*e1fe3e4aSElliott Hughes    }
378*e1fe3e4aSElliott Hughes
379*e1fe3e4aSElliott Hughes    newTags = set(featureTags) - existingTags
380*e1fe3e4aSElliott Hughes    if newTags:
381*e1fe3e4aSElliott Hughes        varFeatures = []
382*e1fe3e4aSElliott Hughes        for featureTag in sorted(newTags):
383*e1fe3e4aSElliott Hughes            varFeature = buildFeatureRecord(featureTag, [])
384*e1fe3e4aSElliott Hughes            table.FeatureList.FeatureRecord.append(varFeature)
385*e1fe3e4aSElliott Hughes            varFeatures.append(varFeature)
386*e1fe3e4aSElliott Hughes        table.FeatureList.FeatureCount = len(table.FeatureList.FeatureRecord)
387*e1fe3e4aSElliott Hughes
388*e1fe3e4aSElliott Hughes        sortFeatureList(table)
389*e1fe3e4aSElliott Hughes
390*e1fe3e4aSElliott Hughes        for varFeature in varFeatures:
391*e1fe3e4aSElliott Hughes            varFeatureIndex = table.FeatureList.FeatureRecord.index(varFeature)
392*e1fe3e4aSElliott Hughes
393*e1fe3e4aSElliott Hughes            for scriptRecord in table.ScriptList.ScriptRecord:
394*e1fe3e4aSElliott Hughes                if scriptRecord.Script.DefaultLangSys is None:
395*e1fe3e4aSElliott Hughes                    raise VarLibError(
396*e1fe3e4aSElliott Hughes                        "Feature variations require that the script "
397*e1fe3e4aSElliott Hughes                        f"'{scriptRecord.ScriptTag}' defines a default language system."
398*e1fe3e4aSElliott Hughes                    )
399*e1fe3e4aSElliott Hughes                langSystems = [lsr.LangSys for lsr in scriptRecord.Script.LangSysRecord]
400*e1fe3e4aSElliott Hughes                for langSys in [scriptRecord.Script.DefaultLangSys] + langSystems:
401*e1fe3e4aSElliott Hughes                    langSys.FeatureIndex.append(varFeatureIndex)
402*e1fe3e4aSElliott Hughes                    langSys.FeatureCount = len(langSys.FeatureIndex)
403*e1fe3e4aSElliott Hughes            varFeatureIndices.add(varFeatureIndex)
404*e1fe3e4aSElliott Hughes
405*e1fe3e4aSElliott Hughes    if existingTags:
406*e1fe3e4aSElliott Hughes        # indices may have changed if we inserted new features and sorted feature list
407*e1fe3e4aSElliott Hughes        # so we must do this after the above
408*e1fe3e4aSElliott Hughes        varFeatureIndices.update(
409*e1fe3e4aSElliott Hughes            index
410*e1fe3e4aSElliott Hughes            for index, feature in enumerate(table.FeatureList.FeatureRecord)
411*e1fe3e4aSElliott Hughes            if feature.FeatureTag in existingTags
412*e1fe3e4aSElliott Hughes        )
413*e1fe3e4aSElliott Hughes
414*e1fe3e4aSElliott Hughes    axisIndices = {
415*e1fe3e4aSElliott Hughes        axis.axisTag: axisIndex for axisIndex, axis in enumerate(font["fvar"].axes)
416*e1fe3e4aSElliott Hughes    }
417*e1fe3e4aSElliott Hughes
418*e1fe3e4aSElliott Hughes    hasFeatureVariations = (
419*e1fe3e4aSElliott Hughes        hasattr(table, "FeatureVariations") and table.FeatureVariations is not None
420*e1fe3e4aSElliott Hughes    )
421*e1fe3e4aSElliott Hughes
422*e1fe3e4aSElliott Hughes    featureVariationRecords = []
423*e1fe3e4aSElliott Hughes    for conditionSet, lookupIndices in conditionalSubstitutions:
424*e1fe3e4aSElliott Hughes        conditionTable = []
425*e1fe3e4aSElliott Hughes        for axisTag, (minValue, maxValue) in sorted(conditionSet.items()):
426*e1fe3e4aSElliott Hughes            if minValue > maxValue:
427*e1fe3e4aSElliott Hughes                raise VarLibValidationError(
428*e1fe3e4aSElliott Hughes                    "A condition set has a minimum value above the maximum value."
429*e1fe3e4aSElliott Hughes                )
430*e1fe3e4aSElliott Hughes            ct = buildConditionTable(axisIndices[axisTag], minValue, maxValue)
431*e1fe3e4aSElliott Hughes            conditionTable.append(ct)
432*e1fe3e4aSElliott Hughes        records = []
433*e1fe3e4aSElliott Hughes        for varFeatureIndex in sorted(varFeatureIndices):
434*e1fe3e4aSElliott Hughes            existingLookupIndices = table.FeatureList.FeatureRecord[
435*e1fe3e4aSElliott Hughes                varFeatureIndex
436*e1fe3e4aSElliott Hughes            ].Feature.LookupListIndex
437*e1fe3e4aSElliott Hughes            combinedLookupIndices = (
438*e1fe3e4aSElliott Hughes                existingLookupIndices + lookupIndices
439*e1fe3e4aSElliott Hughes                if processLast
440*e1fe3e4aSElliott Hughes                else lookupIndices + existingLookupIndices
441*e1fe3e4aSElliott Hughes            )
442*e1fe3e4aSElliott Hughes
443*e1fe3e4aSElliott Hughes            records.append(
444*e1fe3e4aSElliott Hughes                buildFeatureTableSubstitutionRecord(
445*e1fe3e4aSElliott Hughes                    varFeatureIndex, combinedLookupIndices
446*e1fe3e4aSElliott Hughes                )
447*e1fe3e4aSElliott Hughes            )
448*e1fe3e4aSElliott Hughes        if hasFeatureVariations and (
449*e1fe3e4aSElliott Hughes            fvr := findFeatureVariationRecord(table.FeatureVariations, conditionTable)
450*e1fe3e4aSElliott Hughes        ):
451*e1fe3e4aSElliott Hughes            fvr.FeatureTableSubstitution.SubstitutionRecord.extend(records)
452*e1fe3e4aSElliott Hughes            fvr.FeatureTableSubstitution.SubstitutionCount = len(
453*e1fe3e4aSElliott Hughes                fvr.FeatureTableSubstitution.SubstitutionRecord
454*e1fe3e4aSElliott Hughes            )
455*e1fe3e4aSElliott Hughes        else:
456*e1fe3e4aSElliott Hughes            featureVariationRecords.append(
457*e1fe3e4aSElliott Hughes                buildFeatureVariationRecord(conditionTable, records)
458*e1fe3e4aSElliott Hughes            )
459*e1fe3e4aSElliott Hughes
460*e1fe3e4aSElliott Hughes    if hasFeatureVariations:
461*e1fe3e4aSElliott Hughes        if table.FeatureVariations.Version != 0x00010000:
462*e1fe3e4aSElliott Hughes            raise VarLibError(
463*e1fe3e4aSElliott Hughes                "Unsupported FeatureVariations table version: "
464*e1fe3e4aSElliott Hughes                f"0x{table.FeatureVariations.Version:08x} (expected 0x00010000)."
465*e1fe3e4aSElliott Hughes            )
466*e1fe3e4aSElliott Hughes        table.FeatureVariations.FeatureVariationRecord.extend(featureVariationRecords)
467*e1fe3e4aSElliott Hughes        table.FeatureVariations.FeatureVariationCount = len(
468*e1fe3e4aSElliott Hughes            table.FeatureVariations.FeatureVariationRecord
469*e1fe3e4aSElliott Hughes        )
470*e1fe3e4aSElliott Hughes    else:
471*e1fe3e4aSElliott Hughes        table.FeatureVariations = buildFeatureVariations(featureVariationRecords)
472*e1fe3e4aSElliott Hughes
473*e1fe3e4aSElliott Hughes
474*e1fe3e4aSElliott Hughes#
475*e1fe3e4aSElliott Hughes# Building GSUB/FeatureVariations internals
476*e1fe3e4aSElliott Hughes#
477*e1fe3e4aSElliott Hughes
478*e1fe3e4aSElliott Hughes
479*e1fe3e4aSElliott Hughesdef buildGSUB():
480*e1fe3e4aSElliott Hughes    """Build a GSUB table from scratch."""
481*e1fe3e4aSElliott Hughes    fontTable = newTable("GSUB")
482*e1fe3e4aSElliott Hughes    gsub = fontTable.table = ot.GSUB()
483*e1fe3e4aSElliott Hughes    gsub.Version = 0x00010001  # allow gsub.FeatureVariations
484*e1fe3e4aSElliott Hughes
485*e1fe3e4aSElliott Hughes    gsub.ScriptList = ot.ScriptList()
486*e1fe3e4aSElliott Hughes    gsub.ScriptList.ScriptRecord = []
487*e1fe3e4aSElliott Hughes    gsub.FeatureList = ot.FeatureList()
488*e1fe3e4aSElliott Hughes    gsub.FeatureList.FeatureRecord = []
489*e1fe3e4aSElliott Hughes    gsub.LookupList = ot.LookupList()
490*e1fe3e4aSElliott Hughes    gsub.LookupList.Lookup = []
491*e1fe3e4aSElliott Hughes
492*e1fe3e4aSElliott Hughes    srec = ot.ScriptRecord()
493*e1fe3e4aSElliott Hughes    srec.ScriptTag = "DFLT"
494*e1fe3e4aSElliott Hughes    srec.Script = ot.Script()
495*e1fe3e4aSElliott Hughes    srec.Script.DefaultLangSys = None
496*e1fe3e4aSElliott Hughes    srec.Script.LangSysRecord = []
497*e1fe3e4aSElliott Hughes    srec.Script.LangSysCount = 0
498*e1fe3e4aSElliott Hughes
499*e1fe3e4aSElliott Hughes    langrec = ot.LangSysRecord()
500*e1fe3e4aSElliott Hughes    langrec.LangSys = ot.LangSys()
501*e1fe3e4aSElliott Hughes    langrec.LangSys.ReqFeatureIndex = 0xFFFF
502*e1fe3e4aSElliott Hughes    langrec.LangSys.FeatureIndex = []
503*e1fe3e4aSElliott Hughes    srec.Script.DefaultLangSys = langrec.LangSys
504*e1fe3e4aSElliott Hughes
505*e1fe3e4aSElliott Hughes    gsub.ScriptList.ScriptRecord.append(srec)
506*e1fe3e4aSElliott Hughes    gsub.ScriptList.ScriptCount = 1
507*e1fe3e4aSElliott Hughes    gsub.FeatureVariations = None
508*e1fe3e4aSElliott Hughes
509*e1fe3e4aSElliott Hughes    return fontTable
510*e1fe3e4aSElliott Hughes
511*e1fe3e4aSElliott Hughes
512*e1fe3e4aSElliott Hughesdef makeSubstitutionsHashable(conditionalSubstitutions):
513*e1fe3e4aSElliott Hughes    """Turn all the substitution dictionaries in sorted tuples of tuples so
514*e1fe3e4aSElliott Hughes    they are hashable, to detect duplicates so we don't write out redundant
515*e1fe3e4aSElliott Hughes    data."""
516*e1fe3e4aSElliott Hughes    allSubstitutions = set()
517*e1fe3e4aSElliott Hughes    condSubst = []
518*e1fe3e4aSElliott Hughes    for conditionSet, substitutionMaps in conditionalSubstitutions:
519*e1fe3e4aSElliott Hughes        substitutions = []
520*e1fe3e4aSElliott Hughes        for substitutionMap in substitutionMaps:
521*e1fe3e4aSElliott Hughes            subst = tuple(sorted(substitutionMap.items()))
522*e1fe3e4aSElliott Hughes            substitutions.append(subst)
523*e1fe3e4aSElliott Hughes            allSubstitutions.add(subst)
524*e1fe3e4aSElliott Hughes        condSubst.append((conditionSet, substitutions))
525*e1fe3e4aSElliott Hughes    return condSubst, sorted(allSubstitutions)
526*e1fe3e4aSElliott Hughes
527*e1fe3e4aSElliott Hughes
528*e1fe3e4aSElliott Hughesclass ShifterVisitor(TTVisitor):
529*e1fe3e4aSElliott Hughes    def __init__(self, shift):
530*e1fe3e4aSElliott Hughes        self.shift = shift
531*e1fe3e4aSElliott Hughes
532*e1fe3e4aSElliott Hughes
533*e1fe3e4aSElliott Hughes@ShifterVisitor.register_attr(ot.Feature, "LookupListIndex")  # GSUB/GPOS
534*e1fe3e4aSElliott Hughesdef visit(visitor, obj, attr, value):
535*e1fe3e4aSElliott Hughes    shift = visitor.shift
536*e1fe3e4aSElliott Hughes    value = [l + shift for l in value]
537*e1fe3e4aSElliott Hughes    setattr(obj, attr, value)
538*e1fe3e4aSElliott Hughes
539*e1fe3e4aSElliott Hughes
540*e1fe3e4aSElliott Hughes@ShifterVisitor.register_attr(
541*e1fe3e4aSElliott Hughes    (ot.SubstLookupRecord, ot.PosLookupRecord), "LookupListIndex"
542*e1fe3e4aSElliott Hughes)
543*e1fe3e4aSElliott Hughesdef visit(visitor, obj, attr, value):
544*e1fe3e4aSElliott Hughes    setattr(obj, attr, visitor.shift + value)
545*e1fe3e4aSElliott Hughes
546*e1fe3e4aSElliott Hughes
547*e1fe3e4aSElliott Hughesdef buildSubstitutionLookups(gsub, allSubstitutions, processLast=False):
548*e1fe3e4aSElliott Hughes    """Build the lookups for the glyph substitutions, return a dict mapping
549*e1fe3e4aSElliott Hughes    the substitution to lookup indices."""
550*e1fe3e4aSElliott Hughes
551*e1fe3e4aSElliott Hughes    # Insert lookups at the beginning of the lookup vector
552*e1fe3e4aSElliott Hughes    # https://github.com/googlefonts/fontmake/issues/950
553*e1fe3e4aSElliott Hughes
554*e1fe3e4aSElliott Hughes    firstIndex = len(gsub.LookupList.Lookup) if processLast else 0
555*e1fe3e4aSElliott Hughes    lookupMap = {}
556*e1fe3e4aSElliott Hughes    for i, substitutionMap in enumerate(allSubstitutions):
557*e1fe3e4aSElliott Hughes        lookupMap[substitutionMap] = firstIndex + i
558*e1fe3e4aSElliott Hughes
559*e1fe3e4aSElliott Hughes    if not processLast:
560*e1fe3e4aSElliott Hughes        # Shift all lookup indices in gsub by len(allSubstitutions)
561*e1fe3e4aSElliott Hughes        shift = len(allSubstitutions)
562*e1fe3e4aSElliott Hughes        visitor = ShifterVisitor(shift)
563*e1fe3e4aSElliott Hughes        visitor.visit(gsub.FeatureList.FeatureRecord)
564*e1fe3e4aSElliott Hughes        visitor.visit(gsub.LookupList.Lookup)
565*e1fe3e4aSElliott Hughes
566*e1fe3e4aSElliott Hughes    for i, subst in enumerate(allSubstitutions):
567*e1fe3e4aSElliott Hughes        substMap = dict(subst)
568*e1fe3e4aSElliott Hughes        lookup = buildLookup([buildSingleSubstSubtable(substMap)])
569*e1fe3e4aSElliott Hughes        if processLast:
570*e1fe3e4aSElliott Hughes            gsub.LookupList.Lookup.append(lookup)
571*e1fe3e4aSElliott Hughes        else:
572*e1fe3e4aSElliott Hughes            gsub.LookupList.Lookup.insert(i, lookup)
573*e1fe3e4aSElliott Hughes        assert gsub.LookupList.Lookup[lookupMap[subst]] is lookup
574*e1fe3e4aSElliott Hughes    gsub.LookupList.LookupCount = len(gsub.LookupList.Lookup)
575*e1fe3e4aSElliott Hughes    return lookupMap
576*e1fe3e4aSElliott Hughes
577*e1fe3e4aSElliott Hughes
578*e1fe3e4aSElliott Hughesdef buildFeatureVariations(featureVariationRecords):
579*e1fe3e4aSElliott Hughes    """Build the FeatureVariations subtable."""
580*e1fe3e4aSElliott Hughes    fv = ot.FeatureVariations()
581*e1fe3e4aSElliott Hughes    fv.Version = 0x00010000
582*e1fe3e4aSElliott Hughes    fv.FeatureVariationRecord = featureVariationRecords
583*e1fe3e4aSElliott Hughes    fv.FeatureVariationCount = len(featureVariationRecords)
584*e1fe3e4aSElliott Hughes    return fv
585*e1fe3e4aSElliott Hughes
586*e1fe3e4aSElliott Hughes
587*e1fe3e4aSElliott Hughesdef buildFeatureRecord(featureTag, lookupListIndices):
588*e1fe3e4aSElliott Hughes    """Build a FeatureRecord."""
589*e1fe3e4aSElliott Hughes    fr = ot.FeatureRecord()
590*e1fe3e4aSElliott Hughes    fr.FeatureTag = featureTag
591*e1fe3e4aSElliott Hughes    fr.Feature = ot.Feature()
592*e1fe3e4aSElliott Hughes    fr.Feature.LookupListIndex = lookupListIndices
593*e1fe3e4aSElliott Hughes    fr.Feature.populateDefaults()
594*e1fe3e4aSElliott Hughes    return fr
595*e1fe3e4aSElliott Hughes
596*e1fe3e4aSElliott Hughes
597*e1fe3e4aSElliott Hughesdef buildFeatureVariationRecord(conditionTable, substitutionRecords):
598*e1fe3e4aSElliott Hughes    """Build a FeatureVariationRecord."""
599*e1fe3e4aSElliott Hughes    fvr = ot.FeatureVariationRecord()
600*e1fe3e4aSElliott Hughes    fvr.ConditionSet = ot.ConditionSet()
601*e1fe3e4aSElliott Hughes    fvr.ConditionSet.ConditionTable = conditionTable
602*e1fe3e4aSElliott Hughes    fvr.ConditionSet.ConditionCount = len(conditionTable)
603*e1fe3e4aSElliott Hughes    fvr.FeatureTableSubstitution = ot.FeatureTableSubstitution()
604*e1fe3e4aSElliott Hughes    fvr.FeatureTableSubstitution.Version = 0x00010000
605*e1fe3e4aSElliott Hughes    fvr.FeatureTableSubstitution.SubstitutionRecord = substitutionRecords
606*e1fe3e4aSElliott Hughes    fvr.FeatureTableSubstitution.SubstitutionCount = len(substitutionRecords)
607*e1fe3e4aSElliott Hughes    return fvr
608*e1fe3e4aSElliott Hughes
609*e1fe3e4aSElliott Hughes
610*e1fe3e4aSElliott Hughesdef buildFeatureTableSubstitutionRecord(featureIndex, lookupListIndices):
611*e1fe3e4aSElliott Hughes    """Build a FeatureTableSubstitutionRecord."""
612*e1fe3e4aSElliott Hughes    ftsr = ot.FeatureTableSubstitutionRecord()
613*e1fe3e4aSElliott Hughes    ftsr.FeatureIndex = featureIndex
614*e1fe3e4aSElliott Hughes    ftsr.Feature = ot.Feature()
615*e1fe3e4aSElliott Hughes    ftsr.Feature.LookupListIndex = lookupListIndices
616*e1fe3e4aSElliott Hughes    ftsr.Feature.LookupCount = len(lookupListIndices)
617*e1fe3e4aSElliott Hughes    return ftsr
618*e1fe3e4aSElliott Hughes
619*e1fe3e4aSElliott Hughes
620*e1fe3e4aSElliott Hughesdef buildConditionTable(axisIndex, filterRangeMinValue, filterRangeMaxValue):
621*e1fe3e4aSElliott Hughes    """Build a ConditionTable."""
622*e1fe3e4aSElliott Hughes    ct = ot.ConditionTable()
623*e1fe3e4aSElliott Hughes    ct.Format = 1
624*e1fe3e4aSElliott Hughes    ct.AxisIndex = axisIndex
625*e1fe3e4aSElliott Hughes    ct.FilterRangeMinValue = filterRangeMinValue
626*e1fe3e4aSElliott Hughes    ct.FilterRangeMaxValue = filterRangeMaxValue
627*e1fe3e4aSElliott Hughes    return ct
628*e1fe3e4aSElliott Hughes
629*e1fe3e4aSElliott Hughes
630*e1fe3e4aSElliott Hughesdef findFeatureVariationRecord(featureVariations, conditionTable):
631*e1fe3e4aSElliott Hughes    """Find a FeatureVariationRecord that has the same conditionTable."""
632*e1fe3e4aSElliott Hughes    if featureVariations.Version != 0x00010000:
633*e1fe3e4aSElliott Hughes        raise VarLibError(
634*e1fe3e4aSElliott Hughes            "Unsupported FeatureVariations table version: "
635*e1fe3e4aSElliott Hughes            f"0x{featureVariations.Version:08x} (expected 0x00010000)."
636*e1fe3e4aSElliott Hughes        )
637*e1fe3e4aSElliott Hughes
638*e1fe3e4aSElliott Hughes    for fvr in featureVariations.FeatureVariationRecord:
639*e1fe3e4aSElliott Hughes        if conditionTable == fvr.ConditionSet.ConditionTable:
640*e1fe3e4aSElliott Hughes            return fvr
641*e1fe3e4aSElliott Hughes
642*e1fe3e4aSElliott Hughes    return None
643*e1fe3e4aSElliott Hughes
644*e1fe3e4aSElliott Hughes
645*e1fe3e4aSElliott Hughesdef sortFeatureList(table):
646*e1fe3e4aSElliott Hughes    """Sort the feature list by feature tag, and remap the feature indices
647*e1fe3e4aSElliott Hughes    elsewhere. This is needed after the feature list has been modified.
648*e1fe3e4aSElliott Hughes    """
649*e1fe3e4aSElliott Hughes    # decorate, sort, undecorate, because we need to make an index remapping table
650*e1fe3e4aSElliott Hughes    tagIndexFea = [
651*e1fe3e4aSElliott Hughes        (fea.FeatureTag, index, fea)
652*e1fe3e4aSElliott Hughes        for index, fea in enumerate(table.FeatureList.FeatureRecord)
653*e1fe3e4aSElliott Hughes    ]
654*e1fe3e4aSElliott Hughes    tagIndexFea.sort()
655*e1fe3e4aSElliott Hughes    table.FeatureList.FeatureRecord = [fea for tag, index, fea in tagIndexFea]
656*e1fe3e4aSElliott Hughes    featureRemap = dict(
657*e1fe3e4aSElliott Hughes        zip([index for tag, index, fea in tagIndexFea], range(len(tagIndexFea)))
658*e1fe3e4aSElliott Hughes    )
659*e1fe3e4aSElliott Hughes
660*e1fe3e4aSElliott Hughes    # Remap the feature indices
661*e1fe3e4aSElliott Hughes    remapFeatures(table, featureRemap)
662*e1fe3e4aSElliott Hughes
663*e1fe3e4aSElliott Hughes
664*e1fe3e4aSElliott Hughesdef remapFeatures(table, featureRemap):
665*e1fe3e4aSElliott Hughes    """Go through the scripts list, and remap feature indices."""
666*e1fe3e4aSElliott Hughes    for scriptIndex, script in enumerate(table.ScriptList.ScriptRecord):
667*e1fe3e4aSElliott Hughes        defaultLangSys = script.Script.DefaultLangSys
668*e1fe3e4aSElliott Hughes        if defaultLangSys is not None:
669*e1fe3e4aSElliott Hughes            _remapLangSys(defaultLangSys, featureRemap)
670*e1fe3e4aSElliott Hughes        for langSysRecordIndex, langSysRec in enumerate(script.Script.LangSysRecord):
671*e1fe3e4aSElliott Hughes            langSys = langSysRec.LangSys
672*e1fe3e4aSElliott Hughes            _remapLangSys(langSys, featureRemap)
673*e1fe3e4aSElliott Hughes
674*e1fe3e4aSElliott Hughes    if hasattr(table, "FeatureVariations") and table.FeatureVariations is not None:
675*e1fe3e4aSElliott Hughes        for fvr in table.FeatureVariations.FeatureVariationRecord:
676*e1fe3e4aSElliott Hughes            for ftsr in fvr.FeatureTableSubstitution.SubstitutionRecord:
677*e1fe3e4aSElliott Hughes                ftsr.FeatureIndex = featureRemap[ftsr.FeatureIndex]
678*e1fe3e4aSElliott Hughes
679*e1fe3e4aSElliott Hughes
680*e1fe3e4aSElliott Hughesdef _remapLangSys(langSys, featureRemap):
681*e1fe3e4aSElliott Hughes    if langSys.ReqFeatureIndex != 0xFFFF:
682*e1fe3e4aSElliott Hughes        langSys.ReqFeatureIndex = featureRemap[langSys.ReqFeatureIndex]
683*e1fe3e4aSElliott Hughes    langSys.FeatureIndex = [featureRemap[index] for index in langSys.FeatureIndex]
684*e1fe3e4aSElliott Hughes
685*e1fe3e4aSElliott Hughes
686*e1fe3e4aSElliott Hughesif __name__ == "__main__":
687*e1fe3e4aSElliott Hughes    import doctest, sys
688*e1fe3e4aSElliott Hughes
689*e1fe3e4aSElliott Hughes    sys.exit(doctest.testmod().failed)
690