xref: /aosp_15_r20/external/fonttools/Tests/ttLib/tables/TupleVariation_test.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1from fontTools.misc.loggingTools import CapturingLogHandler
2from fontTools.misc.testTools import parseXML
3from fontTools.misc.textTools import deHexStr, hexStr
4from fontTools.misc.xmlWriter import XMLWriter
5from fontTools.ttLib.tables.TupleVariation import (
6    log,
7    TupleVariation,
8    compileSharedTuples,
9    decompileSharedTuples,
10    compileTupleVariationStore,
11    decompileTupleVariationStore,
12    inferRegion_,
13)
14from io import BytesIO
15import random
16import unittest
17
18
19def hexencode(s):
20    h = hexStr(s).upper()
21    return " ".join([h[i : i + 2] for i in range(0, len(h), 2)])
22
23
24AXES = {
25    "wdth": (0.25, 0.375, 0.5),
26    "wght": (0.0, 1.0, 1.0),
27    "opsz": (-0.75, -0.75, 0.0),
28}
29
30
31# Shared tuples in the 'gvar' table of the Skia font, as printed
32# in Apple's TrueType specification.
33# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6gvar.html
34SKIA_GVAR_SHARED_TUPLES_DATA = deHexStr(
35    "40 00 00 00 C0 00 00 00 00 00 40 00 00 00 C0 00 "
36    "C0 00 C0 00 40 00 C0 00 40 00 40 00 C0 00 40 00"
37)
38
39SKIA_GVAR_SHARED_TUPLES = [
40    {"wght": 1.0, "wdth": 0.0},
41    {"wght": -1.0, "wdth": 0.0},
42    {"wght": 0.0, "wdth": 1.0},
43    {"wght": 0.0, "wdth": -1.0},
44    {"wght": -1.0, "wdth": -1.0},
45    {"wght": 1.0, "wdth": -1.0},
46    {"wght": 1.0, "wdth": 1.0},
47    {"wght": -1.0, "wdth": 1.0},
48]
49
50
51# Tuple Variation Store of uppercase I in the Skia font, as printed in Apple's
52# TrueType spec. The actual Skia font uses a different table for uppercase I
53# than what is printed in Apple's spec, but we still want to make sure that
54# we can parse the data as it appears in the specification.
55# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6gvar.html
56SKIA_GVAR_I_DATA = deHexStr(
57    "00 08 00 24 00 33 20 00 00 15 20 01 00 1B 20 02 "
58    "00 24 20 03 00 15 20 04 00 26 20 07 00 0D 20 06 "
59    "00 1A 20 05 00 40 01 01 01 81 80 43 FF 7E FF 7E "
60    "FF 7E FF 7E 00 81 45 01 01 01 03 01 04 01 04 01 "
61    "04 01 02 80 40 00 82 81 81 04 3A 5A 3E 43 20 81 "
62    "04 0E 40 15 45 7C 83 00 0D 9E F3 F2 F0 F0 F0 F0 "
63    "F3 9E A0 A1 A1 A1 9F 80 00 91 81 91 00 0D 0A 0A "
64    "09 0A 0A 0A 0A 0A 0A 0A 0A 0A 0A 0B 80 00 15 81 "
65    "81 00 C4 89 00 C4 83 00 0D 80 99 98 96 96 96 96 "
66    "99 80 82 83 83 83 81 80 40 FF 18 81 81 04 E6 F9 "
67    "10 21 02 81 04 E8 E5 EB 4D DA 83 00 0D CE D3 D4 "
68    "D3 D3 D3 D5 D2 CE CC CD CD CD CD 80 00 A1 81 91 "
69    "00 0D 07 03 04 02 02 02 03 03 07 07 08 08 08 07 "
70    "80 00 09 81 81 00 28 40 00 A4 02 24 24 66 81 04 "
71    "08 FA FA FA 28 83 00 82 02 FF FF FF 83 02 01 01 "
72    "01 84 91 00 80 06 07 08 08 08 08 0A 07 80 03 FE "
73    "FF FF FF 81 00 08 81 82 02 EE EE EE 8B 6D 00"
74)
75
76
77class TupleVariationTest(unittest.TestCase):
78    def __init__(self, methodName):
79        unittest.TestCase.__init__(self, methodName)
80        # Python 3 renamed assertRaisesRegexp to assertRaisesRegex,
81        # and fires deprecation warnings if a program uses the old name.
82        if not hasattr(self, "assertRaisesRegex"):
83            self.assertRaisesRegex = self.assertRaisesRegexp
84
85    def test_equal(self):
86        var1 = TupleVariation({"wght": (0.0, 1.0, 1.0)}, [(0, 0), (9, 8), (7, 6)])
87        var2 = TupleVariation({"wght": (0.0, 1.0, 1.0)}, [(0, 0), (9, 8), (7, 6)])
88        self.assertEqual(var1, var2)
89
90    def test_equal_differentAxes(self):
91        var1 = TupleVariation({"wght": (0.0, 1.0, 1.0)}, [(0, 0), (9, 8), (7, 6)])
92        var2 = TupleVariation({"wght": (0.7, 0.8, 0.9)}, [(0, 0), (9, 8), (7, 6)])
93        self.assertNotEqual(var1, var2)
94
95    def test_equal_differentCoordinates(self):
96        var1 = TupleVariation({"wght": (0.0, 1.0, 1.0)}, [(0, 0), (9, 8), (7, 6)])
97        var2 = TupleVariation({"wght": (0.0, 1.0, 1.0)}, [(0, 0), (9, 8)])
98        self.assertNotEqual(var1, var2)
99
100    def test_hasImpact_someDeltasNotZero(self):
101        axes = {"wght": (0.0, 1.0, 1.0)}
102        var = TupleVariation(axes, [(0, 0), (9, 8), (7, 6)])
103        self.assertTrue(var.hasImpact())
104
105    def test_hasImpact_allDeltasZero(self):
106        axes = {"wght": (0.0, 1.0, 1.0)}
107        var = TupleVariation(axes, [(0, 0), (0, 0), (0, 0)])
108        self.assertTrue(var.hasImpact())
109
110    def test_hasImpact_allDeltasNone(self):
111        axes = {"wght": (0.0, 1.0, 1.0)}
112        var = TupleVariation(axes, [None, None, None])
113        self.assertFalse(var.hasImpact())
114
115    def test_toXML_badDeltaFormat(self):
116        writer = XMLWriter(BytesIO())
117        g = TupleVariation(AXES, ["String"])
118        with CapturingLogHandler(log, "ERROR") as captor:
119            g.toXML(writer, ["wdth"])
120        self.assertIn("bad delta format", [r.msg for r in captor.records])
121        self.assertEqual(
122            [
123                "<tuple>",
124                '<coord axis="wdth" min="0.25" value="0.375" max="0.5"/>',
125                "<!-- bad delta #0 -->",
126                "</tuple>",
127            ],
128            TupleVariationTest.xml_lines(writer),
129        )
130
131    def test_toXML_constants(self):
132        writer = XMLWriter(BytesIO())
133        g = TupleVariation(AXES, [42, None, 23, 0, -17, None])
134        g.toXML(writer, ["wdth", "wght", "opsz"])
135        self.assertEqual(
136            [
137                "<tuple>",
138                '<coord axis="wdth" min="0.25" value="0.375" max="0.5"/>',
139                '<coord axis="wght" value="1.0"/>',
140                '<coord axis="opsz" value="-0.75"/>',
141                '<delta cvt="0" value="42"/>',
142                '<delta cvt="2" value="23"/>',
143                '<delta cvt="3" value="0"/>',
144                '<delta cvt="4" value="-17"/>',
145                "</tuple>",
146            ],
147            TupleVariationTest.xml_lines(writer),
148        )
149
150    def test_toXML_points(self):
151        writer = XMLWriter(BytesIO())
152        g = TupleVariation(AXES, [(9, 8), None, (7, 6), (0, 0), (-1, -2), None])
153        g.toXML(writer, ["wdth", "wght", "opsz"])
154        self.assertEqual(
155            [
156                "<tuple>",
157                '<coord axis="wdth" min="0.25" value="0.375" max="0.5"/>',
158                '<coord axis="wght" value="1.0"/>',
159                '<coord axis="opsz" value="-0.75"/>',
160                '<delta pt="0" x="9" y="8"/>',
161                '<delta pt="2" x="7" y="6"/>',
162                '<delta pt="3" x="0" y="0"/>',
163                '<delta pt="4" x="-1" y="-2"/>',
164                "</tuple>",
165            ],
166            TupleVariationTest.xml_lines(writer),
167        )
168
169    def test_toXML_allDeltasNone(self):
170        writer = XMLWriter(BytesIO())
171        axes = {"wght": (0.0, 1.0, 1.0)}
172        g = TupleVariation(axes, [None] * 5)
173        g.toXML(writer, ["wght", "wdth"])
174        self.assertEqual(
175            [
176                "<tuple>",
177                '<coord axis="wght" value="1.0"/>',
178                "<!-- no deltas -->",
179                "</tuple>",
180            ],
181            TupleVariationTest.xml_lines(writer),
182        )
183
184    def test_toXML_axes_floats(self):
185        writer = XMLWriter(BytesIO())
186        axes = {
187            "wght": (0.0, 0.2999878, 0.7000122),
188            "wdth": (0.0, 0.4000244, 0.4000244),
189        }
190        g = TupleVariation(axes, [None] * 5)
191        g.toXML(writer, ["wght", "wdth"])
192        self.assertEqual(
193            [
194                '<coord axis="wght" min="0.0" value="0.3" max="0.7"/>',
195                '<coord axis="wdth" value="0.4"/>',
196            ],
197            TupleVariationTest.xml_lines(writer)[1:3],
198        )
199
200    def test_fromXML_badDeltaFormat(self):
201        g = TupleVariation({}, [])
202        with CapturingLogHandler(log, "WARNING") as captor:
203            for name, attrs, content in parseXML('<delta a="1" b="2"/>'):
204                g.fromXML(name, attrs, content)
205        self.assertIn("bad delta format: a, b", [r.msg for r in captor.records])
206
207    def test_fromXML_constants(self):
208        g = TupleVariation({}, [None] * 4)
209        for name, attrs, content in parseXML(
210            '<coord axis="wdth" min="0.25" value="0.375" max="0.5"/>'
211            '<coord axis="wght" value="1.0"/>'
212            '<coord axis="opsz" value="-0.75"/>'
213            '<delta cvt="1" value="42"/>'
214            '<delta cvt="2" value="-23"/>'
215        ):
216            g.fromXML(name, attrs, content)
217        self.assertEqual(AXES, g.axes)
218        self.assertEqual([None, 42, -23, None], g.coordinates)
219
220    def test_fromXML_points(self):
221        g = TupleVariation({}, [None] * 4)
222        for name, attrs, content in parseXML(
223            '<coord axis="wdth" min="0.25" value="0.375" max="0.5"/>'
224            '<coord axis="wght" value="1.0"/>'
225            '<coord axis="opsz" value="-0.75"/>'
226            '<delta pt="1" x="33" y="44"/>'
227            '<delta pt="2" x="-2" y="170"/>'
228        ):
229            g.fromXML(name, attrs, content)
230        self.assertEqual(AXES, g.axes)
231        self.assertEqual([None, (33, 44), (-2, 170), None], g.coordinates)
232
233    def test_fromXML_axes_floats(self):
234        g = TupleVariation({}, [None] * 4)
235        for name, attrs, content in parseXML(
236            '<coord axis="wght" min="0.0" value="0.3" max="0.7"/>'
237            '<coord axis="wdth" value="0.4"/>'
238        ):
239            g.fromXML(name, attrs, content)
240
241        self.assertEqual(g.axes["wght"][0], 0)
242        self.assertAlmostEqual(g.axes["wght"][1], 0.2999878)
243        self.assertAlmostEqual(g.axes["wght"][2], 0.7000122)
244
245        self.assertEqual(g.axes["wdth"][0], 0)
246        self.assertAlmostEqual(g.axes["wdth"][1], 0.4000244)
247        self.assertAlmostEqual(g.axes["wdth"][2], 0.4000244)
248
249    def test_compile_sharedPeaks_nonIntermediate_sharedPoints(self):
250        var = TupleVariation(
251            {"wght": (0.0, 0.5, 0.5), "wdth": (0.0, 0.8, 0.8)}, [(7, 4), (8, 5), (9, 6)]
252        )
253        axisTags = ["wght", "wdth"]
254        sharedPeakIndices = {var.compileCoord(axisTags): 0x77}
255        tup, deltas = var.compile(axisTags, sharedPeakIndices, pointData=b"")
256        # len(deltas)=8; flags=None; tupleIndex=0x77
257        # embeddedPeaks=[]; intermediateCoord=[]
258        self.assertEqual("00 08 00 77", hexencode(tup))
259        self.assertEqual(
260            "02 07 08 09 " "02 04 05 06",  # deltaX: [7, 8, 9]  # deltaY: [4, 5, 6]
261            hexencode(deltas),
262        )
263
264    def test_compile_sharedPeaks_intermediate_sharedPoints(self):
265        var = TupleVariation(
266            {"wght": (0.3, 0.5, 0.7), "wdth": (0.1, 0.8, 0.9)}, [(7, 4), (8, 5), (9, 6)]
267        )
268        axisTags = ["wght", "wdth"]
269        sharedPeakIndices = {var.compileCoord(axisTags): 0x77}
270        tup, deltas = var.compile(axisTags, sharedPeakIndices, pointData=b"")
271        # len(deltas)=8; flags=INTERMEDIATE_REGION; tupleIndex=0x77
272        # embeddedPeak=[]; intermediateCoord=[(0.3, 0.1), (0.7, 0.9)]
273        self.assertEqual("00 08 40 77 13 33 06 66 2C CD 39 9A", hexencode(tup))
274        self.assertEqual(
275            "02 07 08 09 " "02 04 05 06",  # deltaX: [7, 8, 9]  # deltaY: [4, 5, 6]
276            hexencode(deltas),
277        )
278
279    def test_compile_sharedPeaks_nonIntermediate_privatePoints(self):
280        var = TupleVariation(
281            {"wght": (0.0, 0.5, 0.5), "wdth": (0.0, 0.8, 0.8)}, [(7, 4), (8, 5), (9, 6)]
282        )
283        axisTags = ["wght", "wdth"]
284        sharedPeakIndices = {var.compileCoord(axisTags): 0x77}
285        tup, deltas = var.compile(axisTags, sharedPeakIndices)
286        # len(deltas)=9; flags=PRIVATE_POINT_NUMBERS; tupleIndex=0x77
287        # embeddedPeak=[]; intermediateCoord=[]
288        self.assertEqual("00 09 20 77", hexencode(tup))
289        self.assertEqual(
290            "00 "  # all points in glyph
291            "02 07 08 09 "  # deltaX: [7, 8, 9]
292            "02 04 05 06",  # deltaY: [4, 5, 6]
293            hexencode(deltas),
294        )
295
296    def test_compile_sharedPeaks_intermediate_privatePoints(self):
297        var = TupleVariation(
298            {"wght": (0.0, 0.5, 1.0), "wdth": (0.0, 0.8, 1.0)}, [(7, 4), (8, 5), (9, 6)]
299        )
300        axisTags = ["wght", "wdth"]
301        sharedPeakIndices = {var.compileCoord(axisTags): 0x77}
302        tuple, deltas = var.compile(axisTags, sharedPeakIndices)
303        # len(deltas)=9; flags=PRIVATE_POINT_NUMBERS; tupleIndex=0x77
304        # embeddedPeak=[]; intermediateCoord=[(0.0, 0.0), (1.0, 1.0)]
305        self.assertEqual("00 09 60 77 00 00 00 00 40 00 40 00", hexencode(tuple))
306        self.assertEqual(
307            "00 "  # all points in glyph
308            "02 07 08 09 "  # deltaX: [7, 8, 9]
309            "02 04 05 06",  # deltaY: [4, 5, 6]
310            hexencode(deltas),
311        )
312
313    def test_compile_embeddedPeak_nonIntermediate_sharedPoints(self):
314        var = TupleVariation(
315            {"wght": (0.0, 0.5, 0.5), "wdth": (0.0, 0.8, 0.8)}, [(7, 4), (8, 5), (9, 6)]
316        )
317        tup, deltas = var.compile(axisTags=["wght", "wdth"], pointData=b"")
318        # len(deltas)=8; flags=EMBEDDED_PEAK_TUPLE
319        # embeddedPeak=[(0.5, 0.8)]; intermediateCoord=[]
320        self.assertEqual("00 08 80 00 20 00 33 33", hexencode(tup))
321        self.assertEqual(
322            "02 07 08 09 " "02 04 05 06",  # deltaX: [7, 8, 9]  # deltaY: [4, 5, 6]
323            hexencode(deltas),
324        )
325
326    def test_compile_embeddedPeak_nonIntermediate_sharedConstants(self):
327        var = TupleVariation(
328            {"wght": (0.0, 0.5, 0.5), "wdth": (0.0, 0.8, 0.8)}, [3, 1, 4]
329        )
330        tup, deltas = var.compile(axisTags=["wght", "wdth"], pointData=b"")
331        # len(deltas)=4; flags=EMBEDDED_PEAK_TUPLE
332        # embeddedPeak=[(0.5, 0.8)]; intermediateCoord=[]
333        self.assertEqual("00 04 80 00 20 00 33 33", hexencode(tup))
334        self.assertEqual("02 03 01 04", hexencode(deltas))  # delta: [3, 1, 4]
335
336    def test_compile_embeddedPeak_intermediate_sharedPoints(self):
337        var = TupleVariation(
338            {"wght": (0.0, 0.5, 1.0), "wdth": (0.0, 0.8, 0.8)}, [(7, 4), (8, 5), (9, 6)]
339        )
340        tup, deltas = var.compile(axisTags=["wght", "wdth"], pointData=b"")
341        # len(deltas)=8; flags=EMBEDDED_PEAK_TUPLE
342        # embeddedPeak=[(0.5, 0.8)]; intermediateCoord=[(0.0, 0.0), (1.0, 0.8)]
343        self.assertEqual(
344            "00 08 C0 00 20 00 33 33 00 00 00 00 40 00 33 33", hexencode(tup)
345        )
346        self.assertEqual(
347            "02 07 08 09 " "02 04 05 06",  # deltaX: [7, 8, 9]  # deltaY: [4, 5, 6]
348            hexencode(deltas),
349        )
350
351    def test_compile_embeddedPeak_nonIntermediate_privatePoints(self):
352        var = TupleVariation(
353            {"wght": (0.0, 0.5, 0.5), "wdth": (0.0, 0.8, 0.8)}, [(7, 4), (8, 5), (9, 6)]
354        )
355        tup, deltas = var.compile(axisTags=["wght", "wdth"])
356        # len(deltas)=9; flags=PRIVATE_POINT_NUMBERS|EMBEDDED_PEAK_TUPLE
357        # embeddedPeak=[(0.5, 0.8)]; intermediateCoord=[]
358        self.assertEqual("00 09 A0 00 20 00 33 33", hexencode(tup))
359        self.assertEqual(
360            "00 "  # all points in glyph
361            "02 07 08 09 "  # deltaX: [7, 8, 9]
362            "02 04 05 06",  # deltaY: [4, 5, 6]
363            hexencode(deltas),
364        )
365
366    def test_compile_embeddedPeak_nonIntermediate_privateConstants(self):
367        var = TupleVariation(
368            {"wght": (0.0, 0.5, 0.5), "wdth": (0.0, 0.8, 0.8)}, [7, 8, 9]
369        )
370        tup, deltas = var.compile(axisTags=["wght", "wdth"])
371        # len(deltas)=5; flags=PRIVATE_POINT_NUMBERS|EMBEDDED_PEAK_TUPLE
372        # embeddedPeak=[(0.5, 0.8)]; intermediateCoord=[]
373        self.assertEqual("00 05 A0 00 20 00 33 33", hexencode(tup))
374        self.assertEqual(
375            "00 " "02 07 08 09",  # all points in glyph  # delta: [7, 8, 9]
376            hexencode(deltas),
377        )
378
379    def test_compile_embeddedPeak_intermediate_privatePoints(self):
380        var = TupleVariation(
381            {"wght": (0.4, 0.5, 0.6), "wdth": (0.7, 0.8, 0.9)}, [(7, 4), (8, 5), (9, 6)]
382        )
383        tup, deltas = var.compile(axisTags=["wght", "wdth"])
384        # len(deltas)=9;
385        # flags=PRIVATE_POINT_NUMBERS|INTERMEDIATE_REGION|EMBEDDED_PEAK_TUPLE
386        # embeddedPeak=(0.5, 0.8); intermediateCoord=[(0.4, 0.7), (0.6, 0.9)]
387        self.assertEqual(
388            "00 09 E0 00 20 00 33 33 19 9A 2C CD 26 66 39 9A", hexencode(tup)
389        )
390        self.assertEqual(
391            "00 "  # all points in glyph
392            "02 07 08 09 "  # deltaX: [7, 8, 9]
393            "02 04 05 06",  # deltaY: [4, 5, 6]
394            hexencode(deltas),
395        )
396
397    def test_compile_embeddedPeak_intermediate_privateConstants(self):
398        var = TupleVariation(
399            {"wght": (0.4, 0.5, 0.6), "wdth": (0.7, 0.8, 0.9)}, [7, 8, 9]
400        )
401        tup, deltas = var.compile(axisTags=["wght", "wdth"])
402        # len(deltas)=5;
403        # flags=PRIVATE_POINT_NUMBERS|INTERMEDIATE_REGION|EMBEDDED_PEAK_TUPLE
404        # embeddedPeak=(0.5, 0.8); intermediateCoord=[(0.4, 0.7), (0.6, 0.9)]
405        self.assertEqual(
406            "00 05 E0 00 20 00 33 33 19 9A 2C CD 26 66 39 9A", hexencode(tup)
407        )
408        self.assertEqual(
409            "00 " "02 07 08 09",  # all points in glyph  # delta: [7, 8, 9]
410            hexencode(deltas),
411        )
412
413    def test_compileCoord(self):
414        var = TupleVariation(
415            {"wght": (-1.0, -1.0, -1.0), "wdth": (0.4, 0.5, 0.6)}, [None] * 4
416        )
417        self.assertEqual("C0 00 20 00", hexencode(var.compileCoord(["wght", "wdth"])))
418        self.assertEqual("20 00 C0 00", hexencode(var.compileCoord(["wdth", "wght"])))
419        self.assertEqual("C0 00", hexencode(var.compileCoord(["wght"])))
420
421    def test_compileIntermediateCoord(self):
422        var = TupleVariation(
423            {"wght": (-1.0, -1.0, 0.0), "wdth": (0.4, 0.5, 0.6)}, [None] * 4
424        )
425        self.assertEqual(
426            "C0 00 19 9A 00 00 26 66",
427            hexencode(var.compileIntermediateCoord(["wght", "wdth"])),
428        )
429        self.assertEqual(
430            "19 9A C0 00 26 66 00 00",
431            hexencode(var.compileIntermediateCoord(["wdth", "wght"])),
432        )
433        self.assertEqual(None, var.compileIntermediateCoord(["wght"]))
434        self.assertEqual(
435            "19 9A 26 66", hexencode(var.compileIntermediateCoord(["wdth"]))
436        )
437
438    def test_decompileCoord(self):
439        decompileCoord = TupleVariation.decompileCoord_
440        data = deHexStr("DE AD C0 00 20 00 DE AD")
441        self.assertEqual(
442            ({"wght": -1.0, "wdth": 0.5}, 6), decompileCoord(["wght", "wdth"], data, 2)
443        )
444
445    def test_decompileCoord_roundTrip(self):
446        # Make sure we are not affected by https://github.com/fonttools/fonttools/issues/286
447        data = deHexStr("7F B9 80 35")
448        values, _ = TupleVariation.decompileCoord_(["wght", "wdth"], data, 0)
449        axisValues = {axis: (val, val, val) for axis, val in values.items()}
450        var = TupleVariation(axisValues, [None] * 4)
451        self.assertEqual("7F B9 80 35", hexencode(var.compileCoord(["wght", "wdth"])))
452
453    def test_compilePoints(self):
454        compilePoints = lambda p: TupleVariation.compilePoints(set(p))
455        self.assertEqual("00", hexencode(compilePoints(set())))  # all points in glyph
456        self.assertEqual("01 00 07", hexencode(compilePoints([7])))
457        self.assertEqual("01 80 FF FF", hexencode(compilePoints([65535])))
458        self.assertEqual("02 01 09 06", hexencode(compilePoints([9, 15])))
459        self.assertEqual(
460            "06 05 07 01 F7 02 01 F2",
461            hexencode(compilePoints([7, 8, 255, 257, 258, 500])),
462        )
463        self.assertEqual("03 01 07 01 80 01 EC", hexencode(compilePoints([7, 8, 500])))
464        self.assertEqual(
465            "04 01 07 01 81 BE E7 0C 0F",
466            hexencode(compilePoints([7, 8, 0xBEEF, 0xCAFE])),
467        )
468        self.maxDiff = None
469        self.assertEqual(
470            "81 2C"
471            + " 7F 00"  # 300 points (0x12c) in total
472            + (127 * " 01")
473            + " 7F"  # first run, contains 128 points: [0 .. 127]
474            + (128 * " 01")
475            + " 2B"  # second run, contains 128 points: [128 .. 255]
476            + (44 * " 01"),  # third run, contains 44 points: [256 .. 299]
477            hexencode(compilePoints(range(300))),
478        )
479        self.assertEqual(
480            "81 8F"
481            + " 7F 00"  # 399 points (0x18f) in total
482            + (127 * " 01")
483            + " 7F"  # first run, contains 128 points: [0 .. 127]
484            + (128 * " 01")
485            + " 7F"  # second run, contains 128 points: [128 .. 255]
486            + (128 * " 01")
487            + " 0E"  # third run, contains 128 points: [256 .. 383]
488            + (15 * " 01"),  # fourth run, contains 15 points: [384 .. 398]
489            hexencode(compilePoints(range(399))),
490        )
491
492    def test_decompilePoints(self):
493        numPointsInGlyph = 65536
494        allPoints = list(range(numPointsInGlyph))
495
496        def decompilePoints(data, offset):
497            points, offset = TupleVariation.decompilePoints_(
498                numPointsInGlyph, deHexStr(data), offset, "gvar"
499            )
500            # Conversion to list needed for Python 3.
501            return (list(points), offset)
502
503        # all points in glyph
504        self.assertEqual((allPoints, 1), decompilePoints("00", 0))
505        # all points in glyph (in overly verbose encoding, not explicitly prohibited by spec)
506        self.assertEqual((allPoints, 2), decompilePoints("80 00", 0))
507        # 2 points; first run: [9, 9+6]
508        self.assertEqual(([9, 15], 4), decompilePoints("02 01 09 06", 0))
509        # 2 points; first run: [0xBEEF, 0xCAFE]. (0x0C0F = 0xCAFE - 0xBEEF)
510        self.assertEqual(([0xBEEF, 0xCAFE], 6), decompilePoints("02 81 BE EF 0C 0F", 0))
511        # 1 point; first run: [7]
512        self.assertEqual(([7], 3), decompilePoints("01 00 07", 0))
513        # 1 point; first run: [7] in overly verbose encoding
514        self.assertEqual(([7], 4), decompilePoints("01 80 00 07", 0))
515        # 1 point; first run: [65535]; requires words to be treated as unsigned numbers
516        self.assertEqual(([65535], 4), decompilePoints("01 80 FF FF", 0))
517        # 4 points; first run: [7, 8]; second run: [255, 257]. 257 is stored in delta-encoded bytes (0xFF + 2).
518        self.assertEqual(
519            ([7, 8, 263, 265], 7), decompilePoints("04 01 07 01 01 FF 02", 0)
520        )
521        # combination of all encodings, preceded and followed by 4 bytes of unused data
522        data = "DE AD DE AD 04 01 07 01 81 BE E7 0C 0F DE AD DE AD"
523        self.assertEqual(([7, 8, 0xBEEF, 0xCAFE], 13), decompilePoints(data, 4))
524        self.assertSetEqual(
525            set(range(300)),
526            set(
527                decompilePoints(
528                    "81 2C"
529                    + " 7F 00"  # 300 points (0x12c) in total
530                    + (127 * " 01")
531                    + " 7F"  # first run, contains 128 points: [0 .. 127]
532                    + (128 * " 01")
533                    + " AB"  # second run, contains 128 points: [128 .. 255]
534                    + (44 * " 00 01"),  # third run, contains 44 points: [256 .. 299]
535                    0,
536                )[0]
537            ),
538        )
539        self.assertSetEqual(
540            set(range(399)),
541            set(
542                decompilePoints(
543                    "81 8F"
544                    + " 7F 00"  # 399 points (0x18f) in total
545                    + (127 * " 01")
546                    + " 7F"  # first run, contains 128 points: [0 .. 127]
547                    + (128 * " 01")
548                    + " FF"  # second run, contains 128 points: [128 .. 255]
549                    + (128 * " 00 01")
550                    + " 8E"  # third run, contains 128 points: [256 .. 383]
551                    + (15 * " 00 01"),  # fourth run, contains 15 points: [384 .. 398]
552                    0,
553                )[0]
554            ),
555        )
556
557    def test_decompilePoints_shouldAcceptBadPointNumbers(self):
558        decompilePoints = TupleVariation.decompilePoints_
559        # 2 points; first run: [3, 9].
560        numPointsInGlyph = 8
561        with CapturingLogHandler(log, "WARNING") as captor:
562            decompilePoints(numPointsInGlyph, deHexStr("02 01 03 06"), 0, "cvar")
563        self.assertIn(
564            "point 9 out of range in 'cvar' table", [r.msg for r in captor.records]
565        )
566
567    def test_decompilePoints_roundTrip(self):
568        numPointsInGlyph = (
569            500  # greater than 255, so we also exercise code path for 16-bit encoding
570        )
571        compile = lambda points: TupleVariation.compilePoints(points)
572        decompile = lambda data: set(
573            TupleVariation.decompilePoints_(numPointsInGlyph, data, 0, "gvar")[0]
574        )
575        for i in range(50):
576            points = set(random.sample(range(numPointsInGlyph), 30))
577            self.assertSetEqual(
578                points,
579                decompile(compile(points)),
580                "failed round-trip decompile/compilePoints; points=%s" % points,
581            )
582        allPoints = set(range(numPointsInGlyph))
583        self.assertSetEqual(allPoints, decompile(compile(allPoints)))
584        self.assertSetEqual(allPoints, decompile(compile(set())))
585
586    def test_compileDeltas_points(self):
587        var = TupleVariation({}, [None, (1, 0), (2, 0), None, (4, 0), None])
588        # deltaX for points: [1, 2, 4]; deltaY for points: [0, 0, 0]
589        self.assertEqual("02 01 02 04 82", hexencode(var.compileDeltas()))
590
591    def test_compileDeltas_constants(self):
592        var = TupleVariation({}, [None, 1, 2, None, 4, None])
593        # delta for cvts: [1, 2, 4]
594        self.assertEqual("02 01 02 04", hexencode(var.compileDeltas()))
595
596    def test_compileDeltaValues(self):
597        compileDeltaValues = lambda values: hexencode(
598            TupleVariation.compileDeltaValues_(values)
599        )
600        # zeroes
601        self.assertEqual("80", compileDeltaValues([0]))
602        self.assertEqual("BF", compileDeltaValues([0] * 64))
603        self.assertEqual("BF 80", compileDeltaValues([0] * 65))
604        self.assertEqual("BF A3", compileDeltaValues([0] * 100))
605        self.assertEqual("BF BF BF BF", compileDeltaValues([0] * 256))
606        # bytes
607        self.assertEqual("00 01", compileDeltaValues([1]))
608        self.assertEqual(
609            "06 01 02 03 7F 80 FF FE", compileDeltaValues([1, 2, 3, 127, -128, -1, -2])
610        )
611        self.assertEqual("3F" + (64 * " 7F"), compileDeltaValues([127] * 64))
612        self.assertEqual("3F" + (64 * " 7F") + " 00 7F", compileDeltaValues([127] * 65))
613        # words
614        self.assertEqual("40 66 66", compileDeltaValues([0x6666]))
615        self.assertEqual(
616            "43 66 66 7F FF FF FF 80 00",
617            compileDeltaValues([0x6666, 32767, -1, -32768]),
618        )
619        self.assertEqual("7F" + (64 * " 11 22"), compileDeltaValues([0x1122] * 64))
620        self.assertEqual(
621            "7F" + (64 * " 11 22") + " 40 11 22", compileDeltaValues([0x1122] * 65)
622        )
623        # bytes, zeroes, bytes: a single zero is more compact when encoded as part of the bytes run
624        self.assertEqual(
625            "04 7F 7F 00 7F 7F", compileDeltaValues([127, 127, 0, 127, 127])
626        )
627        self.assertEqual(
628            "01 7F 7F 81 01 7F 7F", compileDeltaValues([127, 127, 0, 0, 127, 127])
629        )
630        self.assertEqual(
631            "01 7F 7F 82 01 7F 7F", compileDeltaValues([127, 127, 0, 0, 0, 127, 127])
632        )
633        self.assertEqual(
634            "01 7F 7F 83 01 7F 7F", compileDeltaValues([127, 127, 0, 0, 0, 0, 127, 127])
635        )
636        # bytes, zeroes
637        self.assertEqual("01 01 00", compileDeltaValues([1, 0]))
638        self.assertEqual("00 01 81", compileDeltaValues([1, 0, 0]))
639        # words, bytes, words: a single byte is more compact when encoded as part of the words run
640        self.assertEqual(
641            "42 66 66 00 02 77 77", compileDeltaValues([0x6666, 2, 0x7777])
642        )
643        self.assertEqual(
644            "40 66 66 01 02 02 40 77 77", compileDeltaValues([0x6666, 2, 2, 0x7777])
645        )
646        # words, zeroes, words
647        self.assertEqual(
648            "40 66 66 80 40 77 77", compileDeltaValues([0x6666, 0, 0x7777])
649        )
650        self.assertEqual(
651            "40 66 66 81 40 77 77", compileDeltaValues([0x6666, 0, 0, 0x7777])
652        )
653        self.assertEqual(
654            "40 66 66 82 40 77 77", compileDeltaValues([0x6666, 0, 0, 0, 0x7777])
655        )
656        # words, zeroes, bytes
657        self.assertEqual(
658            "40 66 66 80 02 01 02 03", compileDeltaValues([0x6666, 0, 1, 2, 3])
659        )
660        self.assertEqual(
661            "40 66 66 81 02 01 02 03", compileDeltaValues([0x6666, 0, 0, 1, 2, 3])
662        )
663        self.assertEqual(
664            "40 66 66 82 02 01 02 03", compileDeltaValues([0x6666, 0, 0, 0, 1, 2, 3])
665        )
666        # words, zeroes
667        self.assertEqual("40 66 66 80", compileDeltaValues([0x6666, 0]))
668        self.assertEqual("40 66 66 81", compileDeltaValues([0x6666, 0, 0]))
669
670    def test_decompileDeltas(self):
671        decompileDeltas = TupleVariation.decompileDeltas_
672        # 83 = zero values (0x80), count = 4 (1 + 0x83 & 0x3F)
673        self.assertEqual(([0, 0, 0, 0], 1), decompileDeltas(4, deHexStr("83"), 0))
674        # 41 01 02 FF FF = signed 16-bit values (0x40), count = 2 (1 + 0x41 & 0x3F)
675        self.assertEqual(
676            ([258, -1], 5), decompileDeltas(2, deHexStr("41 01 02 FF FF"), 0)
677        )
678        # 01 81 07 = signed 8-bit values, count = 2 (1 + 0x01 & 0x3F)
679        self.assertEqual(([-127, 7], 3), decompileDeltas(2, deHexStr("01 81 07"), 0))
680        # combination of all three encodings, preceded and followed by 4 bytes of unused data
681        data = deHexStr("DE AD BE EF 83 40 01 02 01 81 80 DE AD BE EF")
682        self.assertEqual(
683            ([0, 0, 0, 0, 258, -127, -128], 11), decompileDeltas(7, data, 4)
684        )
685
686    def test_decompileDeltas_roundTrip(self):
687        numDeltas = 30
688        compile = TupleVariation.compileDeltaValues_
689        decompile = lambda data: TupleVariation.decompileDeltas_(numDeltas, data, 0)[0]
690        for i in range(50):
691            deltas = random.sample(range(-128, 127), 10)
692            deltas.extend(random.sample(range(-32768, 32767), 10))
693            deltas.extend([0] * 10)
694            random.shuffle(deltas)
695            self.assertListEqual(deltas, decompile(compile(deltas)))
696
697    def test_compileSharedTuples(self):
698        # Below, the peak coordinate {"wght": 1.0, "wdth": 0.8} appears
699        # three times (most frequent sorted first); {"wght": 1.0, "wdth": 0.5}
700        # and {"wght": 1.0, "wdth": 0.7} both appears two times (tie) and
701        # are sorted alphanumerically to ensure determinism.
702        # The peak coordinate {"wght": 1.0, "wdth": 0.9} appears only once
703        # and is thus ignored.
704        # Because the start and end of variation ranges is not encoded
705        # into the shared pool, they should get ignored.
706        deltas = [None] * 4
707        variations = [
708            TupleVariation({"wght": (1.0, 1.0, 1.0), "wdth": (0.5, 0.7, 1.0)}, deltas),
709            TupleVariation({"wght": (1.0, 1.0, 1.0), "wdth": (0.2, 0.7, 1.0)}, deltas),
710            TupleVariation({"wght": (1.0, 1.0, 1.0), "wdth": (0.2, 0.8, 1.0)}, deltas),
711            TupleVariation({"wght": (1.0, 1.0, 1.0), "wdth": (0.3, 0.5, 1.0)}, deltas),
712            TupleVariation({"wght": (1.0, 1.0, 1.0), "wdth": (0.3, 0.8, 1.0)}, deltas),
713            TupleVariation({"wght": (1.0, 1.0, 1.0), "wdth": (0.3, 0.9, 1.0)}, deltas),
714            TupleVariation({"wght": (1.0, 1.0, 1.0), "wdth": (0.4, 0.8, 1.0)}, deltas),
715            TupleVariation({"wght": (1.0, 1.0, 1.0), "wdth": (0.5, 0.5, 1.0)}, deltas),
716        ]
717        result = compileSharedTuples(["wght", "wdth"], variations)
718        self.assertEqual(
719            [hexencode(c) for c in result],
720            ["40 00 33 33", "40 00 20 00", "40 00 2C CD"],
721        )
722
723    def test_decompileSharedTuples_Skia(self):
724        sharedTuples = decompileSharedTuples(
725            axisTags=["wght", "wdth"],
726            sharedTupleCount=8,
727            data=SKIA_GVAR_SHARED_TUPLES_DATA,
728            offset=0,
729        )
730        self.assertEqual(sharedTuples, SKIA_GVAR_SHARED_TUPLES)
731
732    def test_decompileSharedTuples_empty(self):
733        self.assertEqual(decompileSharedTuples(["wght"], 0, b"", 0), [])
734
735    def test_compileTupleVariationStore_allVariationsRedundant(self):
736        axes = {"wght": (0.3, 0.4, 0.5), "opsz": (0.7, 0.8, 0.9)}
737        variations = [
738            TupleVariation(axes, [None] * 4),
739            TupleVariation(axes, [None] * 4),
740            TupleVariation(axes, [None] * 4),
741        ]
742        self.assertEqual(
743            compileTupleVariationStore(
744                variations,
745                pointCount=8,
746                axisTags=["wght", "opsz"],
747                sharedTupleIndices={},
748            ),
749            (0, b"", b""),
750        )
751
752    def test_compileTupleVariationStore_noVariations(self):
753        self.assertEqual(
754            compileTupleVariationStore(
755                variations=[],
756                pointCount=8,
757                axisTags=["wght", "opsz"],
758                sharedTupleIndices={},
759            ),
760            (0, b"", b""),
761        )
762
763    def test_compileTupleVariationStore_roundTrip_cvar(self):
764        deltas = [1, 2, 3, 4]
765        variations = [
766            TupleVariation({"wght": (0.5, 1.0, 1.0), "wdth": (1.0, 1.0, 1.0)}, deltas),
767            TupleVariation({"wght": (1.0, 1.0, 1.0), "wdth": (1.0, 1.0, 1.0)}, deltas),
768        ]
769        tupleVariationCount, tuples, data = compileTupleVariationStore(
770            variations, pointCount=4, axisTags=["wght", "wdth"], sharedTupleIndices={}
771        )
772        self.assertEqual(
773            decompileTupleVariationStore(
774                "cvar",
775                ["wght", "wdth"],
776                tupleVariationCount,
777                pointCount=4,
778                sharedTuples={},
779                data=(tuples + data),
780                pos=0,
781                dataPos=len(tuples),
782            ),
783            variations,
784        )
785
786    def test_compileTupleVariationStore_roundTrip_gvar(self):
787        deltas = [(1, 1), (2, 2), (3, 3), (4, 4)]
788        variations = [
789            TupleVariation({"wght": (0.5, 1.0, 1.0), "wdth": (1.0, 1.0, 1.0)}, deltas),
790            TupleVariation({"wght": (1.0, 1.0, 1.0), "wdth": (1.0, 1.0, 1.0)}, deltas),
791        ]
792        tupleVariationCount, tuples, data = compileTupleVariationStore(
793            variations, pointCount=4, axisTags=["wght", "wdth"], sharedTupleIndices={}
794        )
795        self.assertEqual(
796            decompileTupleVariationStore(
797                "gvar",
798                ["wght", "wdth"],
799                tupleVariationCount,
800                pointCount=4,
801                sharedTuples={},
802                data=(tuples + data),
803                pos=0,
804                dataPos=len(tuples),
805            ),
806            variations,
807        )
808
809    def test_decompileTupleVariationStore_Skia_I(self):
810        tvar = decompileTupleVariationStore(
811            tableTag="gvar",
812            axisTags=["wght", "wdth"],
813            tupleVariationCount=8,
814            pointCount=18,
815            sharedTuples=SKIA_GVAR_SHARED_TUPLES,
816            data=SKIA_GVAR_I_DATA,
817            pos=4,
818            dataPos=36,
819        )
820        self.assertEqual(len(tvar), 8)
821        self.assertEqual(tvar[0].axes, {"wght": (0.0, 1.0, 1.0)})
822        self.assertEqual(
823            " ".join(["%d,%d" % c for c in tvar[0].coordinates]),
824            "257,0 -127,0 -128,58 -130,90 -130,62 -130,67 -130,32 -127,0 "
825            "257,0 259,14 260,64 260,21 260,69 258,124 0,0 130,0 0,0 0,0",
826        )
827
828    def test_decompileTupleVariationStore_empty(self):
829        self.assertEqual(
830            decompileTupleVariationStore(
831                tableTag="gvar",
832                axisTags=[],
833                tupleVariationCount=0,
834                pointCount=5,
835                sharedTuples=[],
836                data=b"",
837                pos=4,
838                dataPos=4,
839            ),
840            [],
841        )
842
843    def test_getTupleSize(self):
844        getTupleSize = TupleVariation.getTupleSize_
845        numAxes = 3
846        self.assertEqual(4 + numAxes * 2, getTupleSize(0x8042, numAxes))
847        self.assertEqual(4 + numAxes * 4, getTupleSize(0x4077, numAxes))
848        self.assertEqual(4, getTupleSize(0x2077, numAxes))
849        self.assertEqual(4, getTupleSize(11, numAxes))
850
851    def test_inferRegion(self):
852        start, end = inferRegion_({"wght": -0.3, "wdth": 0.7})
853        self.assertEqual(start, {"wght": -0.3, "wdth": 0.0})
854        self.assertEqual(end, {"wght": 0.0, "wdth": 0.7})
855
856    @staticmethod
857    def xml_lines(writer):
858        content = writer.file.getvalue().decode("utf-8")
859        return [line.strip() for line in content.splitlines()][1:]
860
861    def test_getCoordWidth(self):
862        empty = TupleVariation({}, [])
863        self.assertEqual(empty.getCoordWidth(), 0)
864
865        empty = TupleVariation({}, [None])
866        self.assertEqual(empty.getCoordWidth(), 0)
867
868        gvarTuple = TupleVariation({}, [None, (0, 0)])
869        self.assertEqual(gvarTuple.getCoordWidth(), 2)
870
871        cvarTuple = TupleVariation({}, [None, 0])
872        self.assertEqual(cvarTuple.getCoordWidth(), 1)
873
874        cvarTuple.coordinates[1] *= 1.0
875        self.assertEqual(cvarTuple.getCoordWidth(), 1)
876
877        with self.assertRaises(TypeError):
878            TupleVariation({}, [None, "a"]).getCoordWidth()
879
880    def test_scaleDeltas_cvar(self):
881        var = TupleVariation({}, [100, None])
882
883        var.scaleDeltas(1.0)
884        self.assertEqual(var.coordinates, [100, None])
885
886        var.scaleDeltas(0.333)
887        self.assertAlmostEqual(var.coordinates[0], 33.3)
888        self.assertIsNone(var.coordinates[1])
889
890        var.scaleDeltas(0.0)
891        self.assertEqual(var.coordinates, [0, None])
892
893    def test_scaleDeltas_gvar(self):
894        var = TupleVariation({}, [(100, 200), None])
895
896        var.scaleDeltas(1.0)
897        self.assertEqual(var.coordinates, [(100, 200), None])
898
899        var.scaleDeltas(0.333)
900        self.assertAlmostEqual(var.coordinates[0][0], 33.3)
901        self.assertAlmostEqual(var.coordinates[0][1], 66.6)
902        self.assertIsNone(var.coordinates[1])
903
904        var.scaleDeltas(0.0)
905        self.assertEqual(var.coordinates, [(0, 0), None])
906
907    def test_roundDeltas_cvar(self):
908        var = TupleVariation({}, [55.5, None, 99.9])
909        var.roundDeltas()
910        self.assertEqual(var.coordinates, [56, None, 100])
911
912    def test_roundDeltas_gvar(self):
913        var = TupleVariation({}, [(55.5, 100.0), None, (99.9, 100.0)])
914        var.roundDeltas()
915        self.assertEqual(var.coordinates, [(56, 100), None, (100, 100)])
916
917    def test_calcInferredDeltas(self):
918        var = TupleVariation({}, [(0, 0), None, None, None])
919        coords = [(1, 1), (1, 1), (1, 1), (1, 1)]
920
921        var.calcInferredDeltas(coords, [])
922
923        self.assertEqual(var.coordinates, [(0, 0), (0, 0), (0, 0), (0, 0)])
924
925    def test_calcInferredDeltas_invalid(self):
926        # cvar tuples can't have inferred deltas
927        with self.assertRaises(TypeError):
928            TupleVariation({}, [0]).calcInferredDeltas([], [])
929
930        # origCoords must have same length as self.coordinates
931        with self.assertRaises(ValueError):
932            TupleVariation({}, [(0, 0), None]).calcInferredDeltas([], [])
933
934        # at least 4 phantom points required
935        with self.assertRaises(AssertionError):
936            TupleVariation({}, [(0, 0), None]).calcInferredDeltas([(0, 0), (0, 0)], [])
937
938        with self.assertRaises(AssertionError):
939            TupleVariation({}, [(0, 0)] + [None] * 5).calcInferredDeltas(
940                [(0, 0)] * 6, [1, 0]  # endPts not in increasing order
941            )
942
943    def test_optimize(self):
944        var = TupleVariation({"wght": (0.0, 1.0, 1.0)}, [(0, 0)] * 5)
945
946        var.optimize([(0, 0)] * 5, [0])
947
948        self.assertEqual(var.coordinates, [None, None, None, None, None])
949
950    def test_optimize_isComposite(self):
951        # when a composite glyph's deltas are all (0, 0), we still want
952        # to write out an entry in gvar, else macOS doesn't apply any
953        # variations to the composite glyph (even if its individual components
954        # do vary).
955        # https://github.com/fonttools/fonttools/issues/1381
956        var = TupleVariation({"wght": (0.0, 1.0, 1.0)}, [(0, 0)] * 5)
957        var.optimize([(0, 0)] * 5, [0], isComposite=True)
958        self.assertEqual(var.coordinates, [(0, 0)] * 5)
959
960        # it takes more than 128 (0, 0) deltas before the optimized tuple with
961        # (None) inferred deltas (except for the first) becomes smaller than
962        # the un-optimized one that has all deltas explicitly set to (0, 0).
963        var = TupleVariation({"wght": (0.0, 1.0, 1.0)}, [(0, 0)] * 129)
964        var.optimize([(0, 0)] * 129, list(range(129 - 4)), isComposite=True)
965        self.assertEqual(var.coordinates, [(0, 0)] + [None] * 128)
966
967    def test_sum_deltas_gvar(self):
968        var1 = TupleVariation(
969            {},
970            [
971                (-20, 0),
972                (-20, 0),
973                (20, 0),
974                (20, 0),
975                (0, 0),
976                (0, 0),
977                (0, 0),
978                (0, 0),
979            ],
980        )
981        var2 = TupleVariation(
982            {},
983            [
984                (-10, 0),
985                (-10, 0),
986                (10, 0),
987                (10, 0),
988                (0, 0),
989                (20, 0),
990                (0, 0),
991                (0, 0),
992            ],
993        )
994
995        var1 += var2
996
997        self.assertEqual(
998            var1.coordinates,
999            [
1000                (-30, 0),
1001                (-30, 0),
1002                (30, 0),
1003                (30, 0),
1004                (0, 0),
1005                (20, 0),
1006                (0, 0),
1007                (0, 0),
1008            ],
1009        )
1010
1011    def test_sum_deltas_gvar_invalid_length(self):
1012        var1 = TupleVariation({}, [(1, 2)])
1013        var2 = TupleVariation({}, [(1, 2), (3, 4)])
1014
1015        with self.assertRaisesRegex(ValueError, "deltas with different lengths"):
1016            var1 += var2
1017
1018    def test_sum_deltas_gvar_with_inferred_points(self):
1019        var1 = TupleVariation({}, [(1, 2), None])
1020        var2 = TupleVariation({}, [(2, 3), None])
1021
1022        with self.assertRaisesRegex(ValueError, "deltas with inferred points"):
1023            var1 += var2
1024
1025    def test_sum_deltas_cvar(self):
1026        axes = {"wght": (0.0, 1.0, 1.0)}
1027        var1 = TupleVariation(axes, [0, 1, None, None])
1028        var2 = TupleVariation(axes, [None, 2, None, 3])
1029        var3 = TupleVariation(axes, [None, None, None, 4])
1030
1031        var1 += var2
1032        var1 += var3
1033
1034        self.assertEqual(var1.coordinates, [0, 3, None, 7])
1035
1036
1037if __name__ == "__main__":
1038    import sys
1039
1040    sys.exit(unittest.main())
1041