xref: /aosp_15_r20/external/fonttools/Lib/fontTools/ttLib/tables/TupleVariation.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1*e1fe3e4aSElliott Hughesfrom fontTools.misc.fixedTools import (
2*e1fe3e4aSElliott Hughes    fixedToFloat as fi2fl,
3*e1fe3e4aSElliott Hughes    floatToFixed as fl2fi,
4*e1fe3e4aSElliott Hughes    floatToFixedToStr as fl2str,
5*e1fe3e4aSElliott Hughes    strToFixedToFloat as str2fl,
6*e1fe3e4aSElliott Hughes    otRound,
7*e1fe3e4aSElliott Hughes)
8*e1fe3e4aSElliott Hughesfrom fontTools.misc.textTools import safeEval
9*e1fe3e4aSElliott Hughesimport array
10*e1fe3e4aSElliott Hughesfrom collections import Counter, defaultdict
11*e1fe3e4aSElliott Hughesimport io
12*e1fe3e4aSElliott Hughesimport logging
13*e1fe3e4aSElliott Hughesimport struct
14*e1fe3e4aSElliott Hughesimport sys
15*e1fe3e4aSElliott Hughes
16*e1fe3e4aSElliott Hughes
17*e1fe3e4aSElliott Hughes# https://www.microsoft.com/typography/otspec/otvarcommonformats.htm
18*e1fe3e4aSElliott Hughes
19*e1fe3e4aSElliott HughesEMBEDDED_PEAK_TUPLE = 0x8000
20*e1fe3e4aSElliott HughesINTERMEDIATE_REGION = 0x4000
21*e1fe3e4aSElliott HughesPRIVATE_POINT_NUMBERS = 0x2000
22*e1fe3e4aSElliott Hughes
23*e1fe3e4aSElliott HughesDELTAS_ARE_ZERO = 0x80
24*e1fe3e4aSElliott HughesDELTAS_ARE_WORDS = 0x40
25*e1fe3e4aSElliott HughesDELTA_RUN_COUNT_MASK = 0x3F
26*e1fe3e4aSElliott Hughes
27*e1fe3e4aSElliott HughesPOINTS_ARE_WORDS = 0x80
28*e1fe3e4aSElliott HughesPOINT_RUN_COUNT_MASK = 0x7F
29*e1fe3e4aSElliott Hughes
30*e1fe3e4aSElliott HughesTUPLES_SHARE_POINT_NUMBERS = 0x8000
31*e1fe3e4aSElliott HughesTUPLE_COUNT_MASK = 0x0FFF
32*e1fe3e4aSElliott HughesTUPLE_INDEX_MASK = 0x0FFF
33*e1fe3e4aSElliott Hughes
34*e1fe3e4aSElliott Hugheslog = logging.getLogger(__name__)
35*e1fe3e4aSElliott Hughes
36*e1fe3e4aSElliott Hughes
37*e1fe3e4aSElliott Hughesclass TupleVariation(object):
38*e1fe3e4aSElliott Hughes    def __init__(self, axes, coordinates):
39*e1fe3e4aSElliott Hughes        self.axes = axes.copy()
40*e1fe3e4aSElliott Hughes        self.coordinates = list(coordinates)
41*e1fe3e4aSElliott Hughes
42*e1fe3e4aSElliott Hughes    def __repr__(self):
43*e1fe3e4aSElliott Hughes        axes = ",".join(
44*e1fe3e4aSElliott Hughes            sorted(["%s=%s" % (name, value) for (name, value) in self.axes.items()])
45*e1fe3e4aSElliott Hughes        )
46*e1fe3e4aSElliott Hughes        return "<TupleVariation %s %s>" % (axes, self.coordinates)
47*e1fe3e4aSElliott Hughes
48*e1fe3e4aSElliott Hughes    def __eq__(self, other):
49*e1fe3e4aSElliott Hughes        return self.coordinates == other.coordinates and self.axes == other.axes
50*e1fe3e4aSElliott Hughes
51*e1fe3e4aSElliott Hughes    def getUsedPoints(self):
52*e1fe3e4aSElliott Hughes        # Empty set means "all points used".
53*e1fe3e4aSElliott Hughes        if None not in self.coordinates:
54*e1fe3e4aSElliott Hughes            return frozenset()
55*e1fe3e4aSElliott Hughes        used = frozenset([i for i, p in enumerate(self.coordinates) if p is not None])
56*e1fe3e4aSElliott Hughes        # Return None if no points used.
57*e1fe3e4aSElliott Hughes        return used if used else None
58*e1fe3e4aSElliott Hughes
59*e1fe3e4aSElliott Hughes    def hasImpact(self):
60*e1fe3e4aSElliott Hughes        """Returns True if this TupleVariation has any visible impact.
61*e1fe3e4aSElliott Hughes
62*e1fe3e4aSElliott Hughes        If the result is False, the TupleVariation can be omitted from the font
63*e1fe3e4aSElliott Hughes        without making any visible difference.
64*e1fe3e4aSElliott Hughes        """
65*e1fe3e4aSElliott Hughes        return any(c is not None for c in self.coordinates)
66*e1fe3e4aSElliott Hughes
67*e1fe3e4aSElliott Hughes    def toXML(self, writer, axisTags):
68*e1fe3e4aSElliott Hughes        writer.begintag("tuple")
69*e1fe3e4aSElliott Hughes        writer.newline()
70*e1fe3e4aSElliott Hughes        for axis in axisTags:
71*e1fe3e4aSElliott Hughes            value = self.axes.get(axis)
72*e1fe3e4aSElliott Hughes            if value is not None:
73*e1fe3e4aSElliott Hughes                minValue, value, maxValue = value
74*e1fe3e4aSElliott Hughes                defaultMinValue = min(value, 0.0)  # -0.3 --> -0.3; 0.7 --> 0.0
75*e1fe3e4aSElliott Hughes                defaultMaxValue = max(value, 0.0)  # -0.3 -->  0.0; 0.7 --> 0.7
76*e1fe3e4aSElliott Hughes                if minValue == defaultMinValue and maxValue == defaultMaxValue:
77*e1fe3e4aSElliott Hughes                    writer.simpletag("coord", axis=axis, value=fl2str(value, 14))
78*e1fe3e4aSElliott Hughes                else:
79*e1fe3e4aSElliott Hughes                    attrs = [
80*e1fe3e4aSElliott Hughes                        ("axis", axis),
81*e1fe3e4aSElliott Hughes                        ("min", fl2str(minValue, 14)),
82*e1fe3e4aSElliott Hughes                        ("value", fl2str(value, 14)),
83*e1fe3e4aSElliott Hughes                        ("max", fl2str(maxValue, 14)),
84*e1fe3e4aSElliott Hughes                    ]
85*e1fe3e4aSElliott Hughes                    writer.simpletag("coord", attrs)
86*e1fe3e4aSElliott Hughes                writer.newline()
87*e1fe3e4aSElliott Hughes        wrote_any_deltas = False
88*e1fe3e4aSElliott Hughes        for i, delta in enumerate(self.coordinates):
89*e1fe3e4aSElliott Hughes            if type(delta) == tuple and len(delta) == 2:
90*e1fe3e4aSElliott Hughes                writer.simpletag("delta", pt=i, x=delta[0], y=delta[1])
91*e1fe3e4aSElliott Hughes                writer.newline()
92*e1fe3e4aSElliott Hughes                wrote_any_deltas = True
93*e1fe3e4aSElliott Hughes            elif type(delta) == int:
94*e1fe3e4aSElliott Hughes                writer.simpletag("delta", cvt=i, value=delta)
95*e1fe3e4aSElliott Hughes                writer.newline()
96*e1fe3e4aSElliott Hughes                wrote_any_deltas = True
97*e1fe3e4aSElliott Hughes            elif delta is not None:
98*e1fe3e4aSElliott Hughes                log.error("bad delta format")
99*e1fe3e4aSElliott Hughes                writer.comment("bad delta #%d" % i)
100*e1fe3e4aSElliott Hughes                writer.newline()
101*e1fe3e4aSElliott Hughes                wrote_any_deltas = True
102*e1fe3e4aSElliott Hughes        if not wrote_any_deltas:
103*e1fe3e4aSElliott Hughes            writer.comment("no deltas")
104*e1fe3e4aSElliott Hughes            writer.newline()
105*e1fe3e4aSElliott Hughes        writer.endtag("tuple")
106*e1fe3e4aSElliott Hughes        writer.newline()
107*e1fe3e4aSElliott Hughes
108*e1fe3e4aSElliott Hughes    def fromXML(self, name, attrs, _content):
109*e1fe3e4aSElliott Hughes        if name == "coord":
110*e1fe3e4aSElliott Hughes            axis = attrs["axis"]
111*e1fe3e4aSElliott Hughes            value = str2fl(attrs["value"], 14)
112*e1fe3e4aSElliott Hughes            defaultMinValue = min(value, 0.0)  # -0.3 --> -0.3; 0.7 --> 0.0
113*e1fe3e4aSElliott Hughes            defaultMaxValue = max(value, 0.0)  # -0.3 -->  0.0; 0.7 --> 0.7
114*e1fe3e4aSElliott Hughes            minValue = str2fl(attrs.get("min", defaultMinValue), 14)
115*e1fe3e4aSElliott Hughes            maxValue = str2fl(attrs.get("max", defaultMaxValue), 14)
116*e1fe3e4aSElliott Hughes            self.axes[axis] = (minValue, value, maxValue)
117*e1fe3e4aSElliott Hughes        elif name == "delta":
118*e1fe3e4aSElliott Hughes            if "pt" in attrs:
119*e1fe3e4aSElliott Hughes                point = safeEval(attrs["pt"])
120*e1fe3e4aSElliott Hughes                x = safeEval(attrs["x"])
121*e1fe3e4aSElliott Hughes                y = safeEval(attrs["y"])
122*e1fe3e4aSElliott Hughes                self.coordinates[point] = (x, y)
123*e1fe3e4aSElliott Hughes            elif "cvt" in attrs:
124*e1fe3e4aSElliott Hughes                cvt = safeEval(attrs["cvt"])
125*e1fe3e4aSElliott Hughes                value = safeEval(attrs["value"])
126*e1fe3e4aSElliott Hughes                self.coordinates[cvt] = value
127*e1fe3e4aSElliott Hughes            else:
128*e1fe3e4aSElliott Hughes                log.warning("bad delta format: %s" % ", ".join(sorted(attrs.keys())))
129*e1fe3e4aSElliott Hughes
130*e1fe3e4aSElliott Hughes    def compile(self, axisTags, sharedCoordIndices={}, pointData=None):
131*e1fe3e4aSElliott Hughes        assert set(self.axes.keys()) <= set(axisTags), (
132*e1fe3e4aSElliott Hughes            "Unknown axis tag found.",
133*e1fe3e4aSElliott Hughes            self.axes.keys(),
134*e1fe3e4aSElliott Hughes            axisTags,
135*e1fe3e4aSElliott Hughes        )
136*e1fe3e4aSElliott Hughes
137*e1fe3e4aSElliott Hughes        tupleData = []
138*e1fe3e4aSElliott Hughes        auxData = []
139*e1fe3e4aSElliott Hughes
140*e1fe3e4aSElliott Hughes        if pointData is None:
141*e1fe3e4aSElliott Hughes            usedPoints = self.getUsedPoints()
142*e1fe3e4aSElliott Hughes            if usedPoints is None:  # Nothing to encode
143*e1fe3e4aSElliott Hughes                return b"", b""
144*e1fe3e4aSElliott Hughes            pointData = self.compilePoints(usedPoints)
145*e1fe3e4aSElliott Hughes
146*e1fe3e4aSElliott Hughes        coord = self.compileCoord(axisTags)
147*e1fe3e4aSElliott Hughes        flags = sharedCoordIndices.get(coord)
148*e1fe3e4aSElliott Hughes        if flags is None:
149*e1fe3e4aSElliott Hughes            flags = EMBEDDED_PEAK_TUPLE
150*e1fe3e4aSElliott Hughes            tupleData.append(coord)
151*e1fe3e4aSElliott Hughes
152*e1fe3e4aSElliott Hughes        intermediateCoord = self.compileIntermediateCoord(axisTags)
153*e1fe3e4aSElliott Hughes        if intermediateCoord is not None:
154*e1fe3e4aSElliott Hughes            flags |= INTERMEDIATE_REGION
155*e1fe3e4aSElliott Hughes            tupleData.append(intermediateCoord)
156*e1fe3e4aSElliott Hughes
157*e1fe3e4aSElliott Hughes        # pointData of b'' implies "use shared points".
158*e1fe3e4aSElliott Hughes        if pointData:
159*e1fe3e4aSElliott Hughes            flags |= PRIVATE_POINT_NUMBERS
160*e1fe3e4aSElliott Hughes            auxData.append(pointData)
161*e1fe3e4aSElliott Hughes
162*e1fe3e4aSElliott Hughes        auxData.append(self.compileDeltas())
163*e1fe3e4aSElliott Hughes        auxData = b"".join(auxData)
164*e1fe3e4aSElliott Hughes
165*e1fe3e4aSElliott Hughes        tupleData.insert(0, struct.pack(">HH", len(auxData), flags))
166*e1fe3e4aSElliott Hughes        return b"".join(tupleData), auxData
167*e1fe3e4aSElliott Hughes
168*e1fe3e4aSElliott Hughes    def compileCoord(self, axisTags):
169*e1fe3e4aSElliott Hughes        result = []
170*e1fe3e4aSElliott Hughes        axes = self.axes
171*e1fe3e4aSElliott Hughes        for axis in axisTags:
172*e1fe3e4aSElliott Hughes            triple = axes.get(axis)
173*e1fe3e4aSElliott Hughes            if triple is None:
174*e1fe3e4aSElliott Hughes                result.append(b"\0\0")
175*e1fe3e4aSElliott Hughes            else:
176*e1fe3e4aSElliott Hughes                result.append(struct.pack(">h", fl2fi(triple[1], 14)))
177*e1fe3e4aSElliott Hughes        return b"".join(result)
178*e1fe3e4aSElliott Hughes
179*e1fe3e4aSElliott Hughes    def compileIntermediateCoord(self, axisTags):
180*e1fe3e4aSElliott Hughes        needed = False
181*e1fe3e4aSElliott Hughes        for axis in axisTags:
182*e1fe3e4aSElliott Hughes            minValue, value, maxValue = self.axes.get(axis, (0.0, 0.0, 0.0))
183*e1fe3e4aSElliott Hughes            defaultMinValue = min(value, 0.0)  # -0.3 --> -0.3; 0.7 --> 0.0
184*e1fe3e4aSElliott Hughes            defaultMaxValue = max(value, 0.0)  # -0.3 -->  0.0; 0.7 --> 0.7
185*e1fe3e4aSElliott Hughes            if (minValue != defaultMinValue) or (maxValue != defaultMaxValue):
186*e1fe3e4aSElliott Hughes                needed = True
187*e1fe3e4aSElliott Hughes                break
188*e1fe3e4aSElliott Hughes        if not needed:
189*e1fe3e4aSElliott Hughes            return None
190*e1fe3e4aSElliott Hughes        minCoords = []
191*e1fe3e4aSElliott Hughes        maxCoords = []
192*e1fe3e4aSElliott Hughes        for axis in axisTags:
193*e1fe3e4aSElliott Hughes            minValue, value, maxValue = self.axes.get(axis, (0.0, 0.0, 0.0))
194*e1fe3e4aSElliott Hughes            minCoords.append(struct.pack(">h", fl2fi(minValue, 14)))
195*e1fe3e4aSElliott Hughes            maxCoords.append(struct.pack(">h", fl2fi(maxValue, 14)))
196*e1fe3e4aSElliott Hughes        return b"".join(minCoords + maxCoords)
197*e1fe3e4aSElliott Hughes
198*e1fe3e4aSElliott Hughes    @staticmethod
199*e1fe3e4aSElliott Hughes    def decompileCoord_(axisTags, data, offset):
200*e1fe3e4aSElliott Hughes        coord = {}
201*e1fe3e4aSElliott Hughes        pos = offset
202*e1fe3e4aSElliott Hughes        for axis in axisTags:
203*e1fe3e4aSElliott Hughes            coord[axis] = fi2fl(struct.unpack(">h", data[pos : pos + 2])[0], 14)
204*e1fe3e4aSElliott Hughes            pos += 2
205*e1fe3e4aSElliott Hughes        return coord, pos
206*e1fe3e4aSElliott Hughes
207*e1fe3e4aSElliott Hughes    @staticmethod
208*e1fe3e4aSElliott Hughes    def compilePoints(points):
209*e1fe3e4aSElliott Hughes        # If the set consists of all points in the glyph, it gets encoded with
210*e1fe3e4aSElliott Hughes        # a special encoding: a single zero byte.
211*e1fe3e4aSElliott Hughes        #
212*e1fe3e4aSElliott Hughes        # To use this optimization, points passed in must be empty set.
213*e1fe3e4aSElliott Hughes        # The following two lines are not strictly necessary as the main code
214*e1fe3e4aSElliott Hughes        # below would emit the same. But this is most common and faster.
215*e1fe3e4aSElliott Hughes        if not points:
216*e1fe3e4aSElliott Hughes            return b"\0"
217*e1fe3e4aSElliott Hughes
218*e1fe3e4aSElliott Hughes        # In the 'gvar' table, the packing of point numbers is a little surprising.
219*e1fe3e4aSElliott Hughes        # It consists of multiple runs, each being a delta-encoded list of integers.
220*e1fe3e4aSElliott Hughes        # For example, the point set {17, 18, 19, 20, 21, 22, 23} gets encoded as
221*e1fe3e4aSElliott Hughes        # [6, 17, 1, 1, 1, 1, 1, 1]. The first value (6) is the run length minus 1.
222*e1fe3e4aSElliott Hughes        # There are two types of runs, with values being either 8 or 16 bit unsigned
223*e1fe3e4aSElliott Hughes        # integers.
224*e1fe3e4aSElliott Hughes        points = list(points)
225*e1fe3e4aSElliott Hughes        points.sort()
226*e1fe3e4aSElliott Hughes        numPoints = len(points)
227*e1fe3e4aSElliott Hughes
228*e1fe3e4aSElliott Hughes        result = bytearray()
229*e1fe3e4aSElliott Hughes        # The binary representation starts with the total number of points in the set,
230*e1fe3e4aSElliott Hughes        # encoded into one or two bytes depending on the value.
231*e1fe3e4aSElliott Hughes        if numPoints < 0x80:
232*e1fe3e4aSElliott Hughes            result.append(numPoints)
233*e1fe3e4aSElliott Hughes        else:
234*e1fe3e4aSElliott Hughes            result.append((numPoints >> 8) | 0x80)
235*e1fe3e4aSElliott Hughes            result.append(numPoints & 0xFF)
236*e1fe3e4aSElliott Hughes
237*e1fe3e4aSElliott Hughes        MAX_RUN_LENGTH = 127
238*e1fe3e4aSElliott Hughes        pos = 0
239*e1fe3e4aSElliott Hughes        lastValue = 0
240*e1fe3e4aSElliott Hughes        while pos < numPoints:
241*e1fe3e4aSElliott Hughes            runLength = 0
242*e1fe3e4aSElliott Hughes
243*e1fe3e4aSElliott Hughes            headerPos = len(result)
244*e1fe3e4aSElliott Hughes            result.append(0)
245*e1fe3e4aSElliott Hughes
246*e1fe3e4aSElliott Hughes            useByteEncoding = None
247*e1fe3e4aSElliott Hughes            while pos < numPoints and runLength <= MAX_RUN_LENGTH:
248*e1fe3e4aSElliott Hughes                curValue = points[pos]
249*e1fe3e4aSElliott Hughes                delta = curValue - lastValue
250*e1fe3e4aSElliott Hughes                if useByteEncoding is None:
251*e1fe3e4aSElliott Hughes                    useByteEncoding = 0 <= delta <= 0xFF
252*e1fe3e4aSElliott Hughes                if useByteEncoding and (delta > 0xFF or delta < 0):
253*e1fe3e4aSElliott Hughes                    # we need to start a new run (which will not use byte encoding)
254*e1fe3e4aSElliott Hughes                    break
255*e1fe3e4aSElliott Hughes                # TODO This never switches back to a byte-encoding from a short-encoding.
256*e1fe3e4aSElliott Hughes                # That's suboptimal.
257*e1fe3e4aSElliott Hughes                if useByteEncoding:
258*e1fe3e4aSElliott Hughes                    result.append(delta)
259*e1fe3e4aSElliott Hughes                else:
260*e1fe3e4aSElliott Hughes                    result.append(delta >> 8)
261*e1fe3e4aSElliott Hughes                    result.append(delta & 0xFF)
262*e1fe3e4aSElliott Hughes                lastValue = curValue
263*e1fe3e4aSElliott Hughes                pos += 1
264*e1fe3e4aSElliott Hughes                runLength += 1
265*e1fe3e4aSElliott Hughes            if useByteEncoding:
266*e1fe3e4aSElliott Hughes                result[headerPos] = runLength - 1
267*e1fe3e4aSElliott Hughes            else:
268*e1fe3e4aSElliott Hughes                result[headerPos] = (runLength - 1) | POINTS_ARE_WORDS
269*e1fe3e4aSElliott Hughes
270*e1fe3e4aSElliott Hughes        return result
271*e1fe3e4aSElliott Hughes
272*e1fe3e4aSElliott Hughes    @staticmethod
273*e1fe3e4aSElliott Hughes    def decompilePoints_(numPoints, data, offset, tableTag):
274*e1fe3e4aSElliott Hughes        """(numPoints, data, offset, tableTag) --> ([point1, point2, ...], newOffset)"""
275*e1fe3e4aSElliott Hughes        assert tableTag in ("cvar", "gvar")
276*e1fe3e4aSElliott Hughes        pos = offset
277*e1fe3e4aSElliott Hughes        numPointsInData = data[pos]
278*e1fe3e4aSElliott Hughes        pos += 1
279*e1fe3e4aSElliott Hughes        if (numPointsInData & POINTS_ARE_WORDS) != 0:
280*e1fe3e4aSElliott Hughes            numPointsInData = (numPointsInData & POINT_RUN_COUNT_MASK) << 8 | data[pos]
281*e1fe3e4aSElliott Hughes            pos += 1
282*e1fe3e4aSElliott Hughes        if numPointsInData == 0:
283*e1fe3e4aSElliott Hughes            return (range(numPoints), pos)
284*e1fe3e4aSElliott Hughes
285*e1fe3e4aSElliott Hughes        result = []
286*e1fe3e4aSElliott Hughes        while len(result) < numPointsInData:
287*e1fe3e4aSElliott Hughes            runHeader = data[pos]
288*e1fe3e4aSElliott Hughes            pos += 1
289*e1fe3e4aSElliott Hughes            numPointsInRun = (runHeader & POINT_RUN_COUNT_MASK) + 1
290*e1fe3e4aSElliott Hughes            point = 0
291*e1fe3e4aSElliott Hughes            if (runHeader & POINTS_ARE_WORDS) != 0:
292*e1fe3e4aSElliott Hughes                points = array.array("H")
293*e1fe3e4aSElliott Hughes                pointsSize = numPointsInRun * 2
294*e1fe3e4aSElliott Hughes            else:
295*e1fe3e4aSElliott Hughes                points = array.array("B")
296*e1fe3e4aSElliott Hughes                pointsSize = numPointsInRun
297*e1fe3e4aSElliott Hughes            points.frombytes(data[pos : pos + pointsSize])
298*e1fe3e4aSElliott Hughes            if sys.byteorder != "big":
299*e1fe3e4aSElliott Hughes                points.byteswap()
300*e1fe3e4aSElliott Hughes
301*e1fe3e4aSElliott Hughes            assert len(points) == numPointsInRun
302*e1fe3e4aSElliott Hughes            pos += pointsSize
303*e1fe3e4aSElliott Hughes
304*e1fe3e4aSElliott Hughes            result.extend(points)
305*e1fe3e4aSElliott Hughes
306*e1fe3e4aSElliott Hughes        # Convert relative to absolute
307*e1fe3e4aSElliott Hughes        absolute = []
308*e1fe3e4aSElliott Hughes        current = 0
309*e1fe3e4aSElliott Hughes        for delta in result:
310*e1fe3e4aSElliott Hughes            current += delta
311*e1fe3e4aSElliott Hughes            absolute.append(current)
312*e1fe3e4aSElliott Hughes        result = absolute
313*e1fe3e4aSElliott Hughes        del absolute
314*e1fe3e4aSElliott Hughes
315*e1fe3e4aSElliott Hughes        badPoints = {str(p) for p in result if p < 0 or p >= numPoints}
316*e1fe3e4aSElliott Hughes        if badPoints:
317*e1fe3e4aSElliott Hughes            log.warning(
318*e1fe3e4aSElliott Hughes                "point %s out of range in '%s' table"
319*e1fe3e4aSElliott Hughes                % (",".join(sorted(badPoints)), tableTag)
320*e1fe3e4aSElliott Hughes            )
321*e1fe3e4aSElliott Hughes        return (result, pos)
322*e1fe3e4aSElliott Hughes
323*e1fe3e4aSElliott Hughes    def compileDeltas(self):
324*e1fe3e4aSElliott Hughes        deltaX = []
325*e1fe3e4aSElliott Hughes        deltaY = []
326*e1fe3e4aSElliott Hughes        if self.getCoordWidth() == 2:
327*e1fe3e4aSElliott Hughes            for c in self.coordinates:
328*e1fe3e4aSElliott Hughes                if c is None:
329*e1fe3e4aSElliott Hughes                    continue
330*e1fe3e4aSElliott Hughes                deltaX.append(c[0])
331*e1fe3e4aSElliott Hughes                deltaY.append(c[1])
332*e1fe3e4aSElliott Hughes        else:
333*e1fe3e4aSElliott Hughes            for c in self.coordinates:
334*e1fe3e4aSElliott Hughes                if c is None:
335*e1fe3e4aSElliott Hughes                    continue
336*e1fe3e4aSElliott Hughes                deltaX.append(c)
337*e1fe3e4aSElliott Hughes        bytearr = bytearray()
338*e1fe3e4aSElliott Hughes        self.compileDeltaValues_(deltaX, bytearr)
339*e1fe3e4aSElliott Hughes        self.compileDeltaValues_(deltaY, bytearr)
340*e1fe3e4aSElliott Hughes        return bytearr
341*e1fe3e4aSElliott Hughes
342*e1fe3e4aSElliott Hughes    @staticmethod
343*e1fe3e4aSElliott Hughes    def compileDeltaValues_(deltas, bytearr=None):
344*e1fe3e4aSElliott Hughes        """[value1, value2, value3, ...] --> bytearray
345*e1fe3e4aSElliott Hughes
346*e1fe3e4aSElliott Hughes        Emits a sequence of runs. Each run starts with a
347*e1fe3e4aSElliott Hughes        byte-sized header whose 6 least significant bits
348*e1fe3e4aSElliott Hughes        (header & 0x3F) indicate how many values are encoded
349*e1fe3e4aSElliott Hughes        in this run. The stored length is the actual length
350*e1fe3e4aSElliott Hughes        minus one; run lengths are thus in the range [1..64].
351*e1fe3e4aSElliott Hughes        If the header byte has its most significant bit (0x80)
352*e1fe3e4aSElliott Hughes        set, all values in this run are zero, and no data
353*e1fe3e4aSElliott Hughes        follows. Otherwise, the header byte is followed by
354*e1fe3e4aSElliott Hughes        ((header & 0x3F) + 1) signed values.  If (header &
355*e1fe3e4aSElliott Hughes        0x40) is clear, the delta values are stored as signed
356*e1fe3e4aSElliott Hughes        bytes; if (header & 0x40) is set, the delta values are
357*e1fe3e4aSElliott Hughes        signed 16-bit integers.
358*e1fe3e4aSElliott Hughes        """  # Explaining the format because the 'gvar' spec is hard to understand.
359*e1fe3e4aSElliott Hughes        if bytearr is None:
360*e1fe3e4aSElliott Hughes            bytearr = bytearray()
361*e1fe3e4aSElliott Hughes        pos = 0
362*e1fe3e4aSElliott Hughes        numDeltas = len(deltas)
363*e1fe3e4aSElliott Hughes        while pos < numDeltas:
364*e1fe3e4aSElliott Hughes            value = deltas[pos]
365*e1fe3e4aSElliott Hughes            if value == 0:
366*e1fe3e4aSElliott Hughes                pos = TupleVariation.encodeDeltaRunAsZeroes_(deltas, pos, bytearr)
367*e1fe3e4aSElliott Hughes            elif -128 <= value <= 127:
368*e1fe3e4aSElliott Hughes                pos = TupleVariation.encodeDeltaRunAsBytes_(deltas, pos, bytearr)
369*e1fe3e4aSElliott Hughes            else:
370*e1fe3e4aSElliott Hughes                pos = TupleVariation.encodeDeltaRunAsWords_(deltas, pos, bytearr)
371*e1fe3e4aSElliott Hughes        return bytearr
372*e1fe3e4aSElliott Hughes
373*e1fe3e4aSElliott Hughes    @staticmethod
374*e1fe3e4aSElliott Hughes    def encodeDeltaRunAsZeroes_(deltas, offset, bytearr):
375*e1fe3e4aSElliott Hughes        pos = offset
376*e1fe3e4aSElliott Hughes        numDeltas = len(deltas)
377*e1fe3e4aSElliott Hughes        while pos < numDeltas and deltas[pos] == 0:
378*e1fe3e4aSElliott Hughes            pos += 1
379*e1fe3e4aSElliott Hughes        runLength = pos - offset
380*e1fe3e4aSElliott Hughes        while runLength >= 64:
381*e1fe3e4aSElliott Hughes            bytearr.append(DELTAS_ARE_ZERO | 63)
382*e1fe3e4aSElliott Hughes            runLength -= 64
383*e1fe3e4aSElliott Hughes        if runLength:
384*e1fe3e4aSElliott Hughes            bytearr.append(DELTAS_ARE_ZERO | (runLength - 1))
385*e1fe3e4aSElliott Hughes        return pos
386*e1fe3e4aSElliott Hughes
387*e1fe3e4aSElliott Hughes    @staticmethod
388*e1fe3e4aSElliott Hughes    def encodeDeltaRunAsBytes_(deltas, offset, bytearr):
389*e1fe3e4aSElliott Hughes        pos = offset
390*e1fe3e4aSElliott Hughes        numDeltas = len(deltas)
391*e1fe3e4aSElliott Hughes        while pos < numDeltas:
392*e1fe3e4aSElliott Hughes            value = deltas[pos]
393*e1fe3e4aSElliott Hughes            if not (-128 <= value <= 127):
394*e1fe3e4aSElliott Hughes                break
395*e1fe3e4aSElliott Hughes            # Within a byte-encoded run of deltas, a single zero
396*e1fe3e4aSElliott Hughes            # is best stored literally as 0x00 value. However,
397*e1fe3e4aSElliott Hughes            # if are two or more zeroes in a sequence, it is
398*e1fe3e4aSElliott Hughes            # better to start a new run. For example, the sequence
399*e1fe3e4aSElliott Hughes            # of deltas [15, 15, 0, 15, 15] becomes 6 bytes
400*e1fe3e4aSElliott Hughes            # (04 0F 0F 00 0F 0F) when storing the zero value
401*e1fe3e4aSElliott Hughes            # literally, but 7 bytes (01 0F 0F 80 01 0F 0F)
402*e1fe3e4aSElliott Hughes            # when starting a new run.
403*e1fe3e4aSElliott Hughes            if value == 0 and pos + 1 < numDeltas and deltas[pos + 1] == 0:
404*e1fe3e4aSElliott Hughes                break
405*e1fe3e4aSElliott Hughes            pos += 1
406*e1fe3e4aSElliott Hughes        runLength = pos - offset
407*e1fe3e4aSElliott Hughes        while runLength >= 64:
408*e1fe3e4aSElliott Hughes            bytearr.append(63)
409*e1fe3e4aSElliott Hughes            bytearr.extend(array.array("b", deltas[offset : offset + 64]))
410*e1fe3e4aSElliott Hughes            offset += 64
411*e1fe3e4aSElliott Hughes            runLength -= 64
412*e1fe3e4aSElliott Hughes        if runLength:
413*e1fe3e4aSElliott Hughes            bytearr.append(runLength - 1)
414*e1fe3e4aSElliott Hughes            bytearr.extend(array.array("b", deltas[offset:pos]))
415*e1fe3e4aSElliott Hughes        return pos
416*e1fe3e4aSElliott Hughes
417*e1fe3e4aSElliott Hughes    @staticmethod
418*e1fe3e4aSElliott Hughes    def encodeDeltaRunAsWords_(deltas, offset, bytearr):
419*e1fe3e4aSElliott Hughes        pos = offset
420*e1fe3e4aSElliott Hughes        numDeltas = len(deltas)
421*e1fe3e4aSElliott Hughes        while pos < numDeltas:
422*e1fe3e4aSElliott Hughes            value = deltas[pos]
423*e1fe3e4aSElliott Hughes            # Within a word-encoded run of deltas, it is easiest
424*e1fe3e4aSElliott Hughes            # to start a new run (with a different encoding)
425*e1fe3e4aSElliott Hughes            # whenever we encounter a zero value. For example,
426*e1fe3e4aSElliott Hughes            # the sequence [0x6666, 0, 0x7777] needs 7 bytes when
427*e1fe3e4aSElliott Hughes            # storing the zero literally (42 66 66 00 00 77 77),
428*e1fe3e4aSElliott Hughes            # and equally 7 bytes when starting a new run
429*e1fe3e4aSElliott Hughes            # (40 66 66 80 40 77 77).
430*e1fe3e4aSElliott Hughes            if value == 0:
431*e1fe3e4aSElliott Hughes                break
432*e1fe3e4aSElliott Hughes
433*e1fe3e4aSElliott Hughes            # Within a word-encoded run of deltas, a single value
434*e1fe3e4aSElliott Hughes            # in the range (-128..127) should be encoded literally
435*e1fe3e4aSElliott Hughes            # because it is more compact. For example, the sequence
436*e1fe3e4aSElliott Hughes            # [0x6666, 2, 0x7777] becomes 7 bytes when storing
437*e1fe3e4aSElliott Hughes            # the value literally (42 66 66 00 02 77 77), but 8 bytes
438*e1fe3e4aSElliott Hughes            # when starting a new run (40 66 66 00 02 40 77 77).
439*e1fe3e4aSElliott Hughes            if (
440*e1fe3e4aSElliott Hughes                (-128 <= value <= 127)
441*e1fe3e4aSElliott Hughes                and pos + 1 < numDeltas
442*e1fe3e4aSElliott Hughes                and (-128 <= deltas[pos + 1] <= 127)
443*e1fe3e4aSElliott Hughes            ):
444*e1fe3e4aSElliott Hughes                break
445*e1fe3e4aSElliott Hughes            pos += 1
446*e1fe3e4aSElliott Hughes        runLength = pos - offset
447*e1fe3e4aSElliott Hughes        while runLength >= 64:
448*e1fe3e4aSElliott Hughes            bytearr.append(DELTAS_ARE_WORDS | 63)
449*e1fe3e4aSElliott Hughes            a = array.array("h", deltas[offset : offset + 64])
450*e1fe3e4aSElliott Hughes            if sys.byteorder != "big":
451*e1fe3e4aSElliott Hughes                a.byteswap()
452*e1fe3e4aSElliott Hughes            bytearr.extend(a)
453*e1fe3e4aSElliott Hughes            offset += 64
454*e1fe3e4aSElliott Hughes            runLength -= 64
455*e1fe3e4aSElliott Hughes        if runLength:
456*e1fe3e4aSElliott Hughes            bytearr.append(DELTAS_ARE_WORDS | (runLength - 1))
457*e1fe3e4aSElliott Hughes            a = array.array("h", deltas[offset:pos])
458*e1fe3e4aSElliott Hughes            if sys.byteorder != "big":
459*e1fe3e4aSElliott Hughes                a.byteswap()
460*e1fe3e4aSElliott Hughes            bytearr.extend(a)
461*e1fe3e4aSElliott Hughes        return pos
462*e1fe3e4aSElliott Hughes
463*e1fe3e4aSElliott Hughes    @staticmethod
464*e1fe3e4aSElliott Hughes    def decompileDeltas_(numDeltas, data, offset):
465*e1fe3e4aSElliott Hughes        """(numDeltas, data, offset) --> ([delta, delta, ...], newOffset)"""
466*e1fe3e4aSElliott Hughes        result = []
467*e1fe3e4aSElliott Hughes        pos = offset
468*e1fe3e4aSElliott Hughes        while len(result) < numDeltas:
469*e1fe3e4aSElliott Hughes            runHeader = data[pos]
470*e1fe3e4aSElliott Hughes            pos += 1
471*e1fe3e4aSElliott Hughes            numDeltasInRun = (runHeader & DELTA_RUN_COUNT_MASK) + 1
472*e1fe3e4aSElliott Hughes            if (runHeader & DELTAS_ARE_ZERO) != 0:
473*e1fe3e4aSElliott Hughes                result.extend([0] * numDeltasInRun)
474*e1fe3e4aSElliott Hughes            else:
475*e1fe3e4aSElliott Hughes                if (runHeader & DELTAS_ARE_WORDS) != 0:
476*e1fe3e4aSElliott Hughes                    deltas = array.array("h")
477*e1fe3e4aSElliott Hughes                    deltasSize = numDeltasInRun * 2
478*e1fe3e4aSElliott Hughes                else:
479*e1fe3e4aSElliott Hughes                    deltas = array.array("b")
480*e1fe3e4aSElliott Hughes                    deltasSize = numDeltasInRun
481*e1fe3e4aSElliott Hughes                deltas.frombytes(data[pos : pos + deltasSize])
482*e1fe3e4aSElliott Hughes                if sys.byteorder != "big":
483*e1fe3e4aSElliott Hughes                    deltas.byteswap()
484*e1fe3e4aSElliott Hughes                assert len(deltas) == numDeltasInRun
485*e1fe3e4aSElliott Hughes                pos += deltasSize
486*e1fe3e4aSElliott Hughes                result.extend(deltas)
487*e1fe3e4aSElliott Hughes        assert len(result) == numDeltas
488*e1fe3e4aSElliott Hughes        return (result, pos)
489*e1fe3e4aSElliott Hughes
490*e1fe3e4aSElliott Hughes    @staticmethod
491*e1fe3e4aSElliott Hughes    def getTupleSize_(flags, axisCount):
492*e1fe3e4aSElliott Hughes        size = 4
493*e1fe3e4aSElliott Hughes        if (flags & EMBEDDED_PEAK_TUPLE) != 0:
494*e1fe3e4aSElliott Hughes            size += axisCount * 2
495*e1fe3e4aSElliott Hughes        if (flags & INTERMEDIATE_REGION) != 0:
496*e1fe3e4aSElliott Hughes            size += axisCount * 4
497*e1fe3e4aSElliott Hughes        return size
498*e1fe3e4aSElliott Hughes
499*e1fe3e4aSElliott Hughes    def getCoordWidth(self):
500*e1fe3e4aSElliott Hughes        """Return 2 if coordinates are (x, y) as in gvar, 1 if single values
501*e1fe3e4aSElliott Hughes        as in cvar, or 0 if empty.
502*e1fe3e4aSElliott Hughes        """
503*e1fe3e4aSElliott Hughes        firstDelta = next((c for c in self.coordinates if c is not None), None)
504*e1fe3e4aSElliott Hughes        if firstDelta is None:
505*e1fe3e4aSElliott Hughes            return 0  # empty or has no impact
506*e1fe3e4aSElliott Hughes        if type(firstDelta) in (int, float):
507*e1fe3e4aSElliott Hughes            return 1
508*e1fe3e4aSElliott Hughes        if type(firstDelta) is tuple and len(firstDelta) == 2:
509*e1fe3e4aSElliott Hughes            return 2
510*e1fe3e4aSElliott Hughes        raise TypeError(
511*e1fe3e4aSElliott Hughes            "invalid type of delta; expected (int or float) number, or "
512*e1fe3e4aSElliott Hughes            "Tuple[number, number]: %r" % firstDelta
513*e1fe3e4aSElliott Hughes        )
514*e1fe3e4aSElliott Hughes
515*e1fe3e4aSElliott Hughes    def scaleDeltas(self, scalar):
516*e1fe3e4aSElliott Hughes        if scalar == 1.0:
517*e1fe3e4aSElliott Hughes            return  # no change
518*e1fe3e4aSElliott Hughes        coordWidth = self.getCoordWidth()
519*e1fe3e4aSElliott Hughes        self.coordinates = [
520*e1fe3e4aSElliott Hughes            (
521*e1fe3e4aSElliott Hughes                None
522*e1fe3e4aSElliott Hughes                if d is None
523*e1fe3e4aSElliott Hughes                else d * scalar if coordWidth == 1 else (d[0] * scalar, d[1] * scalar)
524*e1fe3e4aSElliott Hughes            )
525*e1fe3e4aSElliott Hughes            for d in self.coordinates
526*e1fe3e4aSElliott Hughes        ]
527*e1fe3e4aSElliott Hughes
528*e1fe3e4aSElliott Hughes    def roundDeltas(self):
529*e1fe3e4aSElliott Hughes        coordWidth = self.getCoordWidth()
530*e1fe3e4aSElliott Hughes        self.coordinates = [
531*e1fe3e4aSElliott Hughes            (
532*e1fe3e4aSElliott Hughes                None
533*e1fe3e4aSElliott Hughes                if d is None
534*e1fe3e4aSElliott Hughes                else otRound(d) if coordWidth == 1 else (otRound(d[0]), otRound(d[1]))
535*e1fe3e4aSElliott Hughes            )
536*e1fe3e4aSElliott Hughes            for d in self.coordinates
537*e1fe3e4aSElliott Hughes        ]
538*e1fe3e4aSElliott Hughes
539*e1fe3e4aSElliott Hughes    def calcInferredDeltas(self, origCoords, endPts):
540*e1fe3e4aSElliott Hughes        from fontTools.varLib.iup import iup_delta
541*e1fe3e4aSElliott Hughes
542*e1fe3e4aSElliott Hughes        if self.getCoordWidth() == 1:
543*e1fe3e4aSElliott Hughes            raise TypeError("Only 'gvar' TupleVariation can have inferred deltas")
544*e1fe3e4aSElliott Hughes        if None in self.coordinates:
545*e1fe3e4aSElliott Hughes            if len(self.coordinates) != len(origCoords):
546*e1fe3e4aSElliott Hughes                raise ValueError(
547*e1fe3e4aSElliott Hughes                    "Expected len(origCoords) == %d; found %d"
548*e1fe3e4aSElliott Hughes                    % (len(self.coordinates), len(origCoords))
549*e1fe3e4aSElliott Hughes                )
550*e1fe3e4aSElliott Hughes            self.coordinates = iup_delta(self.coordinates, origCoords, endPts)
551*e1fe3e4aSElliott Hughes
552*e1fe3e4aSElliott Hughes    def optimize(self, origCoords, endPts, tolerance=0.5, isComposite=False):
553*e1fe3e4aSElliott Hughes        from fontTools.varLib.iup import iup_delta_optimize
554*e1fe3e4aSElliott Hughes
555*e1fe3e4aSElliott Hughes        if None in self.coordinates:
556*e1fe3e4aSElliott Hughes            return  # already optimized
557*e1fe3e4aSElliott Hughes
558*e1fe3e4aSElliott Hughes        deltaOpt = iup_delta_optimize(
559*e1fe3e4aSElliott Hughes            self.coordinates, origCoords, endPts, tolerance=tolerance
560*e1fe3e4aSElliott Hughes        )
561*e1fe3e4aSElliott Hughes        if None in deltaOpt:
562*e1fe3e4aSElliott Hughes            if isComposite and all(d is None for d in deltaOpt):
563*e1fe3e4aSElliott Hughes                # Fix for macOS composites
564*e1fe3e4aSElliott Hughes                # https://github.com/fonttools/fonttools/issues/1381
565*e1fe3e4aSElliott Hughes                deltaOpt = [(0, 0)] + [None] * (len(deltaOpt) - 1)
566*e1fe3e4aSElliott Hughes            # Use "optimized" version only if smaller...
567*e1fe3e4aSElliott Hughes            varOpt = TupleVariation(self.axes, deltaOpt)
568*e1fe3e4aSElliott Hughes
569*e1fe3e4aSElliott Hughes            # Shouldn't matter that this is different from fvar...?
570*e1fe3e4aSElliott Hughes            axisTags = sorted(self.axes.keys())
571*e1fe3e4aSElliott Hughes            tupleData, auxData = self.compile(axisTags)
572*e1fe3e4aSElliott Hughes            unoptimizedLength = len(tupleData) + len(auxData)
573*e1fe3e4aSElliott Hughes            tupleData, auxData = varOpt.compile(axisTags)
574*e1fe3e4aSElliott Hughes            optimizedLength = len(tupleData) + len(auxData)
575*e1fe3e4aSElliott Hughes
576*e1fe3e4aSElliott Hughes            if optimizedLength < unoptimizedLength:
577*e1fe3e4aSElliott Hughes                self.coordinates = varOpt.coordinates
578*e1fe3e4aSElliott Hughes
579*e1fe3e4aSElliott Hughes    def __imul__(self, scalar):
580*e1fe3e4aSElliott Hughes        self.scaleDeltas(scalar)
581*e1fe3e4aSElliott Hughes        return self
582*e1fe3e4aSElliott Hughes
583*e1fe3e4aSElliott Hughes    def __iadd__(self, other):
584*e1fe3e4aSElliott Hughes        if not isinstance(other, TupleVariation):
585*e1fe3e4aSElliott Hughes            return NotImplemented
586*e1fe3e4aSElliott Hughes        deltas1 = self.coordinates
587*e1fe3e4aSElliott Hughes        length = len(deltas1)
588*e1fe3e4aSElliott Hughes        deltas2 = other.coordinates
589*e1fe3e4aSElliott Hughes        if len(deltas2) != length:
590*e1fe3e4aSElliott Hughes            raise ValueError("cannot sum TupleVariation deltas with different lengths")
591*e1fe3e4aSElliott Hughes        # 'None' values have different meanings in gvar vs cvar TupleVariations:
592*e1fe3e4aSElliott Hughes        # within the gvar, when deltas are not provided explicitly for some points,
593*e1fe3e4aSElliott Hughes        # they need to be inferred; whereas for the 'cvar' table, if deltas are not
594*e1fe3e4aSElliott Hughes        # provided for some CVT values, then no adjustments are made (i.e. None == 0).
595*e1fe3e4aSElliott Hughes        # Thus, we cannot sum deltas for gvar TupleVariations if they contain
596*e1fe3e4aSElliott Hughes        # inferred inferred deltas (the latter need to be computed first using
597*e1fe3e4aSElliott Hughes        # 'calcInferredDeltas' method), but we can treat 'None' values in cvar
598*e1fe3e4aSElliott Hughes        # deltas as if they are zeros.
599*e1fe3e4aSElliott Hughes        if self.getCoordWidth() == 2:
600*e1fe3e4aSElliott Hughes            for i, d2 in zip(range(length), deltas2):
601*e1fe3e4aSElliott Hughes                d1 = deltas1[i]
602*e1fe3e4aSElliott Hughes                try:
603*e1fe3e4aSElliott Hughes                    deltas1[i] = (d1[0] + d2[0], d1[1] + d2[1])
604*e1fe3e4aSElliott Hughes                except TypeError:
605*e1fe3e4aSElliott Hughes                    raise ValueError("cannot sum gvar deltas with inferred points")
606*e1fe3e4aSElliott Hughes        else:
607*e1fe3e4aSElliott Hughes            for i, d2 in zip(range(length), deltas2):
608*e1fe3e4aSElliott Hughes                d1 = deltas1[i]
609*e1fe3e4aSElliott Hughes                if d1 is not None and d2 is not None:
610*e1fe3e4aSElliott Hughes                    deltas1[i] = d1 + d2
611*e1fe3e4aSElliott Hughes                elif d1 is None and d2 is not None:
612*e1fe3e4aSElliott Hughes                    deltas1[i] = d2
613*e1fe3e4aSElliott Hughes                # elif d2 is None do nothing
614*e1fe3e4aSElliott Hughes        return self
615*e1fe3e4aSElliott Hughes
616*e1fe3e4aSElliott Hughes
617*e1fe3e4aSElliott Hughesdef decompileSharedTuples(axisTags, sharedTupleCount, data, offset):
618*e1fe3e4aSElliott Hughes    result = []
619*e1fe3e4aSElliott Hughes    for _ in range(sharedTupleCount):
620*e1fe3e4aSElliott Hughes        t, offset = TupleVariation.decompileCoord_(axisTags, data, offset)
621*e1fe3e4aSElliott Hughes        result.append(t)
622*e1fe3e4aSElliott Hughes    return result
623*e1fe3e4aSElliott Hughes
624*e1fe3e4aSElliott Hughes
625*e1fe3e4aSElliott Hughesdef compileSharedTuples(
626*e1fe3e4aSElliott Hughes    axisTags, variations, MAX_NUM_SHARED_COORDS=TUPLE_INDEX_MASK + 1
627*e1fe3e4aSElliott Hughes):
628*e1fe3e4aSElliott Hughes    coordCount = Counter()
629*e1fe3e4aSElliott Hughes    for var in variations:
630*e1fe3e4aSElliott Hughes        coord = var.compileCoord(axisTags)
631*e1fe3e4aSElliott Hughes        coordCount[coord] += 1
632*e1fe3e4aSElliott Hughes    # In python < 3.7, most_common() ordering is non-deterministic
633*e1fe3e4aSElliott Hughes    # so apply a sort to make sure the ordering is consistent.
634*e1fe3e4aSElliott Hughes    sharedCoords = sorted(
635*e1fe3e4aSElliott Hughes        coordCount.most_common(MAX_NUM_SHARED_COORDS),
636*e1fe3e4aSElliott Hughes        key=lambda item: (-item[1], item[0]),
637*e1fe3e4aSElliott Hughes    )
638*e1fe3e4aSElliott Hughes    return [c[0] for c in sharedCoords if c[1] > 1]
639*e1fe3e4aSElliott Hughes
640*e1fe3e4aSElliott Hughes
641*e1fe3e4aSElliott Hughesdef compileTupleVariationStore(
642*e1fe3e4aSElliott Hughes    variations, pointCount, axisTags, sharedTupleIndices, useSharedPoints=True
643*e1fe3e4aSElliott Hughes):
644*e1fe3e4aSElliott Hughes    # pointCount is actually unused. Keeping for API compat.
645*e1fe3e4aSElliott Hughes    del pointCount
646*e1fe3e4aSElliott Hughes    newVariations = []
647*e1fe3e4aSElliott Hughes    pointDatas = []
648*e1fe3e4aSElliott Hughes    # Compile all points and figure out sharing if desired
649*e1fe3e4aSElliott Hughes    sharedPoints = None
650*e1fe3e4aSElliott Hughes
651*e1fe3e4aSElliott Hughes    # Collect, count, and compile point-sets for all variation sets
652*e1fe3e4aSElliott Hughes    pointSetCount = defaultdict(int)
653*e1fe3e4aSElliott Hughes    for v in variations:
654*e1fe3e4aSElliott Hughes        points = v.getUsedPoints()
655*e1fe3e4aSElliott Hughes        if points is None:  # Empty variations
656*e1fe3e4aSElliott Hughes            continue
657*e1fe3e4aSElliott Hughes        pointSetCount[points] += 1
658*e1fe3e4aSElliott Hughes        newVariations.append(v)
659*e1fe3e4aSElliott Hughes        pointDatas.append(points)
660*e1fe3e4aSElliott Hughes    variations = newVariations
661*e1fe3e4aSElliott Hughes    del newVariations
662*e1fe3e4aSElliott Hughes
663*e1fe3e4aSElliott Hughes    if not variations:
664*e1fe3e4aSElliott Hughes        return (0, b"", b"")
665*e1fe3e4aSElliott Hughes
666*e1fe3e4aSElliott Hughes    n = len(variations[0].coordinates)
667*e1fe3e4aSElliott Hughes    assert all(
668*e1fe3e4aSElliott Hughes        len(v.coordinates) == n for v in variations
669*e1fe3e4aSElliott Hughes    ), "Variation sets have different sizes"
670*e1fe3e4aSElliott Hughes
671*e1fe3e4aSElliott Hughes    compiledPoints = {
672*e1fe3e4aSElliott Hughes        pointSet: TupleVariation.compilePoints(pointSet) for pointSet in pointSetCount
673*e1fe3e4aSElliott Hughes    }
674*e1fe3e4aSElliott Hughes
675*e1fe3e4aSElliott Hughes    tupleVariationCount = len(variations)
676*e1fe3e4aSElliott Hughes    tuples = []
677*e1fe3e4aSElliott Hughes    data = []
678*e1fe3e4aSElliott Hughes
679*e1fe3e4aSElliott Hughes    if useSharedPoints:
680*e1fe3e4aSElliott Hughes        # Find point-set which saves most bytes.
681*e1fe3e4aSElliott Hughes        def key(pn):
682*e1fe3e4aSElliott Hughes            pointSet = pn[0]
683*e1fe3e4aSElliott Hughes            count = pn[1]
684*e1fe3e4aSElliott Hughes            return len(compiledPoints[pointSet]) * (count - 1)
685*e1fe3e4aSElliott Hughes
686*e1fe3e4aSElliott Hughes        sharedPoints = max(pointSetCount.items(), key=key)[0]
687*e1fe3e4aSElliott Hughes
688*e1fe3e4aSElliott Hughes        data.append(compiledPoints[sharedPoints])
689*e1fe3e4aSElliott Hughes        tupleVariationCount |= TUPLES_SHARE_POINT_NUMBERS
690*e1fe3e4aSElliott Hughes
691*e1fe3e4aSElliott Hughes    # b'' implies "use shared points"
692*e1fe3e4aSElliott Hughes    pointDatas = [
693*e1fe3e4aSElliott Hughes        compiledPoints[points] if points != sharedPoints else b""
694*e1fe3e4aSElliott Hughes        for points in pointDatas
695*e1fe3e4aSElliott Hughes    ]
696*e1fe3e4aSElliott Hughes
697*e1fe3e4aSElliott Hughes    for v, p in zip(variations, pointDatas):
698*e1fe3e4aSElliott Hughes        thisTuple, thisData = v.compile(axisTags, sharedTupleIndices, pointData=p)
699*e1fe3e4aSElliott Hughes
700*e1fe3e4aSElliott Hughes        tuples.append(thisTuple)
701*e1fe3e4aSElliott Hughes        data.append(thisData)
702*e1fe3e4aSElliott Hughes
703*e1fe3e4aSElliott Hughes    tuples = b"".join(tuples)
704*e1fe3e4aSElliott Hughes    data = b"".join(data)
705*e1fe3e4aSElliott Hughes    return tupleVariationCount, tuples, data
706*e1fe3e4aSElliott Hughes
707*e1fe3e4aSElliott Hughes
708*e1fe3e4aSElliott Hughesdef decompileTupleVariationStore(
709*e1fe3e4aSElliott Hughes    tableTag,
710*e1fe3e4aSElliott Hughes    axisTags,
711*e1fe3e4aSElliott Hughes    tupleVariationCount,
712*e1fe3e4aSElliott Hughes    pointCount,
713*e1fe3e4aSElliott Hughes    sharedTuples,
714*e1fe3e4aSElliott Hughes    data,
715*e1fe3e4aSElliott Hughes    pos,
716*e1fe3e4aSElliott Hughes    dataPos,
717*e1fe3e4aSElliott Hughes):
718*e1fe3e4aSElliott Hughes    numAxes = len(axisTags)
719*e1fe3e4aSElliott Hughes    result = []
720*e1fe3e4aSElliott Hughes    if (tupleVariationCount & TUPLES_SHARE_POINT_NUMBERS) != 0:
721*e1fe3e4aSElliott Hughes        sharedPoints, dataPos = TupleVariation.decompilePoints_(
722*e1fe3e4aSElliott Hughes            pointCount, data, dataPos, tableTag
723*e1fe3e4aSElliott Hughes        )
724*e1fe3e4aSElliott Hughes    else:
725*e1fe3e4aSElliott Hughes        sharedPoints = []
726*e1fe3e4aSElliott Hughes    for _ in range(tupleVariationCount & TUPLE_COUNT_MASK):
727*e1fe3e4aSElliott Hughes        dataSize, flags = struct.unpack(">HH", data[pos : pos + 4])
728*e1fe3e4aSElliott Hughes        tupleSize = TupleVariation.getTupleSize_(flags, numAxes)
729*e1fe3e4aSElliott Hughes        tupleData = data[pos : pos + tupleSize]
730*e1fe3e4aSElliott Hughes        pointDeltaData = data[dataPos : dataPos + dataSize]
731*e1fe3e4aSElliott Hughes        result.append(
732*e1fe3e4aSElliott Hughes            decompileTupleVariation_(
733*e1fe3e4aSElliott Hughes                pointCount,
734*e1fe3e4aSElliott Hughes                sharedTuples,
735*e1fe3e4aSElliott Hughes                sharedPoints,
736*e1fe3e4aSElliott Hughes                tableTag,
737*e1fe3e4aSElliott Hughes                axisTags,
738*e1fe3e4aSElliott Hughes                tupleData,
739*e1fe3e4aSElliott Hughes                pointDeltaData,
740*e1fe3e4aSElliott Hughes            )
741*e1fe3e4aSElliott Hughes        )
742*e1fe3e4aSElliott Hughes        pos += tupleSize
743*e1fe3e4aSElliott Hughes        dataPos += dataSize
744*e1fe3e4aSElliott Hughes    return result
745*e1fe3e4aSElliott Hughes
746*e1fe3e4aSElliott Hughes
747*e1fe3e4aSElliott Hughesdef decompileTupleVariation_(
748*e1fe3e4aSElliott Hughes    pointCount, sharedTuples, sharedPoints, tableTag, axisTags, data, tupleData
749*e1fe3e4aSElliott Hughes):
750*e1fe3e4aSElliott Hughes    assert tableTag in ("cvar", "gvar"), tableTag
751*e1fe3e4aSElliott Hughes    flags = struct.unpack(">H", data[2:4])[0]
752*e1fe3e4aSElliott Hughes    pos = 4
753*e1fe3e4aSElliott Hughes    if (flags & EMBEDDED_PEAK_TUPLE) == 0:
754*e1fe3e4aSElliott Hughes        peak = sharedTuples[flags & TUPLE_INDEX_MASK]
755*e1fe3e4aSElliott Hughes    else:
756*e1fe3e4aSElliott Hughes        peak, pos = TupleVariation.decompileCoord_(axisTags, data, pos)
757*e1fe3e4aSElliott Hughes    if (flags & INTERMEDIATE_REGION) != 0:
758*e1fe3e4aSElliott Hughes        start, pos = TupleVariation.decompileCoord_(axisTags, data, pos)
759*e1fe3e4aSElliott Hughes        end, pos = TupleVariation.decompileCoord_(axisTags, data, pos)
760*e1fe3e4aSElliott Hughes    else:
761*e1fe3e4aSElliott Hughes        start, end = inferRegion_(peak)
762*e1fe3e4aSElliott Hughes    axes = {}
763*e1fe3e4aSElliott Hughes    for axis in axisTags:
764*e1fe3e4aSElliott Hughes        region = start[axis], peak[axis], end[axis]
765*e1fe3e4aSElliott Hughes        if region != (0.0, 0.0, 0.0):
766*e1fe3e4aSElliott Hughes            axes[axis] = region
767*e1fe3e4aSElliott Hughes    pos = 0
768*e1fe3e4aSElliott Hughes    if (flags & PRIVATE_POINT_NUMBERS) != 0:
769*e1fe3e4aSElliott Hughes        points, pos = TupleVariation.decompilePoints_(
770*e1fe3e4aSElliott Hughes            pointCount, tupleData, pos, tableTag
771*e1fe3e4aSElliott Hughes        )
772*e1fe3e4aSElliott Hughes    else:
773*e1fe3e4aSElliott Hughes        points = sharedPoints
774*e1fe3e4aSElliott Hughes
775*e1fe3e4aSElliott Hughes    deltas = [None] * pointCount
776*e1fe3e4aSElliott Hughes
777*e1fe3e4aSElliott Hughes    if tableTag == "cvar":
778*e1fe3e4aSElliott Hughes        deltas_cvt, pos = TupleVariation.decompileDeltas_(len(points), tupleData, pos)
779*e1fe3e4aSElliott Hughes        for p, delta in zip(points, deltas_cvt):
780*e1fe3e4aSElliott Hughes            if 0 <= p < pointCount:
781*e1fe3e4aSElliott Hughes                deltas[p] = delta
782*e1fe3e4aSElliott Hughes
783*e1fe3e4aSElliott Hughes    elif tableTag == "gvar":
784*e1fe3e4aSElliott Hughes        deltas_x, pos = TupleVariation.decompileDeltas_(len(points), tupleData, pos)
785*e1fe3e4aSElliott Hughes        deltas_y, pos = TupleVariation.decompileDeltas_(len(points), tupleData, pos)
786*e1fe3e4aSElliott Hughes        for p, x, y in zip(points, deltas_x, deltas_y):
787*e1fe3e4aSElliott Hughes            if 0 <= p < pointCount:
788*e1fe3e4aSElliott Hughes                deltas[p] = (x, y)
789*e1fe3e4aSElliott Hughes
790*e1fe3e4aSElliott Hughes    return TupleVariation(axes, deltas)
791*e1fe3e4aSElliott Hughes
792*e1fe3e4aSElliott Hughes
793*e1fe3e4aSElliott Hughesdef inferRegion_(peak):
794*e1fe3e4aSElliott Hughes    """Infer start and end for a (non-intermediate) region
795*e1fe3e4aSElliott Hughes
796*e1fe3e4aSElliott Hughes    This helper function computes the applicability region for
797*e1fe3e4aSElliott Hughes    variation tuples whose INTERMEDIATE_REGION flag is not set in the
798*e1fe3e4aSElliott Hughes    TupleVariationHeader structure.  Variation tuples apply only to
799*e1fe3e4aSElliott Hughes    certain regions of the variation space; outside that region, the
800*e1fe3e4aSElliott Hughes    tuple has no effect.  To make the binary encoding more compact,
801*e1fe3e4aSElliott Hughes    TupleVariationHeaders can omit the intermediateStartTuple and
802*e1fe3e4aSElliott Hughes    intermediateEndTuple fields.
803*e1fe3e4aSElliott Hughes    """
804*e1fe3e4aSElliott Hughes    start, end = {}, {}
805*e1fe3e4aSElliott Hughes    for axis, value in peak.items():
806*e1fe3e4aSElliott Hughes        start[axis] = min(value, 0.0)  # -0.3 --> -0.3; 0.7 --> 0.0
807*e1fe3e4aSElliott Hughes        end[axis] = max(value, 0.0)  # -0.3 -->  0.0; 0.7 --> 0.7
808*e1fe3e4aSElliott Hughes    return (start, end)
809