xref: /aosp_15_r20/external/fonttools/Tests/otlLib/builder_test.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1import io
2import struct
3from fontTools.misc.fixedTools import floatToFixed, fixedToFloat
4from fontTools.misc.testTools import getXML
5from fontTools.otlLib import builder, error
6from fontTools import ttLib
7from fontTools.ttLib.tables import otTables
8import pytest
9
10
11class BuilderTest(object):
12    GLYPHS = (
13        ".notdef space zero one two three four five six "
14        "A B C a b c grave acute cedilla f_f_i f_i c_t"
15    ).split()
16    GLYPHMAP = {name: num for num, name in enumerate(GLYPHS)}
17
18    ANCHOR1 = builder.buildAnchor(11, -11)
19    ANCHOR2 = builder.buildAnchor(22, -22)
20    ANCHOR3 = builder.buildAnchor(33, -33)
21
22    def test_buildAnchor_format1(self):
23        anchor = builder.buildAnchor(23, 42)
24        assert getXML(anchor.toXML) == [
25            '<Anchor Format="1">',
26            '  <XCoordinate value="23"/>',
27            '  <YCoordinate value="42"/>',
28            "</Anchor>",
29        ]
30
31    def test_buildAnchor_format2(self):
32        anchor = builder.buildAnchor(23, 42, point=17)
33        assert getXML(anchor.toXML) == [
34            '<Anchor Format="2">',
35            '  <XCoordinate value="23"/>',
36            '  <YCoordinate value="42"/>',
37            '  <AnchorPoint value="17"/>',
38            "</Anchor>",
39        ]
40
41    def test_buildAnchor_format3(self):
42        anchor = builder.buildAnchor(
43            23,
44            42,
45            deviceX=builder.buildDevice({1: 1, 0: 0}),
46            deviceY=builder.buildDevice({7: 7}),
47        )
48        assert getXML(anchor.toXML) == [
49            '<Anchor Format="3">',
50            '  <XCoordinate value="23"/>',
51            '  <YCoordinate value="42"/>',
52            "  <XDeviceTable>",
53            '    <StartSize value="0"/>',
54            '    <EndSize value="1"/>',
55            '    <DeltaFormat value="1"/>',
56            '    <DeltaValue value="[0, 1]"/>',
57            "  </XDeviceTable>",
58            "  <YDeviceTable>",
59            '    <StartSize value="7"/>',
60            '    <EndSize value="7"/>',
61            '    <DeltaFormat value="2"/>',
62            '    <DeltaValue value="[7]"/>',
63            "  </YDeviceTable>",
64            "</Anchor>",
65        ]
66
67    def test_buildAttachList(self):
68        attachList = builder.buildAttachList(
69            {"zero": [23, 7], "one": [1]}, self.GLYPHMAP
70        )
71        assert getXML(attachList.toXML) == [
72            "<AttachList>",
73            "  <Coverage>",
74            '    <Glyph value="zero"/>',
75            '    <Glyph value="one"/>',
76            "  </Coverage>",
77            "  <!-- GlyphCount=2 -->",
78            '  <AttachPoint index="0">',
79            "    <!-- PointCount=2 -->",
80            '    <PointIndex index="0" value="7"/>',
81            '    <PointIndex index="1" value="23"/>',
82            "  </AttachPoint>",
83            '  <AttachPoint index="1">',
84            "    <!-- PointCount=1 -->",
85            '    <PointIndex index="0" value="1"/>',
86            "  </AttachPoint>",
87            "</AttachList>",
88        ]
89
90    def test_buildAttachList_empty(self):
91        assert builder.buildAttachList({}, self.GLYPHMAP) is None
92
93    def test_buildAttachPoint(self):
94        attachPoint = builder.buildAttachPoint([7, 3])
95        assert getXML(attachPoint.toXML) == [
96            "<AttachPoint>",
97            "  <!-- PointCount=2 -->",
98            '  <PointIndex index="0" value="3"/>',
99            '  <PointIndex index="1" value="7"/>',
100            "</AttachPoint>",
101        ]
102
103    def test_buildAttachPoint_empty(self):
104        assert builder.buildAttachPoint([]) is None
105
106    def test_buildAttachPoint_duplicate(self):
107        attachPoint = builder.buildAttachPoint([7, 3, 7])
108        assert getXML(attachPoint.toXML) == [
109            "<AttachPoint>",
110            "  <!-- PointCount=2 -->",
111            '  <PointIndex index="0" value="3"/>',
112            '  <PointIndex index="1" value="7"/>',
113            "</AttachPoint>",
114        ]
115
116    def test_buildBaseArray(self):
117        anchor = builder.buildAnchor
118        baseArray = builder.buildBaseArray(
119            {"a": {2: anchor(300, 80)}, "c": {1: anchor(300, 80), 2: anchor(300, -20)}},
120            numMarkClasses=4,
121            glyphMap=self.GLYPHMAP,
122        )
123        assert getXML(baseArray.toXML) == [
124            "<BaseArray>",
125            "  <!-- BaseCount=2 -->",
126            '  <BaseRecord index="0">',
127            '    <BaseAnchor index="0" empty="1"/>',
128            '    <BaseAnchor index="1" empty="1"/>',
129            '    <BaseAnchor index="2" Format="1">',
130            '      <XCoordinate value="300"/>',
131            '      <YCoordinate value="80"/>',
132            "    </BaseAnchor>",
133            '    <BaseAnchor index="3" empty="1"/>',
134            "  </BaseRecord>",
135            '  <BaseRecord index="1">',
136            '    <BaseAnchor index="0" empty="1"/>',
137            '    <BaseAnchor index="1" Format="1">',
138            '      <XCoordinate value="300"/>',
139            '      <YCoordinate value="80"/>',
140            "    </BaseAnchor>",
141            '    <BaseAnchor index="2" Format="1">',
142            '      <XCoordinate value="300"/>',
143            '      <YCoordinate value="-20"/>',
144            "    </BaseAnchor>",
145            '    <BaseAnchor index="3" empty="1"/>',
146            "  </BaseRecord>",
147            "</BaseArray>",
148        ]
149
150    def test_buildBaseRecord(self):
151        a = builder.buildAnchor
152        rec = builder.buildBaseRecord([a(500, -20), None, a(300, -15)])
153        assert getXML(rec.toXML) == [
154            "<BaseRecord>",
155            '  <BaseAnchor index="0" Format="1">',
156            '    <XCoordinate value="500"/>',
157            '    <YCoordinate value="-20"/>',
158            "  </BaseAnchor>",
159            '  <BaseAnchor index="1" empty="1"/>',
160            '  <BaseAnchor index="2" Format="1">',
161            '    <XCoordinate value="300"/>',
162            '    <YCoordinate value="-15"/>',
163            "  </BaseAnchor>",
164            "</BaseRecord>",
165        ]
166
167    def test_buildCaretValueForCoord(self):
168        caret = builder.buildCaretValueForCoord(500)
169        assert getXML(caret.toXML) == [
170            '<CaretValue Format="1">',
171            '  <Coordinate value="500"/>',
172            "</CaretValue>",
173        ]
174
175    def test_buildCaretValueForPoint(self):
176        caret = builder.buildCaretValueForPoint(23)
177        assert getXML(caret.toXML) == [
178            '<CaretValue Format="2">',
179            '  <CaretValuePoint value="23"/>',
180            "</CaretValue>",
181        ]
182
183    def test_buildComponentRecord(self):
184        a = builder.buildAnchor
185        rec = builder.buildComponentRecord([a(500, -20), None, a(300, -15)])
186        assert getXML(rec.toXML) == [
187            "<ComponentRecord>",
188            '  <LigatureAnchor index="0" Format="1">',
189            '    <XCoordinate value="500"/>',
190            '    <YCoordinate value="-20"/>',
191            "  </LigatureAnchor>",
192            '  <LigatureAnchor index="1" empty="1"/>',
193            '  <LigatureAnchor index="2" Format="1">',
194            '    <XCoordinate value="300"/>',
195            '    <YCoordinate value="-15"/>',
196            "  </LigatureAnchor>",
197            "</ComponentRecord>",
198        ]
199
200    def test_buildComponentRecord_empty(self):
201        assert builder.buildComponentRecord([]) is None
202
203    def test_buildComponentRecord_None(self):
204        assert builder.buildComponentRecord(None) is None
205
206    def test_buildCoverage(self):
207        cov = builder.buildCoverage(("two", "four", "two"), {"two": 2, "four": 4})
208        assert getXML(cov.toXML) == [
209            "<Coverage>",
210            '  <Glyph value="two"/>',
211            '  <Glyph value="four"/>',
212            "</Coverage>",
213        ]
214
215    def test_buildCursivePos(self):
216        pos = builder.buildCursivePosSubtable(
217            {"two": (self.ANCHOR1, self.ANCHOR2), "four": (self.ANCHOR3, self.ANCHOR1)},
218            self.GLYPHMAP,
219        )
220        assert getXML(pos.toXML) == [
221            '<CursivePos Format="1">',
222            "  <Coverage>",
223            '    <Glyph value="two"/>',
224            '    <Glyph value="four"/>',
225            "  </Coverage>",
226            "  <!-- EntryExitCount=2 -->",
227            '  <EntryExitRecord index="0">',
228            '    <EntryAnchor Format="1">',
229            '      <XCoordinate value="11"/>',
230            '      <YCoordinate value="-11"/>',
231            "    </EntryAnchor>",
232            '    <ExitAnchor Format="1">',
233            '      <XCoordinate value="22"/>',
234            '      <YCoordinate value="-22"/>',
235            "    </ExitAnchor>",
236            "  </EntryExitRecord>",
237            '  <EntryExitRecord index="1">',
238            '    <EntryAnchor Format="1">',
239            '      <XCoordinate value="33"/>',
240            '      <YCoordinate value="-33"/>',
241            "    </EntryAnchor>",
242            '    <ExitAnchor Format="1">',
243            '      <XCoordinate value="11"/>',
244            '      <YCoordinate value="-11"/>',
245            "    </ExitAnchor>",
246            "  </EntryExitRecord>",
247            "</CursivePos>",
248        ]
249
250    def test_buildDevice_format1(self):
251        device = builder.buildDevice({1: 1, 0: 0})
252        assert getXML(device.toXML) == [
253            "<Device>",
254            '  <StartSize value="0"/>',
255            '  <EndSize value="1"/>',
256            '  <DeltaFormat value="1"/>',
257            '  <DeltaValue value="[0, 1]"/>',
258            "</Device>",
259        ]
260
261    def test_buildDevice_format2(self):
262        device = builder.buildDevice({2: 2, 0: 1, 1: 0})
263        assert getXML(device.toXML) == [
264            "<Device>",
265            '  <StartSize value="0"/>',
266            '  <EndSize value="2"/>',
267            '  <DeltaFormat value="2"/>',
268            '  <DeltaValue value="[1, 0, 2]"/>',
269            "</Device>",
270        ]
271
272    def test_buildDevice_format3(self):
273        device = builder.buildDevice({5: 3, 1: 77})
274        assert getXML(device.toXML) == [
275            "<Device>",
276            '  <StartSize value="1"/>',
277            '  <EndSize value="5"/>',
278            '  <DeltaFormat value="3"/>',
279            '  <DeltaValue value="[77, 0, 0, 0, 3]"/>',
280            "</Device>",
281        ]
282
283    def test_buildLigatureArray(self):
284        anchor = builder.buildAnchor
285        ligatureArray = builder.buildLigatureArray(
286            {
287                "f_i": [{2: anchor(300, -20)}, {}],
288                "c_t": [{}, {1: anchor(500, 350), 2: anchor(1300, -20)}],
289            },
290            numMarkClasses=4,
291            glyphMap=self.GLYPHMAP,
292        )
293        assert getXML(ligatureArray.toXML) == [
294            "<LigatureArray>",
295            "  <!-- LigatureCount=2 -->",
296            '  <LigatureAttach index="0">',  # f_i
297            "    <!-- ComponentCount=2 -->",
298            '    <ComponentRecord index="0">',
299            '      <LigatureAnchor index="0" empty="1"/>',
300            '      <LigatureAnchor index="1" empty="1"/>',
301            '      <LigatureAnchor index="2" Format="1">',
302            '        <XCoordinate value="300"/>',
303            '        <YCoordinate value="-20"/>',
304            "      </LigatureAnchor>",
305            '      <LigatureAnchor index="3" empty="1"/>',
306            "    </ComponentRecord>",
307            '    <ComponentRecord index="1">',
308            '      <LigatureAnchor index="0" empty="1"/>',
309            '      <LigatureAnchor index="1" empty="1"/>',
310            '      <LigatureAnchor index="2" empty="1"/>',
311            '      <LigatureAnchor index="3" empty="1"/>',
312            "    </ComponentRecord>",
313            "  </LigatureAttach>",
314            '  <LigatureAttach index="1">',
315            "    <!-- ComponentCount=2 -->",
316            '    <ComponentRecord index="0">',
317            '      <LigatureAnchor index="0" empty="1"/>',
318            '      <LigatureAnchor index="1" empty="1"/>',
319            '      <LigatureAnchor index="2" empty="1"/>',
320            '      <LigatureAnchor index="3" empty="1"/>',
321            "    </ComponentRecord>",
322            '    <ComponentRecord index="1">',
323            '      <LigatureAnchor index="0" empty="1"/>',
324            '      <LigatureAnchor index="1" Format="1">',
325            '        <XCoordinate value="500"/>',
326            '        <YCoordinate value="350"/>',
327            "      </LigatureAnchor>",
328            '      <LigatureAnchor index="2" Format="1">',
329            '        <XCoordinate value="1300"/>',
330            '        <YCoordinate value="-20"/>',
331            "      </LigatureAnchor>",
332            '      <LigatureAnchor index="3" empty="1"/>',
333            "    </ComponentRecord>",
334            "  </LigatureAttach>",
335            "</LigatureArray>",
336        ]
337
338    def test_buildLigatureAttach(self):
339        anchor = builder.buildAnchor
340        attach = builder.buildLigatureAttach(
341            [[anchor(500, -10), None], [None, anchor(300, -20), None]]
342        )
343        assert getXML(attach.toXML) == [
344            "<LigatureAttach>",
345            "  <!-- ComponentCount=2 -->",
346            '  <ComponentRecord index="0">',
347            '    <LigatureAnchor index="0" Format="1">',
348            '      <XCoordinate value="500"/>',
349            '      <YCoordinate value="-10"/>',
350            "    </LigatureAnchor>",
351            '    <LigatureAnchor index="1" empty="1"/>',
352            "  </ComponentRecord>",
353            '  <ComponentRecord index="1">',
354            '    <LigatureAnchor index="0" empty="1"/>',
355            '    <LigatureAnchor index="1" Format="1">',
356            '      <XCoordinate value="300"/>',
357            '      <YCoordinate value="-20"/>',
358            "    </LigatureAnchor>",
359            '    <LigatureAnchor index="2" empty="1"/>',
360            "  </ComponentRecord>",
361            "</LigatureAttach>",
362        ]
363
364    def test_buildLigatureAttach_emptyComponents(self):
365        attach = builder.buildLigatureAttach([[], None])
366        assert getXML(attach.toXML) == [
367            "<LigatureAttach>",
368            "  <!-- ComponentCount=2 -->",
369            '  <ComponentRecord index="0" empty="1"/>',
370            '  <ComponentRecord index="1" empty="1"/>',
371            "</LigatureAttach>",
372        ]
373
374    def test_buildLigatureAttach_noComponents(self):
375        attach = builder.buildLigatureAttach([])
376        assert getXML(attach.toXML) == [
377            "<LigatureAttach>",
378            "  <!-- ComponentCount=0 -->",
379            "</LigatureAttach>",
380        ]
381
382    def test_buildLigCaretList(self):
383        carets = builder.buildLigCaretList(
384            {"f_f_i": [300, 600]}, {"c_t": [42]}, self.GLYPHMAP
385        )
386        assert getXML(carets.toXML) == [
387            "<LigCaretList>",
388            "  <Coverage>",
389            '    <Glyph value="f_f_i"/>',
390            '    <Glyph value="c_t"/>',
391            "  </Coverage>",
392            "  <!-- LigGlyphCount=2 -->",
393            '  <LigGlyph index="0">',
394            "    <!-- CaretCount=2 -->",
395            '    <CaretValue index="0" Format="1">',
396            '      <Coordinate value="300"/>',
397            "    </CaretValue>",
398            '    <CaretValue index="1" Format="1">',
399            '      <Coordinate value="600"/>',
400            "    </CaretValue>",
401            "  </LigGlyph>",
402            '  <LigGlyph index="1">',
403            "    <!-- CaretCount=1 -->",
404            '    <CaretValue index="0" Format="2">',
405            '      <CaretValuePoint value="42"/>',
406            "    </CaretValue>",
407            "  </LigGlyph>",
408            "</LigCaretList>",
409        ]
410
411    def test_buildLigCaretList_bothCoordsAndPointsForSameGlyph(self):
412        carets = builder.buildLigCaretList(
413            {"f_f_i": [300]}, {"f_f_i": [7]}, self.GLYPHMAP
414        )
415        assert getXML(carets.toXML) == [
416            "<LigCaretList>",
417            "  <Coverage>",
418            '    <Glyph value="f_f_i"/>',
419            "  </Coverage>",
420            "  <!-- LigGlyphCount=1 -->",
421            '  <LigGlyph index="0">',
422            "    <!-- CaretCount=2 -->",
423            '    <CaretValue index="0" Format="1">',
424            '      <Coordinate value="300"/>',
425            "    </CaretValue>",
426            '    <CaretValue index="1" Format="2">',
427            '      <CaretValuePoint value="7"/>',
428            "    </CaretValue>",
429            "  </LigGlyph>",
430            "</LigCaretList>",
431        ]
432
433    def test_buildLigCaretList_empty(self):
434        assert builder.buildLigCaretList({}, {}, self.GLYPHMAP) is None
435
436    def test_buildLigCaretList_None(self):
437        assert builder.buildLigCaretList(None, None, self.GLYPHMAP) is None
438
439    def test_buildLigGlyph_coords(self):
440        lig = builder.buildLigGlyph([500, 800], None)
441        assert getXML(lig.toXML) == [
442            "<LigGlyph>",
443            "  <!-- CaretCount=2 -->",
444            '  <CaretValue index="0" Format="1">',
445            '    <Coordinate value="500"/>',
446            "  </CaretValue>",
447            '  <CaretValue index="1" Format="1">',
448            '    <Coordinate value="800"/>',
449            "  </CaretValue>",
450            "</LigGlyph>",
451        ]
452
453    def test_buildLigGlyph_empty(self):
454        assert builder.buildLigGlyph([], []) is None
455
456    def test_buildLigGlyph_None(self):
457        assert builder.buildLigGlyph(None, None) is None
458
459    def test_buildLigGlyph_points(self):
460        lig = builder.buildLigGlyph(None, [2])
461        assert getXML(lig.toXML) == [
462            "<LigGlyph>",
463            "  <!-- CaretCount=1 -->",
464            '  <CaretValue index="0" Format="2">',
465            '    <CaretValuePoint value="2"/>',
466            "  </CaretValue>",
467            "</LigGlyph>",
468        ]
469
470    def test_buildLookup(self):
471        s1 = builder.buildSingleSubstSubtable({"one": "two"})
472        s2 = builder.buildSingleSubstSubtable({"three": "four"})
473        lookup = builder.buildLookup([s1, s2], flags=7)
474        assert getXML(lookup.toXML) == [
475            "<Lookup>",
476            '  <LookupType value="1"/>',
477            '  <LookupFlag value="7"/><!-- rightToLeft ignoreBaseGlyphs ignoreLigatures -->',
478            "  <!-- SubTableCount=2 -->",
479            '  <SingleSubst index="0">',
480            '    <Substitution in="one" out="two"/>',
481            "  </SingleSubst>",
482            '  <SingleSubst index="1">',
483            '    <Substitution in="three" out="four"/>',
484            "  </SingleSubst>",
485            "</Lookup>",
486        ]
487
488    def test_buildLookup_badFlags(self):
489        s = builder.buildSingleSubstSubtable({"one": "two"})
490        with pytest.raises(
491            AssertionError,
492            match=(
493                "if markFilterSet is None, flags must not set "
494                "LOOKUP_FLAG_USE_MARK_FILTERING_SET; flags=0x0010"
495            ),
496        ) as excinfo:
497            builder.buildLookup([s], builder.LOOKUP_FLAG_USE_MARK_FILTERING_SET, None)
498
499    def test_buildLookup_conflictingSubtableTypes(self):
500        s1 = builder.buildSingleSubstSubtable({"one": "two"})
501        s2 = builder.buildAlternateSubstSubtable({"one": ["two", "three"]})
502        with pytest.raises(
503            AssertionError, match="all subtables must have the same LookupType"
504        ) as excinfo:
505            builder.buildLookup([s1, s2])
506
507    def test_buildLookup_noSubtables(self):
508        assert builder.buildLookup([]) is None
509        assert builder.buildLookup(None) is None
510        assert builder.buildLookup([None]) is None
511        assert builder.buildLookup([None, None]) is None
512
513    def test_buildLookup_markFilterSet(self):
514        s = builder.buildSingleSubstSubtable({"one": "two"})
515        flags = (
516            builder.LOOKUP_FLAG_RIGHT_TO_LEFT
517            | builder.LOOKUP_FLAG_USE_MARK_FILTERING_SET
518        )
519        lookup = builder.buildLookup([s], flags, markFilterSet=999)
520        assert getXML(lookup.toXML) == [
521            "<Lookup>",
522            '  <LookupType value="1"/>',
523            '  <LookupFlag value="17"/><!-- rightToLeft useMarkFilteringSet -->',
524            "  <!-- SubTableCount=1 -->",
525            '  <SingleSubst index="0">',
526            '    <Substitution in="one" out="two"/>',
527            "  </SingleSubst>",
528            '  <MarkFilteringSet value="999"/>',
529            "</Lookup>",
530        ]
531
532    def test_buildMarkArray(self):
533        markArray = builder.buildMarkArray(
534            {
535                "acute": (7, builder.buildAnchor(300, 800)),
536                "grave": (2, builder.buildAnchor(10, 80)),
537            },
538            self.GLYPHMAP,
539        )
540        assert self.GLYPHMAP["grave"] < self.GLYPHMAP["acute"]
541        assert getXML(markArray.toXML) == [
542            "<MarkArray>",
543            "  <!-- MarkCount=2 -->",
544            '  <MarkRecord index="0">',
545            '    <Class value="2"/>',
546            '    <MarkAnchor Format="1">',
547            '      <XCoordinate value="10"/>',
548            '      <YCoordinate value="80"/>',
549            "    </MarkAnchor>",
550            "  </MarkRecord>",
551            '  <MarkRecord index="1">',
552            '    <Class value="7"/>',
553            '    <MarkAnchor Format="1">',
554            '      <XCoordinate value="300"/>',
555            '      <YCoordinate value="800"/>',
556            "    </MarkAnchor>",
557            "  </MarkRecord>",
558            "</MarkArray>",
559        ]
560
561    def test_buildMarkBasePosSubtable(self):
562        anchor = builder.buildAnchor
563        marks = {
564            "acute": (0, anchor(300, 700)),
565            "cedilla": (1, anchor(300, -100)),
566            "grave": (0, anchor(300, 700)),
567        }
568        bases = {
569            # Make sure we can handle missing entries.
570            "A": {},  # no entry for any markClass
571            "B": {0: anchor(500, 900)},  # only markClass 0 specified
572            "C": {1: anchor(500, -10)},  # only markClass 1 specified
573            "a": {0: anchor(500, 400), 1: anchor(500, -20)},
574            "b": {0: anchor(500, 800), 1: anchor(500, -20)},
575        }
576        table = builder.buildMarkBasePosSubtable(marks, bases, self.GLYPHMAP)
577        assert getXML(table.toXML) == [
578            '<MarkBasePos Format="1">',
579            "  <MarkCoverage>",
580            '    <Glyph value="grave"/>',
581            '    <Glyph value="acute"/>',
582            '    <Glyph value="cedilla"/>',
583            "  </MarkCoverage>",
584            "  <BaseCoverage>",
585            '    <Glyph value="A"/>',
586            '    <Glyph value="B"/>',
587            '    <Glyph value="C"/>',
588            '    <Glyph value="a"/>',
589            '    <Glyph value="b"/>',
590            "  </BaseCoverage>",
591            "  <!-- ClassCount=2 -->",
592            "  <MarkArray>",
593            "    <!-- MarkCount=3 -->",
594            '    <MarkRecord index="0">',  # grave
595            '      <Class value="0"/>',
596            '      <MarkAnchor Format="1">',
597            '        <XCoordinate value="300"/>',
598            '        <YCoordinate value="700"/>',
599            "      </MarkAnchor>",
600            "    </MarkRecord>",
601            '    <MarkRecord index="1">',  # acute
602            '      <Class value="0"/>',
603            '      <MarkAnchor Format="1">',
604            '        <XCoordinate value="300"/>',
605            '        <YCoordinate value="700"/>',
606            "      </MarkAnchor>",
607            "    </MarkRecord>",
608            '    <MarkRecord index="2">',  # cedilla
609            '      <Class value="1"/>',
610            '      <MarkAnchor Format="1">',
611            '        <XCoordinate value="300"/>',
612            '        <YCoordinate value="-100"/>',
613            "      </MarkAnchor>",
614            "    </MarkRecord>",
615            "  </MarkArray>",
616            "  <BaseArray>",
617            "    <!-- BaseCount=5 -->",
618            '    <BaseRecord index="0">',  # A
619            '      <BaseAnchor index="0" empty="1"/>',
620            '      <BaseAnchor index="1" empty="1"/>',
621            "    </BaseRecord>",
622            '    <BaseRecord index="1">',  # B
623            '      <BaseAnchor index="0" Format="1">',
624            '        <XCoordinate value="500"/>',
625            '        <YCoordinate value="900"/>',
626            "      </BaseAnchor>",
627            '      <BaseAnchor index="1" empty="1"/>',
628            "    </BaseRecord>",
629            '    <BaseRecord index="2">',  # C
630            '      <BaseAnchor index="0" empty="1"/>',
631            '      <BaseAnchor index="1" Format="1">',
632            '        <XCoordinate value="500"/>',
633            '        <YCoordinate value="-10"/>',
634            "      </BaseAnchor>",
635            "    </BaseRecord>",
636            '    <BaseRecord index="3">',  # a
637            '      <BaseAnchor index="0" Format="1">',
638            '        <XCoordinate value="500"/>',
639            '        <YCoordinate value="400"/>',
640            "      </BaseAnchor>",
641            '      <BaseAnchor index="1" Format="1">',
642            '        <XCoordinate value="500"/>',
643            '        <YCoordinate value="-20"/>',
644            "      </BaseAnchor>",
645            "    </BaseRecord>",
646            '    <BaseRecord index="4">',  # b
647            '      <BaseAnchor index="0" Format="1">',
648            '        <XCoordinate value="500"/>',
649            '        <YCoordinate value="800"/>',
650            "      </BaseAnchor>",
651            '      <BaseAnchor index="1" Format="1">',
652            '        <XCoordinate value="500"/>',
653            '        <YCoordinate value="-20"/>',
654            "      </BaseAnchor>",
655            "    </BaseRecord>",
656            "  </BaseArray>",
657            "</MarkBasePos>",
658        ]
659
660    def test_buildMarkGlyphSetsDef(self):
661        marksets = builder.buildMarkGlyphSetsDef(
662            [{"acute", "grave"}, {"cedilla", "grave"}], self.GLYPHMAP
663        )
664        assert getXML(marksets.toXML) == [
665            "<MarkGlyphSetsDef>",
666            '  <MarkSetTableFormat value="1"/>',
667            "  <!-- MarkSetCount=2 -->",
668            '  <Coverage index="0">',
669            '    <Glyph value="grave"/>',
670            '    <Glyph value="acute"/>',
671            "  </Coverage>",
672            '  <Coverage index="1">',
673            '    <Glyph value="grave"/>',
674            '    <Glyph value="cedilla"/>',
675            "  </Coverage>",
676            "</MarkGlyphSetsDef>",
677        ]
678
679    def test_buildMarkGlyphSetsDef_empty(self):
680        assert builder.buildMarkGlyphSetsDef([], self.GLYPHMAP) is None
681
682    def test_buildMarkGlyphSetsDef_None(self):
683        assert builder.buildMarkGlyphSetsDef(None, self.GLYPHMAP) is None
684
685    def test_buildMarkLigPosSubtable(self):
686        anchor = builder.buildAnchor
687        marks = {
688            "acute": (0, anchor(300, 700)),
689            "cedilla": (1, anchor(300, -100)),
690            "grave": (0, anchor(300, 700)),
691        }
692        bases = {
693            "f_i": [{}, {0: anchor(200, 400)}],  # nothing on f; only 1 on i
694            "c_t": [
695                {0: anchor(500, 600), 1: anchor(500, -20)},  # c
696                {0: anchor(1300, 800), 1: anchor(1300, -20)},  # t
697            ],
698        }
699        table = builder.buildMarkLigPosSubtable(marks, bases, self.GLYPHMAP)
700        assert getXML(table.toXML) == [
701            '<MarkLigPos Format="1">',
702            "  <MarkCoverage>",
703            '    <Glyph value="grave"/>',
704            '    <Glyph value="acute"/>',
705            '    <Glyph value="cedilla"/>',
706            "  </MarkCoverage>",
707            "  <LigatureCoverage>",
708            '    <Glyph value="f_i"/>',
709            '    <Glyph value="c_t"/>',
710            "  </LigatureCoverage>",
711            "  <!-- ClassCount=2 -->",
712            "  <MarkArray>",
713            "    <!-- MarkCount=3 -->",
714            '    <MarkRecord index="0">',
715            '      <Class value="0"/>',
716            '      <MarkAnchor Format="1">',
717            '        <XCoordinate value="300"/>',
718            '        <YCoordinate value="700"/>',
719            "      </MarkAnchor>",
720            "    </MarkRecord>",
721            '    <MarkRecord index="1">',
722            '      <Class value="0"/>',
723            '      <MarkAnchor Format="1">',
724            '        <XCoordinate value="300"/>',
725            '        <YCoordinate value="700"/>',
726            "      </MarkAnchor>",
727            "    </MarkRecord>",
728            '    <MarkRecord index="2">',
729            '      <Class value="1"/>',
730            '      <MarkAnchor Format="1">',
731            '        <XCoordinate value="300"/>',
732            '        <YCoordinate value="-100"/>',
733            "      </MarkAnchor>",
734            "    </MarkRecord>",
735            "  </MarkArray>",
736            "  <LigatureArray>",
737            "    <!-- LigatureCount=2 -->",
738            '    <LigatureAttach index="0">',
739            "      <!-- ComponentCount=2 -->",
740            '      <ComponentRecord index="0">',
741            '        <LigatureAnchor index="0" empty="1"/>',
742            '        <LigatureAnchor index="1" empty="1"/>',
743            "      </ComponentRecord>",
744            '      <ComponentRecord index="1">',
745            '        <LigatureAnchor index="0" Format="1">',
746            '          <XCoordinate value="200"/>',
747            '          <YCoordinate value="400"/>',
748            "        </LigatureAnchor>",
749            '        <LigatureAnchor index="1" empty="1"/>',
750            "      </ComponentRecord>",
751            "    </LigatureAttach>",
752            '    <LigatureAttach index="1">',
753            "      <!-- ComponentCount=2 -->",
754            '      <ComponentRecord index="0">',
755            '        <LigatureAnchor index="0" Format="1">',
756            '          <XCoordinate value="500"/>',
757            '          <YCoordinate value="600"/>',
758            "        </LigatureAnchor>",
759            '        <LigatureAnchor index="1" Format="1">',
760            '          <XCoordinate value="500"/>',
761            '          <YCoordinate value="-20"/>',
762            "        </LigatureAnchor>",
763            "      </ComponentRecord>",
764            '      <ComponentRecord index="1">',
765            '        <LigatureAnchor index="0" Format="1">',
766            '          <XCoordinate value="1300"/>',
767            '          <YCoordinate value="800"/>',
768            "        </LigatureAnchor>",
769            '        <LigatureAnchor index="1" Format="1">',
770            '          <XCoordinate value="1300"/>',
771            '          <YCoordinate value="-20"/>',
772            "        </LigatureAnchor>",
773            "      </ComponentRecord>",
774            "    </LigatureAttach>",
775            "  </LigatureArray>",
776            "</MarkLigPos>",
777        ]
778
779    def test_buildMarkRecord(self):
780        rec = builder.buildMarkRecord(17, builder.buildAnchor(500, -20))
781        assert getXML(rec.toXML) == [
782            "<MarkRecord>",
783            '  <Class value="17"/>',
784            '  <MarkAnchor Format="1">',
785            '    <XCoordinate value="500"/>',
786            '    <YCoordinate value="-20"/>',
787            "  </MarkAnchor>",
788            "</MarkRecord>",
789        ]
790
791    def test_buildMark2Record(self):
792        a = builder.buildAnchor
793        rec = builder.buildMark2Record([a(500, -20), None, a(300, -15)])
794        assert getXML(rec.toXML) == [
795            "<Mark2Record>",
796            '  <Mark2Anchor index="0" Format="1">',
797            '    <XCoordinate value="500"/>',
798            '    <YCoordinate value="-20"/>',
799            "  </Mark2Anchor>",
800            '  <Mark2Anchor index="1" empty="1"/>',
801            '  <Mark2Anchor index="2" Format="1">',
802            '    <XCoordinate value="300"/>',
803            '    <YCoordinate value="-15"/>',
804            "  </Mark2Anchor>",
805            "</Mark2Record>",
806        ]
807
808    def test_buildPairPosClassesSubtable(self):
809        d20 = builder.buildValue({"XPlacement": -20})
810        d50 = builder.buildValue({"XPlacement": -50})
811        d0 = builder.buildValue({})
812        d8020 = builder.buildValue({"XPlacement": -80, "YPlacement": -20})
813        subtable = builder.buildPairPosClassesSubtable(
814            {
815                (tuple("A"), tuple(["zero"])): (d0, d50),
816                (tuple("A"), tuple(["one", "two"])): (None, d20),
817                (tuple(["B", "C"]), tuple(["zero"])): (d8020, d50),
818            },
819            self.GLYPHMAP,
820        )
821        assert getXML(subtable.toXML) == [
822            '<PairPos Format="2">',
823            "  <Coverage>",
824            '    <Glyph value="A"/>',
825            '    <Glyph value="B"/>',
826            '    <Glyph value="C"/>',
827            "  </Coverage>",
828            '  <ValueFormat1 value="3"/>',
829            '  <ValueFormat2 value="1"/>',
830            "  <ClassDef1>",
831            '    <ClassDef glyph="A" class="1"/>',
832            "  </ClassDef1>",
833            "  <ClassDef2>",
834            '    <ClassDef glyph="one" class="1"/>',
835            '    <ClassDef glyph="two" class="1"/>',
836            '    <ClassDef glyph="zero" class="2"/>',
837            "  </ClassDef2>",
838            "  <!-- Class1Count=2 -->",
839            "  <!-- Class2Count=3 -->",
840            '  <Class1Record index="0">',
841            '    <Class2Record index="0">',
842            '      <Value1 XPlacement="0" YPlacement="0"/>',
843            '      <Value2 XPlacement="0"/>',
844            "    </Class2Record>",
845            '    <Class2Record index="1">',
846            '      <Value1 XPlacement="0" YPlacement="0"/>',
847            '      <Value2 XPlacement="0"/>',
848            "    </Class2Record>",
849            '    <Class2Record index="2">',
850            '      <Value1 XPlacement="-80" YPlacement="-20"/>',
851            '      <Value2 XPlacement="-50"/>',
852            "    </Class2Record>",
853            "  </Class1Record>",
854            '  <Class1Record index="1">',
855            '    <Class2Record index="0">',
856            '      <Value1 XPlacement="0" YPlacement="0"/>',
857            '      <Value2 XPlacement="0"/>',
858            "    </Class2Record>",
859            '    <Class2Record index="1">',
860            '      <Value1 XPlacement="0" YPlacement="0"/>',
861            '      <Value2 XPlacement="-20"/>',
862            "    </Class2Record>",
863            '    <Class2Record index="2">',
864            '      <Value1 XPlacement="0" YPlacement="0"/>',
865            '      <Value2 XPlacement="-50"/>',
866            "    </Class2Record>",
867            "  </Class1Record>",
868            "</PairPos>",
869        ]
870
871    def test_buildPairPosGlyphs(self):
872        d50 = builder.buildValue({"XPlacement": -50})
873        d8020 = builder.buildValue({"XPlacement": -80, "YPlacement": -20})
874        subtables = builder.buildPairPosGlyphs(
875            {("A", "zero"): (None, d50), ("A", "one"): (d8020, d50)}, self.GLYPHMAP
876        )
877        assert sum([getXML(t.toXML) for t in subtables], []) == [
878            '<PairPos Format="1">',
879            "  <Coverage>",
880            '    <Glyph value="A"/>',
881            "  </Coverage>",
882            '  <ValueFormat1 value="0"/>',
883            '  <ValueFormat2 value="1"/>',
884            "  <!-- PairSetCount=1 -->",
885            '  <PairSet index="0">',
886            "    <!-- PairValueCount=1 -->",
887            '    <PairValueRecord index="0">',
888            '      <SecondGlyph value="zero"/>',
889            '      <Value2 XPlacement="-50"/>',
890            "    </PairValueRecord>",
891            "  </PairSet>",
892            "</PairPos>",
893            '<PairPos Format="1">',
894            "  <Coverage>",
895            '    <Glyph value="A"/>',
896            "  </Coverage>",
897            '  <ValueFormat1 value="3"/>',
898            '  <ValueFormat2 value="1"/>',
899            "  <!-- PairSetCount=1 -->",
900            '  <PairSet index="0">',
901            "    <!-- PairValueCount=1 -->",
902            '    <PairValueRecord index="0">',
903            '      <SecondGlyph value="one"/>',
904            '      <Value1 XPlacement="-80" YPlacement="-20"/>',
905            '      <Value2 XPlacement="-50"/>',
906            "    </PairValueRecord>",
907            "  </PairSet>",
908            "</PairPos>",
909        ]
910
911    def test_buildPairPosGlyphsSubtable(self):
912        d20 = builder.buildValue({"XPlacement": -20})
913        d50 = builder.buildValue({"XPlacement": -50})
914        d0 = builder.buildValue({})
915        d8020 = builder.buildValue({"XPlacement": -80, "YPlacement": -20})
916        subtable = builder.buildPairPosGlyphsSubtable(
917            {
918                ("A", "zero"): (d0, d50),
919                ("A", "one"): (None, d20),
920                ("B", "five"): (d8020, d50),
921            },
922            self.GLYPHMAP,
923        )
924
925        assert getXML(subtable.toXML) == [
926            '<PairPos Format="1">',
927            "  <Coverage>",
928            '    <Glyph value="A"/>',
929            '    <Glyph value="B"/>',
930            "  </Coverage>",
931            '  <ValueFormat1 value="3"/>',
932            '  <ValueFormat2 value="1"/>',
933            "  <!-- PairSetCount=2 -->",
934            '  <PairSet index="0">',
935            "    <!-- PairValueCount=2 -->",
936            '    <PairValueRecord index="0">',
937            '      <SecondGlyph value="zero"/>',
938            '      <Value1 XPlacement="0" YPlacement="0"/>',
939            '      <Value2 XPlacement="-50"/>',
940            "    </PairValueRecord>",
941            '    <PairValueRecord index="1">',
942            '      <SecondGlyph value="one"/>',
943            '      <Value1 XPlacement="0" YPlacement="0"/>',
944            '      <Value2 XPlacement="-20"/>',
945            "    </PairValueRecord>",
946            "  </PairSet>",
947            '  <PairSet index="1">',
948            "    <!-- PairValueCount=1 -->",
949            '    <PairValueRecord index="0">',
950            '      <SecondGlyph value="five"/>',
951            '      <Value1 XPlacement="-80" YPlacement="-20"/>',
952            '      <Value2 XPlacement="-50"/>',
953            "    </PairValueRecord>",
954            "  </PairSet>",
955            "</PairPos>",
956        ]
957
958    def test_buildSinglePos(self):
959        subtables = builder.buildSinglePos(
960            {
961                "one": builder.buildValue({"XPlacement": 500}),
962                "two": builder.buildValue({"XPlacement": 500}),
963                "three": builder.buildValue({"XPlacement": 200}),
964                "four": builder.buildValue({"XPlacement": 400}),
965                "five": builder.buildValue({"XPlacement": 500}),
966                "six": builder.buildValue({"YPlacement": -6}),
967            },
968            self.GLYPHMAP,
969        )
970        assert sum([getXML(t.toXML) for t in subtables], []) == [
971            '<SinglePos Format="2">',
972            "  <Coverage>",
973            '    <Glyph value="one"/>',
974            '    <Glyph value="two"/>',
975            '    <Glyph value="three"/>',
976            '    <Glyph value="four"/>',
977            '    <Glyph value="five"/>',
978            "  </Coverage>",
979            '  <ValueFormat value="1"/>',
980            "  <!-- ValueCount=5 -->",
981            '  <Value index="0" XPlacement="500"/>',
982            '  <Value index="1" XPlacement="500"/>',
983            '  <Value index="2" XPlacement="200"/>',
984            '  <Value index="3" XPlacement="400"/>',
985            '  <Value index="4" XPlacement="500"/>',
986            "</SinglePos>",
987            '<SinglePos Format="1">',
988            "  <Coverage>",
989            '    <Glyph value="six"/>',
990            "  </Coverage>",
991            '  <ValueFormat value="2"/>',
992            '  <Value YPlacement="-6"/>',
993            "</SinglePos>",
994        ]
995
996    def test_buildSinglePos_ValueFormat0(self):
997        subtables = builder.buildSinglePos(
998            {"zero": builder.buildValue({})}, self.GLYPHMAP
999        )
1000        assert sum([getXML(t.toXML) for t in subtables], []) == [
1001            '<SinglePos Format="1">',
1002            "  <Coverage>",
1003            '    <Glyph value="zero"/>',
1004            "  </Coverage>",
1005            '  <ValueFormat value="0"/>',
1006            "</SinglePos>",
1007        ]
1008
1009    def test_buildSinglePosSubtable_format1(self):
1010        subtable = builder.buildSinglePosSubtable(
1011            {
1012                "one": builder.buildValue({"XPlacement": 777}),
1013                "two": builder.buildValue({"XPlacement": 777}),
1014            },
1015            self.GLYPHMAP,
1016        )
1017        assert getXML(subtable.toXML) == [
1018            '<SinglePos Format="1">',
1019            "  <Coverage>",
1020            '    <Glyph value="one"/>',
1021            '    <Glyph value="two"/>',
1022            "  </Coverage>",
1023            '  <ValueFormat value="1"/>',
1024            '  <Value XPlacement="777"/>',
1025            "</SinglePos>",
1026        ]
1027
1028    def test_buildSinglePosSubtable_format2(self):
1029        subtable = builder.buildSinglePosSubtable(
1030            {
1031                "one": builder.buildValue({"XPlacement": 777}),
1032                "two": builder.buildValue({"YPlacement": -888}),
1033            },
1034            self.GLYPHMAP,
1035        )
1036        assert getXML(subtable.toXML) == [
1037            '<SinglePos Format="2">',
1038            "  <Coverage>",
1039            '    <Glyph value="one"/>',
1040            '    <Glyph value="two"/>',
1041            "  </Coverage>",
1042            '  <ValueFormat value="3"/>',
1043            "  <!-- ValueCount=2 -->",
1044            '  <Value index="0" XPlacement="777" YPlacement="0"/>',
1045            '  <Value index="1" XPlacement="0" YPlacement="-888"/>',
1046            "</SinglePos>",
1047        ]
1048
1049    def test_buildValue(self):
1050        value = builder.buildValue({"XPlacement": 7, "YPlacement": 23})
1051        func = lambda writer, font: value.toXML(writer, font, valueName="Val")
1052        assert getXML(func) == ['<Val XPlacement="7" YPlacement="23"/>']
1053
1054    def test_getLigatureSortKey(self):
1055        components = lambda s: [tuple(word) for word in s.split()]
1056        c = components("fi fl ff ffi fff")
1057        c.sort(key=otTables.LigatureSubst._getLigatureSortKey)
1058        assert c == components("ffi fff fi fl ff")
1059
1060    def test_getSinglePosValueKey(self):
1061        device = builder.buildDevice({10: 1, 11: 3})
1062        a1 = builder.buildValue({"XPlacement": 500, "XPlaDevice": device})
1063        a2 = builder.buildValue({"XPlacement": 500, "XPlaDevice": device})
1064        b = builder.buildValue({"XPlacement": 500})
1065        keyA1 = builder._getSinglePosValueKey(a1)
1066        keyA2 = builder._getSinglePosValueKey(a1)
1067        keyB = builder._getSinglePosValueKey(b)
1068        assert keyA1 == keyA2
1069        assert hash(keyA1) == hash(keyA2)
1070        assert keyA1 != keyB
1071        assert hash(keyA1) != hash(keyB)
1072
1073
1074class ClassDefBuilderTest(object):
1075    def test_build_usingClass0(self):
1076        b = builder.ClassDefBuilder(useClass0=True)
1077        b.add({"aa", "bb"})
1078        b.add({"a", "b"})
1079        b.add({"c"})
1080        b.add({"e", "f", "g", "h"})
1081        cdef = b.build()
1082        assert isinstance(cdef, otTables.ClassDef)
1083        assert cdef.classDefs == {"a": 1, "b": 1, "c": 3, "aa": 2, "bb": 2}
1084
1085    def test_build_notUsingClass0(self):
1086        b = builder.ClassDefBuilder(useClass0=False)
1087        b.add({"a", "b"})
1088        b.add({"c"})
1089        b.add({"e", "f", "g", "h"})
1090        cdef = b.build()
1091        assert isinstance(cdef, otTables.ClassDef)
1092        assert cdef.classDefs == {
1093            "a": 2,
1094            "b": 2,
1095            "c": 3,
1096            "e": 1,
1097            "f": 1,
1098            "g": 1,
1099            "h": 1,
1100        }
1101
1102    def test_canAdd(self):
1103        b = builder.ClassDefBuilder(useClass0=True)
1104        b.add({"a", "b", "c", "d"})
1105        b.add({"e", "f"})
1106        assert b.canAdd({"a", "b", "c", "d"})
1107        assert b.canAdd({"e", "f"})
1108        assert b.canAdd({"g", "h", "i"})
1109        assert not b.canAdd({"b", "c", "d"})
1110        assert not b.canAdd({"a", "b", "c", "d", "e", "f"})
1111        assert not b.canAdd({"d", "e", "f"})
1112        assert not b.canAdd({"f"})
1113
1114    def test_add_exception(self):
1115        b = builder.ClassDefBuilder(useClass0=True)
1116        b.add({"a", "b", "c"})
1117        with pytest.raises(error.OpenTypeLibError):
1118            b.add({"a", "d"})
1119
1120
1121buildStatTable_test_data = [
1122    (
1123        [
1124            dict(
1125                tag="wght",
1126                name="Weight",
1127                values=[
1128                    dict(value=100, name="Thin"),
1129                    dict(value=400, name="Regular", flags=0x2),
1130                    dict(value=900, name="Black"),
1131                ],
1132            )
1133        ],
1134        None,
1135        "Regular",
1136        [
1137            "  <STAT>",
1138            '    <Version value="0x00010001"/>',
1139            '    <DesignAxisRecordSize value="8"/>',
1140            "    <!-- DesignAxisCount=1 -->",
1141            "    <DesignAxisRecord>",
1142            '      <Axis index="0">',
1143            '        <AxisTag value="wght"/>',
1144            '        <AxisNameID value="257"/>  <!-- Weight -->',
1145            '        <AxisOrdering value="0"/>',
1146            "      </Axis>",
1147            "    </DesignAxisRecord>",
1148            "    <!-- AxisValueCount=3 -->",
1149            "    <AxisValueArray>",
1150            '      <AxisValue index="0" Format="1">',
1151            '        <AxisIndex value="0"/>',
1152            '        <Flags value="0"/>',
1153            '        <ValueNameID value="258"/>  <!-- Thin -->',
1154            '        <Value value="100.0"/>',
1155            "      </AxisValue>",
1156            '      <AxisValue index="1" Format="1">',
1157            '        <AxisIndex value="0"/>',
1158            '        <Flags value="2"/>  <!-- ElidableAxisValueName -->',
1159            '        <ValueNameID value="256"/>  <!-- Regular -->',
1160            '        <Value value="400.0"/>',
1161            "      </AxisValue>",
1162            '      <AxisValue index="2" Format="1">',
1163            '        <AxisIndex value="0"/>',
1164            '        <Flags value="0"/>',
1165            '        <ValueNameID value="259"/>  <!-- Black -->',
1166            '        <Value value="900.0"/>',
1167            "      </AxisValue>",
1168            "    </AxisValueArray>",
1169            '    <ElidedFallbackNameID value="256"/>  <!-- Regular -->',
1170            "  </STAT>",
1171        ],
1172    ),
1173    (
1174        [
1175            dict(
1176                tag="wght",
1177                name=dict(en="Weight", nl="Gewicht"),
1178                values=[
1179                    dict(value=100, name=dict(en="Thin", nl="Dun")),
1180                    dict(value=400, name="Regular", flags=0x2),
1181                    dict(value=900, name="Black"),
1182                ],
1183            ),
1184            dict(
1185                tag="wdth",
1186                name="Width",
1187                values=[
1188                    dict(value=50, name="Condensed"),
1189                    dict(value=100, name="Regular", flags=0x2),
1190                    dict(value=200, name="Extended"),
1191                ],
1192            ),
1193        ],
1194        None,
1195        2,
1196        [
1197            "  <STAT>",
1198            '    <Version value="0x00010001"/>',
1199            '    <DesignAxisRecordSize value="8"/>',
1200            "    <!-- DesignAxisCount=2 -->",
1201            "    <DesignAxisRecord>",
1202            '      <Axis index="0">',
1203            '        <AxisTag value="wght"/>',
1204            '        <AxisNameID value="256"/>  <!-- Weight -->',
1205            '        <AxisOrdering value="0"/>',
1206            "      </Axis>",
1207            '      <Axis index="1">',
1208            '        <AxisTag value="wdth"/>',
1209            '        <AxisNameID value="260"/>  <!-- Width -->',
1210            '        <AxisOrdering value="1"/>',
1211            "      </Axis>",
1212            "    </DesignAxisRecord>",
1213            "    <!-- AxisValueCount=6 -->",
1214            "    <AxisValueArray>",
1215            '      <AxisValue index="0" Format="1">',
1216            '        <AxisIndex value="0"/>',
1217            '        <Flags value="0"/>',
1218            '        <ValueNameID value="257"/>  <!-- Thin -->',
1219            '        <Value value="100.0"/>',
1220            "      </AxisValue>",
1221            '      <AxisValue index="1" Format="1">',
1222            '        <AxisIndex value="0"/>',
1223            '        <Flags value="2"/>  <!-- ElidableAxisValueName -->',
1224            '        <ValueNameID value="258"/>  <!-- Regular -->',
1225            '        <Value value="400.0"/>',
1226            "      </AxisValue>",
1227            '      <AxisValue index="2" Format="1">',
1228            '        <AxisIndex value="0"/>',
1229            '        <Flags value="0"/>',
1230            '        <ValueNameID value="259"/>  <!-- Black -->',
1231            '        <Value value="900.0"/>',
1232            "      </AxisValue>",
1233            '      <AxisValue index="3" Format="1">',
1234            '        <AxisIndex value="1"/>',
1235            '        <Flags value="0"/>',
1236            '        <ValueNameID value="261"/>  <!-- Condensed -->',
1237            '        <Value value="50.0"/>',
1238            "      </AxisValue>",
1239            '      <AxisValue index="4" Format="1">',
1240            '        <AxisIndex value="1"/>',
1241            '        <Flags value="2"/>  <!-- ElidableAxisValueName -->',
1242            '        <ValueNameID value="258"/>  <!-- Regular -->',
1243            '        <Value value="100.0"/>',
1244            "      </AxisValue>",
1245            '      <AxisValue index="5" Format="1">',
1246            '        <AxisIndex value="1"/>',
1247            '        <Flags value="0"/>',
1248            '        <ValueNameID value="262"/>  <!-- Extended -->',
1249            '        <Value value="200.0"/>',
1250            "      </AxisValue>",
1251            "    </AxisValueArray>",
1252            '    <ElidedFallbackNameID value="2"/>  <!-- missing from name table -->',
1253            "  </STAT>",
1254        ],
1255    ),
1256    (
1257        [
1258            dict(
1259                tag="wght",
1260                name="Weight",
1261                values=[
1262                    dict(value=400, name="Regular", flags=0x2),
1263                    dict(value=600, linkedValue=650, name="Bold"),
1264                ],
1265            )
1266        ],
1267        None,
1268        18,
1269        [
1270            "  <STAT>",
1271            '    <Version value="0x00010001"/>',
1272            '    <DesignAxisRecordSize value="8"/>',
1273            "    <!-- DesignAxisCount=1 -->",
1274            "    <DesignAxisRecord>",
1275            '      <Axis index="0">',
1276            '        <AxisTag value="wght"/>',
1277            '        <AxisNameID value="256"/>  <!-- Weight -->',
1278            '        <AxisOrdering value="0"/>',
1279            "      </Axis>",
1280            "    </DesignAxisRecord>",
1281            "    <!-- AxisValueCount=2 -->",
1282            "    <AxisValueArray>",
1283            '      <AxisValue index="0" Format="1">',
1284            '        <AxisIndex value="0"/>',
1285            '        <Flags value="2"/>  <!-- ElidableAxisValueName -->',
1286            '        <ValueNameID value="257"/>  <!-- Regular -->',
1287            '        <Value value="400.0"/>',
1288            "      </AxisValue>",
1289            '      <AxisValue index="1" Format="3">',
1290            '        <AxisIndex value="0"/>',
1291            '        <Flags value="0"/>',
1292            '        <ValueNameID value="258"/>  <!-- Bold -->',
1293            '        <Value value="600.0"/>',
1294            '        <LinkedValue value="650.0"/>',
1295            "      </AxisValue>",
1296            "    </AxisValueArray>",
1297            '    <ElidedFallbackNameID value="18"/>  <!-- missing from name table -->',
1298            "  </STAT>",
1299        ],
1300    ),
1301    (
1302        [
1303            dict(
1304                tag="opsz",
1305                name="Optical Size",
1306                values=[
1307                    dict(nominalValue=6, rangeMaxValue=10, name="Small"),
1308                    dict(
1309                        rangeMinValue=10,
1310                        nominalValue=14,
1311                        rangeMaxValue=24,
1312                        name="Text",
1313                        flags=0x2,
1314                    ),
1315                    dict(rangeMinValue=24, nominalValue=600, name="Display"),
1316                ],
1317            )
1318        ],
1319        None,
1320        2,
1321        [
1322            "  <STAT>",
1323            '    <Version value="0x00010001"/>',
1324            '    <DesignAxisRecordSize value="8"/>',
1325            "    <!-- DesignAxisCount=1 -->",
1326            "    <DesignAxisRecord>",
1327            '      <Axis index="0">',
1328            '        <AxisTag value="opsz"/>',
1329            '        <AxisNameID value="256"/>  <!-- Optical Size -->',
1330            '        <AxisOrdering value="0"/>',
1331            "      </Axis>",
1332            "    </DesignAxisRecord>",
1333            "    <!-- AxisValueCount=3 -->",
1334            "    <AxisValueArray>",
1335            '      <AxisValue index="0" Format="2">',
1336            '        <AxisIndex value="0"/>',
1337            '        <Flags value="0"/>',
1338            '        <ValueNameID value="257"/>  <!-- Small -->',
1339            '        <NominalValue value="6.0"/>',
1340            '        <RangeMinValue value="-32768.0"/>',
1341            '        <RangeMaxValue value="10.0"/>',
1342            "      </AxisValue>",
1343            '      <AxisValue index="1" Format="2">',
1344            '        <AxisIndex value="0"/>',
1345            '        <Flags value="2"/>  <!-- ElidableAxisValueName -->',
1346            '        <ValueNameID value="258"/>  <!-- Text -->',
1347            '        <NominalValue value="14.0"/>',
1348            '        <RangeMinValue value="10.0"/>',
1349            '        <RangeMaxValue value="24.0"/>',
1350            "      </AxisValue>",
1351            '      <AxisValue index="2" Format="2">',
1352            '        <AxisIndex value="0"/>',
1353            '        <Flags value="0"/>',
1354            '        <ValueNameID value="259"/>  <!-- Display -->',
1355            '        <NominalValue value="600.0"/>',
1356            '        <RangeMinValue value="24.0"/>',
1357            '        <RangeMaxValue value="32767.99998"/>',
1358            "      </AxisValue>",
1359            "    </AxisValueArray>",
1360            '    <ElidedFallbackNameID value="2"/>  <!-- missing from name table -->',
1361            "  </STAT>",
1362        ],
1363    ),
1364    (
1365        [
1366            dict(tag="wght", name="Weight", ordering=1, values=[]),
1367            dict(
1368                tag="ABCD",
1369                name="ABCDTest",
1370                ordering=0,
1371                values=[dict(value=100, name="Regular", flags=0x2)],
1372            ),
1373        ],
1374        [dict(location=dict(wght=300, ABCD=100), name="Regular ABCD")],
1375        18,
1376        [
1377            "  <STAT>",
1378            '    <Version value="0x00010002"/>',
1379            '    <DesignAxisRecordSize value="8"/>',
1380            "    <!-- DesignAxisCount=2 -->",
1381            "    <DesignAxisRecord>",
1382            '      <Axis index="0">',
1383            '        <AxisTag value="wght"/>',
1384            '        <AxisNameID value="256"/>  <!-- Weight -->',
1385            '        <AxisOrdering value="1"/>',
1386            "      </Axis>",
1387            '      <Axis index="1">',
1388            '        <AxisTag value="ABCD"/>',
1389            '        <AxisNameID value="257"/>  <!-- ABCDTest -->',
1390            '        <AxisOrdering value="0"/>',
1391            "      </Axis>",
1392            "    </DesignAxisRecord>",
1393            "    <!-- AxisValueCount=2 -->",
1394            "    <AxisValueArray>",
1395            '      <AxisValue index="0" Format="4">',
1396            "        <!-- AxisCount=2 -->",
1397            '        <Flags value="0"/>',
1398            '        <ValueNameID value="259"/>  <!-- Regular ABCD -->',
1399            '        <AxisValueRecord index="0">',
1400            '          <AxisIndex value="0"/>',
1401            '          <Value value="300.0"/>',
1402            "        </AxisValueRecord>",
1403            '        <AxisValueRecord index="1">',
1404            '          <AxisIndex value="1"/>',
1405            '          <Value value="100.0"/>',
1406            "        </AxisValueRecord>",
1407            "      </AxisValue>",
1408            '      <AxisValue index="1" Format="1">',
1409            '        <AxisIndex value="1"/>',
1410            '        <Flags value="2"/>  <!-- ElidableAxisValueName -->',
1411            '        <ValueNameID value="258"/>  <!-- Regular -->',
1412            '        <Value value="100.0"/>',
1413            "      </AxisValue>",
1414            "    </AxisValueArray>",
1415            '    <ElidedFallbackNameID value="18"/>  <!-- missing from name table -->',
1416            "  </STAT>",
1417        ],
1418    ),
1419]
1420
1421
1422@pytest.mark.parametrize(
1423    "axes, axisValues, elidedFallbackName, expected_ttx", buildStatTable_test_data
1424)
1425def test_buildStatTable(axes, axisValues, elidedFallbackName, expected_ttx):
1426    font = ttLib.TTFont()
1427    font["name"] = ttLib.newTable("name")
1428    font["name"].names = []
1429    # https://github.com/fonttools/fonttools/issues/1985
1430    # Add nameID < 256 that matches a test axis name, to test whether
1431    # the nameID is not reused: AxisNameIDs must be > 255 according
1432    # to the spec.
1433    font["name"].addMultilingualName(dict(en="ABCDTest"), nameID=6)
1434    builder.buildStatTable(font, axes, axisValues, elidedFallbackName)
1435    f = io.StringIO()
1436    font.saveXML(f, tables=["STAT"])
1437    ttx = f.getvalue().splitlines()
1438    ttx = ttx[3:-2]  # strip XML header and <ttFont> element
1439    assert expected_ttx == ttx
1440    # Compile and round-trip
1441    f = io.BytesIO()
1442    font.save(f)
1443    font = ttLib.TTFont(f)
1444    f = io.StringIO()
1445    font.saveXML(f, tables=["STAT"])
1446    ttx = f.getvalue().splitlines()
1447    ttx = ttx[3:-2]  # strip XML header and <ttFont> element
1448    assert expected_ttx == ttx
1449
1450
1451def test_buildStatTable_platform_specific_names():
1452    # PR: https://github.com/fonttools/fonttools/pull/2528
1453    # Introduce new 'platform' feature for creating a STAT table.
1454    # Set windowsNames and or macNames to create name table entries
1455    # in the specified platforms
1456    font_obj = ttLib.TTFont()
1457    font_obj["name"] = ttLib.newTable("name")
1458    font_obj["name"].names = []
1459
1460    wght_values = [
1461        dict(nominalValue=200, rangeMinValue=200, rangeMaxValue=250, name="ExtraLight"),
1462        dict(nominalValue=300, rangeMinValue=250, rangeMaxValue=350, name="Light"),
1463        dict(
1464            nominalValue=400,
1465            rangeMinValue=350,
1466            rangeMaxValue=450,
1467            name="Regular",
1468            flags=0x2,
1469        ),
1470        dict(nominalValue=500, rangeMinValue=450, rangeMaxValue=650, name="Medium"),
1471        dict(nominalValue=700, rangeMinValue=650, rangeMaxValue=750, name="Bold"),
1472        dict(nominalValue=800, rangeMinValue=750, rangeMaxValue=850, name="ExtraBold"),
1473        dict(nominalValue=900, rangeMinValue=850, rangeMaxValue=900, name="Black"),
1474    ]
1475
1476    AXES = [
1477        dict(
1478            tag="wght",
1479            name="Weight",
1480            ordering=1,
1481            values=wght_values,
1482        ),
1483    ]
1484
1485    font_obj["name"].setName("ExtraLight", 260, 3, 1, 0x409)
1486    font_obj["name"].setName("Light", 261, 3, 1, 0x409)
1487    font_obj["name"].setName("Regular", 262, 3, 1, 0x409)
1488    font_obj["name"].setName("Medium", 263, 3, 1, 0x409)
1489    font_obj["name"].setName("Bold", 264, 3, 1, 0x409)
1490    font_obj["name"].setName("ExtraBold", 265, 3, 1, 0x409)
1491    font_obj["name"].setName("Black", 266, 3, 1, 0x409)
1492
1493    font_obj["name"].setName("Weight", 270, 3, 1, 0x409)
1494
1495    expected_names = [x.string for x in font_obj["name"].names]
1496
1497    builder.buildStatTable(font_obj, AXES, windowsNames=True, macNames=False)
1498    actual_names = [x.string for x in font_obj["name"].names]
1499
1500    # no new name records were added by buildStatTable
1501    # because windows-only names with the same strings were already present
1502    assert expected_names == actual_names
1503
1504    font_obj["name"].removeNames(nameID=270)
1505    expected_names = [x.string for x in font_obj["name"].names] + ["Weight"]
1506
1507    builder.buildStatTable(font_obj, AXES, windowsNames=True, macNames=False)
1508    actual_names = [x.string for x in font_obj["name"].names]
1509    # One new name records 'Weight' were added by buildStatTable
1510    assert expected_names == actual_names
1511
1512    builder.buildStatTable(font_obj, AXES, windowsNames=True, macNames=True)
1513    actual_names = [x.string for x in font_obj["name"].names]
1514    expected_names = [
1515        "Weight",
1516        "Weight",
1517        "Weight",
1518        "ExtraLight",
1519        "ExtraLight",
1520        "ExtraLight",
1521        "Light",
1522        "Light",
1523        "Light",
1524        "Regular",
1525        "Regular",
1526        "Regular",
1527        "Medium",
1528        "Medium",
1529        "Medium",
1530        "Bold",
1531        "Bold",
1532        "Bold",
1533        "ExtraBold",
1534        "ExtraBold",
1535        "ExtraBold",
1536        "Black",
1537        "Black",
1538        "Black",
1539    ]
1540    # Because there is an inconsistency in the names add new name IDs
1541    # for each platform -> windowsNames=True, macNames=True
1542    assert sorted(expected_names) == sorted(actual_names)
1543
1544
1545def test_stat_infinities():
1546    negInf = floatToFixed(builder.AXIS_VALUE_NEGATIVE_INFINITY, 16)
1547    assert struct.pack(">l", negInf) == b"\x80\x00\x00\x00"
1548    posInf = floatToFixed(builder.AXIS_VALUE_POSITIVE_INFINITY, 16)
1549    assert struct.pack(">l", posInf) == b"\x7f\xff\xff\xff"
1550
1551
1552def test_buildMathTable_empty():
1553    ttFont = ttLib.TTFont()
1554    ttFont.setGlyphOrder([])
1555    builder.buildMathTable(ttFont)
1556
1557    assert "MATH" in ttFont
1558    mathTable = ttFont["MATH"].table
1559    assert mathTable.Version == 0x00010000
1560
1561    assert mathTable.MathConstants is None
1562    assert mathTable.MathGlyphInfo is None
1563    assert mathTable.MathVariants is None
1564
1565
1566def test_buildMathTable_constants():
1567    ttFont = ttLib.TTFont()
1568    ttFont.setGlyphOrder([])
1569    constants = {
1570        "AccentBaseHeight": 516,
1571        "AxisHeight": 262,
1572        "DelimitedSubFormulaMinHeight": 1500,
1573        "DisplayOperatorMinHeight": 2339,
1574        "FlattenedAccentBaseHeight": 698,
1575        "FractionDenomDisplayStyleGapMin": 198,
1576        "FractionDenominatorDisplayStyleShiftDown": 698,
1577        "FractionDenominatorGapMin": 66,
1578        "FractionDenominatorShiftDown": 465,
1579        "FractionNumDisplayStyleGapMin": 198,
1580        "FractionNumeratorDisplayStyleShiftUp": 774,
1581        "FractionNumeratorGapMin": 66,
1582        "FractionNumeratorShiftUp": 516,
1583        "FractionRuleThickness": 66,
1584        "LowerLimitBaselineDropMin": 585,
1585        "LowerLimitGapMin": 132,
1586        "MathLeading": 300,
1587        "OverbarExtraAscender": 66,
1588        "OverbarRuleThickness": 66,
1589        "OverbarVerticalGap": 198,
1590        "RadicalDegreeBottomRaisePercent": 75,
1591        "RadicalDisplayStyleVerticalGap": 195,
1592        "RadicalExtraAscender": 66,
1593        "RadicalKernAfterDegree": -556,
1594        "RadicalKernBeforeDegree": 278,
1595        "RadicalRuleThickness": 66,
1596        "RadicalVerticalGap": 82,
1597        "ScriptPercentScaleDown": 70,
1598        "ScriptScriptPercentScaleDown": 55,
1599        "SkewedFractionHorizontalGap": 66,
1600        "SkewedFractionVerticalGap": 77,
1601        "SpaceAfterScript": 42,
1602        "StackBottomDisplayStyleShiftDown": 698,
1603        "StackBottomShiftDown": 465,
1604        "StackDisplayStyleGapMin": 462,
1605        "StackGapMin": 198,
1606        "StackTopDisplayStyleShiftUp": 774,
1607        "StackTopShiftUp": 516,
1608        "StretchStackBottomShiftDown": 585,
1609        "StretchStackGapAboveMin": 132,
1610        "StretchStackGapBelowMin": 132,
1611        "StretchStackTopShiftUp": 165,
1612        "SubSuperscriptGapMin": 264,
1613        "SubscriptBaselineDropMin": 105,
1614        "SubscriptShiftDown": 140,
1615        "SubscriptTopMax": 413,
1616        "SuperscriptBaselineDropMax": 221,
1617        "SuperscriptBottomMaxWithSubscript": 413,
1618        "SuperscriptBottomMin": 129,
1619        "SuperscriptShiftUp": 477,
1620        "SuperscriptShiftUpCramped": 358,
1621        "UnderbarExtraDescender": 66,
1622        "UnderbarRuleThickness": 66,
1623        "UnderbarVerticalGap": 198,
1624        "UpperLimitBaselineRiseMin": 165,
1625        "UpperLimitGapMin": 132,
1626    }
1627    builder.buildMathTable(ttFont, constants=constants)
1628    mathTable = ttFont["MATH"].table
1629    assert mathTable.MathConstants
1630    assert mathTable.MathGlyphInfo is None
1631    assert mathTable.MathVariants is None
1632    for k, v in constants.items():
1633        r = getattr(mathTable.MathConstants, k)
1634        try:
1635            r = r.Value
1636        except AttributeError:
1637            pass
1638        assert r == v
1639
1640
1641def test_buildMathTable_italicsCorrection():
1642    ttFont = ttLib.TTFont()
1643    ttFont.setGlyphOrder(["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"])
1644    italicsCorrections = {"A": 100, "C": 300, "D": 400, "E": 500}
1645    builder.buildMathTable(ttFont, italicsCorrections=italicsCorrections)
1646    mathTable = ttFont["MATH"].table
1647    assert mathTable.MathConstants is None
1648    assert mathTable.MathGlyphInfo
1649    assert mathTable.MathVariants is None
1650    assert set(
1651        mathTable.MathGlyphInfo.MathItalicsCorrectionInfo.Coverage.glyphs
1652    ) == set(italicsCorrections.keys())
1653    for glyph, correction in zip(
1654        mathTable.MathGlyphInfo.MathItalicsCorrectionInfo.Coverage.glyphs,
1655        mathTable.MathGlyphInfo.MathItalicsCorrectionInfo.ItalicsCorrection,
1656    ):
1657        assert correction.Value == italicsCorrections[glyph]
1658
1659
1660def test_buildMathTable_topAccentAttachment():
1661    ttFont = ttLib.TTFont()
1662    ttFont.setGlyphOrder(["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"])
1663    topAccentAttachments = {"A": 10, "B": 20, "C": 30, "E": 50}
1664    builder.buildMathTable(ttFont, topAccentAttachments=topAccentAttachments)
1665    mathTable = ttFont["MATH"].table
1666    assert mathTable.MathConstants is None
1667    assert mathTable.MathGlyphInfo
1668    assert mathTable.MathVariants is None
1669    assert set(
1670        mathTable.MathGlyphInfo.MathTopAccentAttachment.TopAccentCoverage.glyphs
1671    ) == set(topAccentAttachments.keys())
1672    for glyph, attachment in zip(
1673        mathTable.MathGlyphInfo.MathTopAccentAttachment.TopAccentCoverage.glyphs,
1674        mathTable.MathGlyphInfo.MathTopAccentAttachment.TopAccentAttachment,
1675    ):
1676        assert attachment.Value == topAccentAttachments[glyph]
1677
1678
1679def test_buildMathTable_extendedShape():
1680    ttFont = ttLib.TTFont()
1681    ttFont.setGlyphOrder(["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"])
1682    extendedShapes = {"A", "C", "E", "F"}
1683    builder.buildMathTable(ttFont, extendedShapes=extendedShapes)
1684    mathTable = ttFont["MATH"].table
1685    assert mathTable.MathConstants is None
1686    assert mathTable.MathGlyphInfo
1687    assert mathTable.MathVariants is None
1688    assert set(mathTable.MathGlyphInfo.ExtendedShapeCoverage.glyphs) == extendedShapes
1689
1690
1691def test_buildMathTable_mathKern():
1692    ttFont = ttLib.TTFont()
1693    ttFont.setGlyphOrder(["A", "B"])
1694    mathKerns = {
1695        "A": {
1696            "TopRight": ([10, 20], [10, 20, 30]),
1697            "BottomRight": ([], [10]),
1698            "TopLeft": ([10], [0, 20]),
1699            "BottomLeft": ([-10, 0], [0, 10, 20]),
1700        },
1701    }
1702    builder.buildMathTable(ttFont, mathKerns=mathKerns)
1703    mathTable = ttFont["MATH"].table
1704    assert mathTable.MathConstants is None
1705    assert mathTable.MathGlyphInfo
1706    assert mathTable.MathVariants is None
1707    assert set(mathTable.MathGlyphInfo.MathKernInfo.MathKernCoverage.glyphs) == set(
1708        mathKerns.keys()
1709    )
1710    for glyph, record in zip(
1711        mathTable.MathGlyphInfo.MathKernInfo.MathKernCoverage.glyphs,
1712        mathTable.MathGlyphInfo.MathKernInfo.MathKernInfoRecords,
1713    ):
1714        h, k = mathKerns[glyph]["TopRight"]
1715        assert [v.Value for v in record.TopRightMathKern.CorrectionHeight] == h
1716        assert [v.Value for v in record.TopRightMathKern.KernValue] == k
1717        h, k = mathKerns[glyph]["BottomRight"]
1718        assert [v.Value for v in record.BottomRightMathKern.CorrectionHeight] == h
1719        assert [v.Value for v in record.BottomRightMathKern.KernValue] == k
1720        h, k = mathKerns[glyph]["TopLeft"]
1721        assert [v.Value for v in record.TopLeftMathKern.CorrectionHeight] == h
1722        assert [v.Value for v in record.TopLeftMathKern.KernValue] == k
1723        h, k = mathKerns[glyph]["BottomLeft"]
1724        assert [v.Value for v in record.BottomLeftMathKern.CorrectionHeight] == h
1725        assert [v.Value for v in record.BottomLeftMathKern.KernValue] == k
1726
1727
1728def test_buildMathTable_vertVariants():
1729    ttFont = ttLib.TTFont()
1730    ttFont.setGlyphOrder(["A", "A.size1", "A.size2"])
1731    vertGlyphVariants = {"A": [("A.size1", 100), ("A.size2", 200)]}
1732    builder.buildMathTable(ttFont, vertGlyphVariants=vertGlyphVariants)
1733    mathTable = ttFont["MATH"].table
1734    assert mathTable.MathConstants is None
1735    assert mathTable.MathGlyphInfo is None
1736    assert mathTable.MathVariants
1737    assert set(mathTable.MathVariants.VertGlyphCoverage.glyphs) == set(
1738        vertGlyphVariants.keys()
1739    )
1740    for glyph, construction in zip(
1741        mathTable.MathVariants.VertGlyphCoverage.glyphs,
1742        mathTable.MathVariants.VertGlyphConstruction,
1743    ):
1744        assert [
1745            (r.VariantGlyph, r.AdvanceMeasurement)
1746            for r in construction.MathGlyphVariantRecord
1747        ] == vertGlyphVariants[glyph]
1748
1749
1750def test_buildMathTable_horizVariants():
1751    ttFont = ttLib.TTFont()
1752    ttFont.setGlyphOrder(["A", "A.size1", "A.size2"])
1753    horizGlyphVariants = {"A": [("A.size1", 100), ("A.size2", 200)]}
1754    builder.buildMathTable(ttFont, horizGlyphVariants=horizGlyphVariants)
1755    mathTable = ttFont["MATH"].table
1756    assert mathTable.MathConstants is None
1757    assert mathTable.MathGlyphInfo is None
1758    assert mathTable.MathVariants
1759    assert set(mathTable.MathVariants.HorizGlyphCoverage.glyphs) == set(
1760        horizGlyphVariants.keys()
1761    )
1762    for glyph, construction in zip(
1763        mathTable.MathVariants.HorizGlyphCoverage.glyphs,
1764        mathTable.MathVariants.HorizGlyphConstruction,
1765    ):
1766        assert [
1767            (r.VariantGlyph, r.AdvanceMeasurement)
1768            for r in construction.MathGlyphVariantRecord
1769        ] == horizGlyphVariants[glyph]
1770
1771
1772def test_buildMathTable_vertAssembly():
1773    ttFont = ttLib.TTFont()
1774    ttFont.setGlyphOrder(["A", "A.top", "A.middle", "A.bottom", "A.extender"])
1775    vertGlyphAssembly = {
1776        "A": [
1777            [
1778                ("A.bottom", 0, 0, 100, 200),
1779                ("A.extender", 1, 50, 50, 100),
1780                ("A.middle", 0, 100, 100, 200),
1781                ("A.extender", 1, 50, 50, 100),
1782                ("A.top", 0, 100, 0, 200),
1783            ],
1784            10,
1785        ],
1786    }
1787    builder.buildMathTable(ttFont, vertGlyphAssembly=vertGlyphAssembly)
1788    mathTable = ttFont["MATH"].table
1789    assert mathTable.MathConstants is None
1790    assert mathTable.MathGlyphInfo is None
1791    assert mathTable.MathVariants
1792    assert set(mathTable.MathVariants.VertGlyphCoverage.glyphs) == set(
1793        vertGlyphAssembly.keys()
1794    )
1795    for glyph, construction in zip(
1796        mathTable.MathVariants.VertGlyphCoverage.glyphs,
1797        mathTable.MathVariants.VertGlyphConstruction,
1798    ):
1799        assert [
1800            [
1801                (
1802                    r.glyph,
1803                    r.PartFlags,
1804                    r.StartConnectorLength,
1805                    r.EndConnectorLength,
1806                    r.FullAdvance,
1807                )
1808                for r in construction.GlyphAssembly.PartRecords
1809            ],
1810            construction.GlyphAssembly.ItalicsCorrection.Value,
1811        ] == vertGlyphAssembly[glyph]
1812
1813
1814def test_buildMathTable_horizAssembly():
1815    ttFont = ttLib.TTFont()
1816    ttFont.setGlyphOrder(["A", "A.top", "A.middle", "A.bottom", "A.extender"])
1817    horizGlyphAssembly = {
1818        "A": [
1819            [
1820                ("A.bottom", 0, 0, 100, 200),
1821                ("A.extender", 1, 50, 50, 100),
1822                ("A.middle", 0, 100, 100, 200),
1823                ("A.extender", 1, 50, 50, 100),
1824                ("A.top", 0, 100, 0, 200),
1825            ],
1826            10,
1827        ],
1828    }
1829    builder.buildMathTable(ttFont, horizGlyphAssembly=horizGlyphAssembly)
1830    mathTable = ttFont["MATH"].table
1831    assert mathTable.MathConstants is None
1832    assert mathTable.MathGlyphInfo is None
1833    assert mathTable.MathVariants
1834    assert set(mathTable.MathVariants.HorizGlyphCoverage.glyphs) == set(
1835        horizGlyphAssembly.keys()
1836    )
1837    for glyph, construction in zip(
1838        mathTable.MathVariants.HorizGlyphCoverage.glyphs,
1839        mathTable.MathVariants.HorizGlyphConstruction,
1840    ):
1841        assert [
1842            [
1843                (
1844                    r.glyph,
1845                    r.PartFlags,
1846                    r.StartConnectorLength,
1847                    r.EndConnectorLength,
1848                    r.FullAdvance,
1849                )
1850                for r in construction.GlyphAssembly.PartRecords
1851            ],
1852            construction.GlyphAssembly.ItalicsCorrection.Value,
1853        ] == horizGlyphAssembly[glyph]
1854
1855
1856class ChainContextualRulesetTest(object):
1857    def test_makeRulesets(self):
1858        font = ttLib.TTFont()
1859        font.setGlyphOrder(["a", "b", "c", "d", "A", "B", "C", "D", "E"])
1860        sb = builder.ChainContextSubstBuilder(font, None)
1861        prefix, input_, suffix, lookups = [["a"], ["b"]], [["c"]], [], [None]
1862        sb.rules.append(builder.ChainContextualRule(prefix, input_, suffix, lookups))
1863
1864        prefix, input_, suffix, lookups = [["a"], ["d"]], [["c"]], [], [None]
1865        sb.rules.append(builder.ChainContextualRule(prefix, input_, suffix, lookups))
1866
1867        sb.add_subtable_break(None)
1868
1869        # Second subtable has some glyph classes
1870        prefix, input_, suffix, lookups = [["A"]], [["E"]], [], [None]
1871        sb.rules.append(builder.ChainContextualRule(prefix, input_, suffix, lookups))
1872        prefix, input_, suffix, lookups = [["A"]], [["C", "D"]], [], [None]
1873        sb.rules.append(builder.ChainContextualRule(prefix, input_, suffix, lookups))
1874        prefix, input_, suffix, lookups = [["A", "B"]], [["E"]], [], [None]
1875        sb.rules.append(builder.ChainContextualRule(prefix, input_, suffix, lookups))
1876
1877        sb.add_subtable_break(None)
1878
1879        # Third subtable has no pre/post context
1880        prefix, input_, suffix, lookups = [], [["E"]], [], [None]
1881        sb.rules.append(builder.ChainContextualRule(prefix, input_, suffix, lookups))
1882        prefix, input_, suffix, lookups = [], [["C", "D"]], [], [None]
1883        sb.rules.append(builder.ChainContextualRule(prefix, input_, suffix, lookups))
1884
1885        rulesets = sb.rulesets()
1886        assert len(rulesets) == 3
1887        assert rulesets[0].hasPrefixOrSuffix
1888        assert not rulesets[0].hasAnyGlyphClasses
1889        cd = rulesets[0].format2ClassDefs()
1890        assert set(cd[0].classes()[1:]) == set([("d",), ("b",), ("a",)])
1891        assert set(cd[1].classes()[1:]) == set([("c",)])
1892        assert set(cd[2].classes()[1:]) == set()
1893
1894        assert rulesets[1].hasPrefixOrSuffix
1895        assert rulesets[1].hasAnyGlyphClasses
1896        assert not rulesets[1].format2ClassDefs()
1897
1898        assert not rulesets[2].hasPrefixOrSuffix
1899        assert rulesets[2].hasAnyGlyphClasses
1900        assert rulesets[2].format2ClassDefs()
1901        cd = rulesets[2].format2ClassDefs()
1902        assert set(cd[0].classes()[1:]) == set()
1903        assert set(cd[1].classes()[1:]) == set([("C", "D"), ("E",)])
1904        assert set(cd[2].classes()[1:]) == set()
1905
1906
1907if __name__ == "__main__":
1908    import sys
1909
1910    sys.exit(pytest.main(sys.argv))
1911