1import io
2import struct
3from fontTools.misc.fixedTools import floatToFixed
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": 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            },
923            self.GLYPHMAP,
924        )
925
926        assert getXML(subtable.toXML) == [
927            '<PairPos Format="1">',
928            "  <Coverage>",
929            '    <Glyph value="A"/>',
930            '    <Glyph value="B"/>',
931            "  </Coverage>",
932            '  <ValueFormat1 value="3"/>',
933            '  <ValueFormat2 value="1"/>',
934            "  <!-- PairSetCount=2 -->",
935            '  <PairSet index="0">',
936            "    <!-- PairValueCount=2 -->",
937            '    <PairValueRecord index="0">',
938            '      <SecondGlyph value="zero"/>',
939            '      <Value1 XPlacement="0" YPlacement="0"/>',
940            '      <Value2 XPlacement="-50"/>',
941            "    </PairValueRecord>",
942            '    <PairValueRecord index="1">',
943            '      <SecondGlyph value="one"/>',
944            '      <Value1 XPlacement="0" YPlacement="0"/>',
945            '      <Value2 XPlacement="-20"/>',
946            "    </PairValueRecord>",
947            "  </PairSet>",
948            '  <PairSet index="1">',
949            "    <!-- PairValueCount=1 -->",
950            '    <PairValueRecord index="0">',
951            '      <SecondGlyph value="five"/>',
952            '      <Value1 XPlacement="-80" YPlacement="-20"/>',
953            '      <Value2 XPlacement="-50"/>',
954            "    </PairValueRecord>",
955            "  </PairSet>",
956            "</PairPos>",
957        ]
958
959    def test_buildSinglePos(self):
960        subtables = builder.buildSinglePos(
961            {
962                "one": builder.buildValue({"XPlacement": 500}),
963                "two": builder.buildValue({"XPlacement": 500}),
964                "three": builder.buildValue({"XPlacement": 200}),
965                "four": builder.buildValue({"XPlacement": 400}),
966                "five": builder.buildValue({"XPlacement": 500}),
967                "six": builder.buildValue({"YPlacement": -6}),
968            },
969            self.GLYPHMAP,
970        )
971        assert sum([getXML(t.toXML) for t in subtables], []) == [
972            '<SinglePos Format="2">',
973            "  <Coverage>",
974            '    <Glyph value="one"/>',
975            '    <Glyph value="two"/>',
976            '    <Glyph value="three"/>',
977            '    <Glyph value="four"/>',
978            '    <Glyph value="five"/>',
979            "  </Coverage>",
980            '  <ValueFormat value="1"/>',
981            "  <!-- ValueCount=5 -->",
982            '  <Value index="0" XPlacement="500"/>',
983            '  <Value index="1" XPlacement="500"/>',
984            '  <Value index="2" XPlacement="200"/>',
985            '  <Value index="3" XPlacement="400"/>',
986            '  <Value index="4" XPlacement="500"/>',
987            "</SinglePos>",
988            '<SinglePos Format="1">',
989            "  <Coverage>",
990            '    <Glyph value="six"/>',
991            "  </Coverage>",
992            '  <ValueFormat value="2"/>',
993            '  <Value YPlacement="-6"/>',
994            "</SinglePos>",
995        ]
996
997    def test_buildSinglePos_ValueFormat0(self):
998        subtables = builder.buildSinglePos(
999            {"zero": builder.buildValue({})}, self.GLYPHMAP
1000        )
1001        assert sum([getXML(t.toXML) for t in subtables], []) == [
1002            '<SinglePos Format="1">',
1003            "  <Coverage>",
1004            '    <Glyph value="zero"/>',
1005            "  </Coverage>",
1006            '  <ValueFormat value="0"/>',
1007            "</SinglePos>",
1008        ]
1009
1010    def test_buildSinglePosSubtable_format1(self):
1011        subtable = builder.buildSinglePosSubtable(
1012            {
1013                "one": builder.buildValue({"XPlacement": 777}),
1014                "two": builder.buildValue({"XPlacement": 777}),
1015            },
1016            self.GLYPHMAP,
1017        )
1018        assert getXML(subtable.toXML) == [
1019            '<SinglePos Format="1">',
1020            "  <Coverage>",
1021            '    <Glyph value="one"/>',
1022            '    <Glyph value="two"/>',
1023            "  </Coverage>",
1024            '  <ValueFormat value="1"/>',
1025            '  <Value XPlacement="777"/>',
1026            "</SinglePos>",
1027        ]
1028
1029    def test_buildSinglePosSubtable_format2(self):
1030        subtable = builder.buildSinglePosSubtable(
1031            {
1032                "one": builder.buildValue({"XPlacement": 777}),
1033                "two": builder.buildValue({"YPlacement": -888}),
1034            },
1035            self.GLYPHMAP,
1036        )
1037        assert getXML(subtable.toXML) == [
1038            '<SinglePos Format="2">',
1039            "  <Coverage>",
1040            '    <Glyph value="one"/>',
1041            '    <Glyph value="two"/>',
1042            "  </Coverage>",
1043            '  <ValueFormat value="3"/>',
1044            "  <!-- ValueCount=2 -->",
1045            '  <Value index="0" XPlacement="777" YPlacement="0"/>',
1046            '  <Value index="1" XPlacement="0" YPlacement="-888"/>',
1047            "</SinglePos>",
1048        ]
1049
1050    def test_buildValue(self):
1051        value = builder.buildValue({"XPlacement": 7, "YPlacement": 23})
1052        func = lambda writer, font: value.toXML(writer, font, valueName="Val")
1053        assert getXML(func) == ['<Val XPlacement="7" YPlacement="23"/>']
1054
1055    def test_getLigatureKey(self):
1056        components = lambda s: [tuple(word) for word in s.split()]
1057        c = components("fi fl ff ffi fff")
1058        c.sort(key=builder._getLigatureKey)
1059        assert c == components("fff ffi ff fi fl")
1060
1061    def test_getSinglePosValueKey(self):
1062        device = builder.buildDevice({10: 1, 11: 3})
1063        a1 = builder.buildValue({"XPlacement": 500, "XPlaDevice": device})
1064        a2 = builder.buildValue({"XPlacement": 500, "XPlaDevice": device})
1065        b = builder.buildValue({"XPlacement": 500})
1066        keyA1 = builder._getSinglePosValueKey(a1)
1067        keyA2 = builder._getSinglePosValueKey(a1)
1068        keyB = builder._getSinglePosValueKey(b)
1069        assert keyA1 == keyA2
1070        assert hash(keyA1) == hash(keyA2)
1071        assert keyA1 != keyB
1072        assert hash(keyA1) != hash(keyB)
1073
1074
1075class ClassDefBuilderTest(object):
1076    def test_build_usingClass0(self):
1077        b = builder.ClassDefBuilder(useClass0=True)
1078        b.add({"aa", "bb"})
1079        b.add({"a", "b"})
1080        b.add({"c"})
1081        b.add({"e", "f", "g", "h"})
1082        cdef = b.build()
1083        assert isinstance(cdef, otTables.ClassDef)
1084        assert cdef.classDefs == {"a": 2, "b": 2, "c": 3, "aa": 1, "bb": 1}
1085
1086    def test_build_notUsingClass0(self):
1087        b = builder.ClassDefBuilder(useClass0=False)
1088        b.add({"a", "b"})
1089        b.add({"c"})
1090        b.add({"e", "f", "g", "h"})
1091        cdef = b.build()
1092        assert isinstance(cdef, otTables.ClassDef)
1093        assert cdef.classDefs == {
1094            "a": 2,
1095            "b": 2,
1096            "c": 3,
1097            "e": 1,
1098            "f": 1,
1099            "g": 1,
1100            "h": 1,
1101        }
1102
1103    def test_canAdd(self):
1104        b = builder.ClassDefBuilder(useClass0=True)
1105        b.add({"a", "b", "c", "d"})
1106        b.add({"e", "f"})
1107        assert b.canAdd({"a", "b", "c", "d"})
1108        assert b.canAdd({"e", "f"})
1109        assert b.canAdd({"g", "h", "i"})
1110        assert not b.canAdd({"b", "c", "d"})
1111        assert not b.canAdd({"a", "b", "c", "d", "e", "f"})
1112        assert not b.canAdd({"d", "e", "f"})
1113        assert not b.canAdd({"f"})
1114
1115    def test_add_exception(self):
1116        b = builder.ClassDefBuilder(useClass0=True)
1117        b.add({"a", "b", "c"})
1118        with pytest.raises(error.OpenTypeLibError):
1119            b.add({"a", "d"})
1120
1121
1122buildStatTable_test_data = [
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')])], None, "Regular", [
1131        '  <STAT>',
1132        '    <Version value="0x00010001"/>',
1133        '    <DesignAxisRecordSize value="8"/>',
1134        '    <!-- DesignAxisCount=1 -->',
1135        '    <DesignAxisRecord>',
1136        '      <Axis index="0">',
1137        '        <AxisTag value="wght"/>',
1138        '        <AxisNameID value="257"/>  <!-- Weight -->',
1139        '        <AxisOrdering value="0"/>',
1140        '      </Axis>',
1141        '    </DesignAxisRecord>',
1142        '    <!-- AxisValueCount=3 -->',
1143        '    <AxisValueArray>',
1144        '      <AxisValue index="0" Format="1">',
1145        '        <AxisIndex value="0"/>',
1146        '        <Flags value="0"/>',
1147        '        <ValueNameID value="258"/>  <!-- Thin -->',
1148        '        <Value value="100.0"/>',
1149        '      </AxisValue>',
1150        '      <AxisValue index="1" Format="1">',
1151        '        <AxisIndex value="0"/>',
1152        '        <Flags value="2"/>  <!-- ElidableAxisValueName -->',
1153        '        <ValueNameID value="256"/>  <!-- Regular -->',
1154        '        <Value value="400.0"/>',
1155        '      </AxisValue>',
1156        '      <AxisValue index="2" Format="1">',
1157        '        <AxisIndex value="0"/>',
1158        '        <Flags value="0"/>',
1159        '        <ValueNameID value="259"/>  <!-- Black -->',
1160        '        <Value value="900.0"/>',
1161        '      </AxisValue>',
1162        '    </AxisValueArray>',
1163        '    <ElidedFallbackNameID value="256"/>  <!-- Regular -->',
1164        '  </STAT>']),
1165    ([
1166        dict(
1167            tag="wght",
1168            name=dict(en="Weight", nl="Gewicht"),
1169            values=[
1170                dict(value=100, name=dict(en='Thin', nl='Dun')),
1171                dict(value=400, name='Regular', flags=0x2),
1172                dict(value=900, name='Black'),
1173            ]),
1174        dict(
1175            tag="wdth",
1176            name="Width",
1177            values=[
1178                dict(value=50, name='Condensed'),
1179                dict(value=100, name='Regular', flags=0x2),
1180                dict(value=200, name='Extended')])], None, 2, [
1181        '  <STAT>',
1182        '    <Version value="0x00010001"/>',
1183        '    <DesignAxisRecordSize value="8"/>',
1184        '    <!-- DesignAxisCount=2 -->',
1185        '    <DesignAxisRecord>',
1186        '      <Axis index="0">',
1187        '        <AxisTag value="wght"/>',
1188        '        <AxisNameID value="256"/>  <!-- Weight -->',
1189        '        <AxisOrdering value="0"/>',
1190        '      </Axis>',
1191        '      <Axis index="1">',
1192        '        <AxisTag value="wdth"/>',
1193        '        <AxisNameID value="260"/>  <!-- Width -->',
1194        '        <AxisOrdering value="1"/>',
1195        '      </Axis>',
1196        '    </DesignAxisRecord>',
1197        '    <!-- AxisValueCount=6 -->',
1198        '    <AxisValueArray>',
1199        '      <AxisValue index="0" Format="1">',
1200        '        <AxisIndex value="0"/>',
1201        '        <Flags value="0"/>',
1202        '        <ValueNameID value="257"/>  <!-- Thin -->',
1203        '        <Value value="100.0"/>',
1204        '      </AxisValue>',
1205        '      <AxisValue index="1" Format="1">',
1206        '        <AxisIndex value="0"/>',
1207        '        <Flags value="2"/>  <!-- ElidableAxisValueName -->',
1208        '        <ValueNameID value="258"/>  <!-- Regular -->',
1209        '        <Value value="400.0"/>',
1210        '      </AxisValue>',
1211        '      <AxisValue index="2" Format="1">',
1212        '        <AxisIndex value="0"/>',
1213        '        <Flags value="0"/>',
1214        '        <ValueNameID value="259"/>  <!-- Black -->',
1215        '        <Value value="900.0"/>',
1216        '      </AxisValue>',
1217        '      <AxisValue index="3" Format="1">',
1218        '        <AxisIndex value="1"/>',
1219        '        <Flags value="0"/>',
1220        '        <ValueNameID value="261"/>  <!-- Condensed -->',
1221        '        <Value value="50.0"/>',
1222        '      </AxisValue>',
1223        '      <AxisValue index="4" Format="1">',
1224        '        <AxisIndex value="1"/>',
1225        '        <Flags value="2"/>  <!-- ElidableAxisValueName -->',
1226        '        <ValueNameID value="258"/>  <!-- Regular -->',
1227        '        <Value value="100.0"/>',
1228        '      </AxisValue>',
1229        '      <AxisValue index="5" Format="1">',
1230        '        <AxisIndex value="1"/>',
1231        '        <Flags value="0"/>',
1232        '        <ValueNameID value="262"/>  <!-- Extended -->',
1233        '        <Value value="200.0"/>',
1234        '      </AxisValue>',
1235        '    </AxisValueArray>',
1236        '    <ElidedFallbackNameID value="2"/>  <!-- missing from name table -->',
1237        '  </STAT>']),
1238    ([
1239        dict(
1240            tag="wght",
1241            name="Weight",
1242            values=[
1243                dict(value=400, name='Regular', flags=0x2),
1244                dict(value=600, linkedValue=650, name='Bold')])], None, 18, [
1245        '  <STAT>',
1246        '    <Version value="0x00010001"/>',
1247        '    <DesignAxisRecordSize value="8"/>',
1248        '    <!-- DesignAxisCount=1 -->',
1249        '    <DesignAxisRecord>',
1250        '      <Axis index="0">',
1251        '        <AxisTag value="wght"/>',
1252        '        <AxisNameID value="256"/>  <!-- Weight -->',
1253        '        <AxisOrdering value="0"/>',
1254        '      </Axis>',
1255        '    </DesignAxisRecord>',
1256        '    <!-- AxisValueCount=2 -->',
1257        '    <AxisValueArray>',
1258        '      <AxisValue index="0" Format="1">',
1259        '        <AxisIndex value="0"/>',
1260        '        <Flags value="2"/>  <!-- ElidableAxisValueName -->',
1261        '        <ValueNameID value="257"/>  <!-- Regular -->',
1262        '        <Value value="400.0"/>',
1263        '      </AxisValue>',
1264        '      <AxisValue index="1" Format="3">',
1265        '        <AxisIndex value="0"/>',
1266        '        <Flags value="0"/>',
1267        '        <ValueNameID value="258"/>  <!-- Bold -->',
1268        '        <Value value="600.0"/>',
1269        '        <LinkedValue value="650.0"/>',
1270        '      </AxisValue>',
1271        '    </AxisValueArray>',
1272        '    <ElidedFallbackNameID value="18"/>  <!-- missing from name table -->',
1273        '  </STAT>']),
1274    ([
1275        dict(
1276            tag="opsz",
1277            name="Optical Size",
1278            values=[
1279                dict(nominalValue=6, rangeMaxValue=10, name='Small'),
1280                dict(rangeMinValue=10, nominalValue=14, rangeMaxValue=24, name='Text', flags=0x2),
1281                dict(rangeMinValue=24, nominalValue=600, name='Display')])], None, 2, [
1282        '  <STAT>',
1283        '    <Version value="0x00010001"/>',
1284        '    <DesignAxisRecordSize value="8"/>',
1285        '    <!-- DesignAxisCount=1 -->',
1286        '    <DesignAxisRecord>',
1287        '      <Axis index="0">',
1288        '        <AxisTag value="opsz"/>',
1289        '        <AxisNameID value="256"/>  <!-- Optical Size -->',
1290        '        <AxisOrdering value="0"/>',
1291        '      </Axis>',
1292        '    </DesignAxisRecord>',
1293        '    <!-- AxisValueCount=3 -->',
1294        '    <AxisValueArray>',
1295        '      <AxisValue index="0" Format="2">',
1296        '        <AxisIndex value="0"/>',
1297        '        <Flags value="0"/>',
1298        '        <ValueNameID value="257"/>  <!-- Small -->',
1299        '        <NominalValue value="6.0"/>',
1300        '        <RangeMinValue value="-32768.0"/>',
1301        '        <RangeMaxValue value="10.0"/>',
1302        '      </AxisValue>',
1303        '      <AxisValue index="1" Format="2">',
1304        '        <AxisIndex value="0"/>',
1305        '        <Flags value="2"/>  <!-- ElidableAxisValueName -->',
1306        '        <ValueNameID value="258"/>  <!-- Text -->',
1307        '        <NominalValue value="14.0"/>',
1308        '        <RangeMinValue value="10.0"/>',
1309        '        <RangeMaxValue value="24.0"/>',
1310        '      </AxisValue>',
1311        '      <AxisValue index="2" Format="2">',
1312        '        <AxisIndex value="0"/>',
1313        '        <Flags value="0"/>',
1314        '        <ValueNameID value="259"/>  <!-- Display -->',
1315        '        <NominalValue value="600.0"/>',
1316        '        <RangeMinValue value="24.0"/>',
1317        '        <RangeMaxValue value="32767.99998"/>',
1318        '      </AxisValue>',
1319        '    </AxisValueArray>',
1320        '    <ElidedFallbackNameID value="2"/>  <!-- missing from name table -->',
1321        '  </STAT>']),
1322    ([
1323        dict(
1324            tag="wght",
1325            name="Weight",
1326            ordering=1,
1327            values=[]),
1328        dict(
1329            tag="ABCD",
1330            name="ABCDTest",
1331            ordering=0,
1332            values=[
1333                dict(value=100, name="Regular", flags=0x2)])],
1334     [dict(location=dict(wght=300, ABCD=100), name='Regular ABCD')], 18, [
1335        '  <STAT>',
1336        '    <Version value="0x00010002"/>',
1337        '    <DesignAxisRecordSize value="8"/>',
1338        '    <!-- DesignAxisCount=2 -->',
1339        '    <DesignAxisRecord>',
1340        '      <Axis index="0">',
1341        '        <AxisTag value="wght"/>',
1342        '        <AxisNameID value="256"/>  <!-- Weight -->',
1343        '        <AxisOrdering value="1"/>',
1344        '      </Axis>',
1345        '      <Axis index="1">',
1346        '        <AxisTag value="ABCD"/>',
1347        '        <AxisNameID value="257"/>  <!-- ABCDTest -->',
1348        '        <AxisOrdering value="0"/>',
1349        '      </Axis>',
1350        '    </DesignAxisRecord>',
1351        '    <!-- AxisValueCount=2 -->',
1352        '    <AxisValueArray>',
1353        '      <AxisValue index="0" Format="4">',
1354        '        <!-- AxisCount=2 -->',
1355        '        <Flags value="0"/>',
1356        '        <ValueNameID value="259"/>  <!-- Regular ABCD -->',
1357        '        <AxisValueRecord index="0">',
1358        '          <AxisIndex value="0"/>',
1359        '          <Value value="300.0"/>',
1360        '        </AxisValueRecord>',
1361        '        <AxisValueRecord index="1">',
1362        '          <AxisIndex value="1"/>',
1363        '          <Value value="100.0"/>',
1364        '        </AxisValueRecord>',
1365        '      </AxisValue>',
1366        '      <AxisValue index="1" Format="1">',
1367        '        <AxisIndex value="1"/>',
1368        '        <Flags value="2"/>  <!-- ElidableAxisValueName -->',
1369        '        <ValueNameID value="258"/>  <!-- Regular -->',
1370        '        <Value value="100.0"/>',
1371        '      </AxisValue>',
1372        '    </AxisValueArray>',
1373        '    <ElidedFallbackNameID value="18"/>  <!-- missing from name table -->',
1374        '  </STAT>']),
1375]
1376
1377
1378@pytest.mark.parametrize("axes, axisValues, elidedFallbackName, expected_ttx", buildStatTable_test_data)
1379def test_buildStatTable(axes, axisValues, elidedFallbackName, expected_ttx):
1380    font = ttLib.TTFont()
1381    font["name"] = ttLib.newTable("name")
1382    font["name"].names = []
1383    # https://github.com/fonttools/fonttools/issues/1985
1384    # Add nameID < 256 that matches a test axis name, to test whether
1385    # the nameID is not reused: AxisNameIDs must be > 255 according
1386    # to the spec.
1387    font["name"].addMultilingualName(dict(en="ABCDTest"), nameID=6)
1388    builder.buildStatTable(font, axes, axisValues, elidedFallbackName)
1389    f = io.StringIO()
1390    font.saveXML(f, tables=["STAT"])
1391    ttx = f.getvalue().splitlines()
1392    ttx = ttx[3:-2]  # strip XML header and <ttFont> element
1393    assert expected_ttx == ttx
1394    # Compile and round-trip
1395    f = io.BytesIO()
1396    font.save(f)
1397    font = ttLib.TTFont(f)
1398    f = io.StringIO()
1399    font.saveXML(f, tables=["STAT"])
1400    ttx = f.getvalue().splitlines()
1401    ttx = ttx[3:-2]  # strip XML header and <ttFont> element
1402    assert expected_ttx == ttx
1403
1404
1405def test_stat_infinities():
1406    negInf = floatToFixed(builder.AXIS_VALUE_NEGATIVE_INFINITY, 16)
1407    assert struct.pack(">l", negInf) == b"\x80\x00\x00\x00"
1408    posInf = floatToFixed(builder.AXIS_VALUE_POSITIVE_INFINITY, 16)
1409    assert struct.pack(">l", posInf) == b"\x7f\xff\xff\xff"
1410
1411
1412class ChainContextualRulesetTest(object):
1413    def test_makeRulesets(self):
1414        font = ttLib.TTFont()
1415        font.setGlyphOrder(["a","b","c","d","A","B","C","D","E"])
1416        sb = builder.ChainContextSubstBuilder(font, None)
1417        prefix, input_, suffix, lookups = [["a"], ["b"]], [["c"]], [], [None]
1418        sb.rules.append(builder.ChainContextualRule(prefix, input_, suffix, lookups))
1419
1420        prefix, input_, suffix, lookups = [["a"], ["d"]], [["c"]], [], [None]
1421        sb.rules.append(builder.ChainContextualRule(prefix, input_, suffix, lookups))
1422
1423        sb.add_subtable_break(None)
1424
1425        # Second subtable has some glyph classes
1426        prefix, input_, suffix, lookups = [["A"]], [["E"]], [], [None]
1427        sb.rules.append(builder.ChainContextualRule(prefix, input_, suffix, lookups))
1428        prefix, input_, suffix, lookups = [["A"]], [["C","D"]], [], [None]
1429        sb.rules.append(builder.ChainContextualRule(prefix, input_, suffix, lookups))
1430        prefix, input_, suffix, lookups = [["A", "B"]], [["E"]], [], [None]
1431        sb.rules.append(builder.ChainContextualRule(prefix, input_, suffix, lookups))
1432
1433        sb.add_subtable_break(None)
1434
1435        # Third subtable has no pre/post context
1436        prefix, input_, suffix, lookups = [], [["E"]], [], [None]
1437        sb.rules.append(builder.ChainContextualRule(prefix, input_, suffix, lookups))
1438        prefix, input_, suffix, lookups = [], [["C","D"]], [], [None]
1439        sb.rules.append(builder.ChainContextualRule(prefix, input_, suffix, lookups))
1440
1441        rulesets = sb.rulesets()
1442        assert len(rulesets) == 3
1443        assert rulesets[0].hasPrefixOrSuffix
1444        assert not rulesets[0].hasAnyGlyphClasses
1445        cd = rulesets[0].format2ClassDefs()
1446        assert set(cd[0].classes()[1:]) == set([("d",),("b",),("a",)])
1447        assert set(cd[1].classes()[1:]) == set([("c",)])
1448        assert set(cd[2].classes()[1:]) == set()
1449
1450        assert rulesets[1].hasPrefixOrSuffix
1451        assert rulesets[1].hasAnyGlyphClasses
1452        assert not rulesets[1].format2ClassDefs()
1453
1454        assert not rulesets[2].hasPrefixOrSuffix
1455        assert rulesets[2].hasAnyGlyphClasses
1456        assert rulesets[2].format2ClassDefs()
1457        cd = rulesets[2].format2ClassDefs()
1458        assert set(cd[0].classes()[1:]) == set()
1459        assert set(cd[1].classes()[1:]) == set([("C","D"), ("E",)])
1460        assert set(cd[2].classes()[1:]) == set()
1461
1462
1463if __name__ == "__main__":
1464    import sys
1465
1466    sys.exit(pytest.main(sys.argv))
1467