• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1from __future__ import print_function, division, absolute_import
2from __future__ import unicode_literals
3from fontTools.misc.testTools import getXML
4from fontTools.otlLib import builder
5from fontTools.ttLib.tables import otTables
6import pytest
7
8
9class BuilderTest(object):
10    GLYPHS = (
11        ".notdef space zero one two three four five six "
12        "A B C a b c grave acute cedilla f_f_i f_i c_t"
13    ).split()
14    GLYPHMAP = {name: num for num, name in enumerate(GLYPHS)}
15
16    ANCHOR1 = builder.buildAnchor(11, -11)
17    ANCHOR2 = builder.buildAnchor(22, -22)
18    ANCHOR3 = builder.buildAnchor(33, -33)
19
20    def test_buildAnchor_format1(self):
21        anchor = builder.buildAnchor(23, 42)
22        assert getXML(anchor.toXML) == [
23            '<Anchor Format="1">',
24            '  <XCoordinate value="23"/>',
25            '  <YCoordinate value="42"/>',
26            "</Anchor>",
27        ]
28
29    def test_buildAnchor_format2(self):
30        anchor = builder.buildAnchor(23, 42, point=17)
31        assert getXML(anchor.toXML) == [
32            '<Anchor Format="2">',
33            '  <XCoordinate value="23"/>',
34            '  <YCoordinate value="42"/>',
35            '  <AnchorPoint value="17"/>',
36            "</Anchor>",
37        ]
38
39    def test_buildAnchor_format3(self):
40        anchor = builder.buildAnchor(
41            23,
42            42,
43            deviceX=builder.buildDevice({1: 1, 0: 0}),
44            deviceY=builder.buildDevice({7: 7}),
45        )
46        assert getXML(anchor.toXML) == [
47            '<Anchor Format="3">',
48            '  <XCoordinate value="23"/>',
49            '  <YCoordinate value="42"/>',
50            "  <XDeviceTable>",
51            '    <StartSize value="0"/>',
52            '    <EndSize value="1"/>',
53            '    <DeltaFormat value="1"/>',
54            '    <DeltaValue value="[0, 1]"/>',
55            "  </XDeviceTable>",
56            "  <YDeviceTable>",
57            '    <StartSize value="7"/>',
58            '    <EndSize value="7"/>',
59            '    <DeltaFormat value="2"/>',
60            '    <DeltaValue value="[7]"/>',
61            "  </YDeviceTable>",
62            "</Anchor>",
63        ]
64
65    def test_buildAttachList(self):
66        attachList = builder.buildAttachList(
67            {"zero": [23, 7], "one": [1]}, self.GLYPHMAP
68        )
69        assert getXML(attachList.toXML) == [
70            "<AttachList>",
71            "  <Coverage>",
72            '    <Glyph value="zero"/>',
73            '    <Glyph value="one"/>',
74            "  </Coverage>",
75            "  <!-- GlyphCount=2 -->",
76            '  <AttachPoint index="0">',
77            "    <!-- PointCount=2 -->",
78            '    <PointIndex index="0" value="7"/>',
79            '    <PointIndex index="1" value="23"/>',
80            "  </AttachPoint>",
81            '  <AttachPoint index="1">',
82            "    <!-- PointCount=1 -->",
83            '    <PointIndex index="0" value="1"/>',
84            "  </AttachPoint>",
85            "</AttachList>",
86        ]
87
88    def test_buildAttachList_empty(self):
89        assert builder.buildAttachList({}, self.GLYPHMAP) is None
90
91    def test_buildAttachPoint(self):
92        attachPoint = builder.buildAttachPoint([7, 3])
93        assert getXML(attachPoint.toXML) == [
94            "<AttachPoint>",
95            "  <!-- PointCount=2 -->",
96            '  <PointIndex index="0" value="3"/>',
97            '  <PointIndex index="1" value="7"/>',
98            "</AttachPoint>",
99        ]
100
101    def test_buildAttachPoint_empty(self):
102        assert builder.buildAttachPoint([]) is None
103
104    def test_buildAttachPoint_duplicate(self):
105        attachPoint = builder.buildAttachPoint([7, 3, 7])
106        assert getXML(attachPoint.toXML) == [
107            "<AttachPoint>",
108            "  <!-- PointCount=2 -->",
109            '  <PointIndex index="0" value="3"/>',
110            '  <PointIndex index="1" value="7"/>',
111            "</AttachPoint>",
112        ]
113
114    def test_buildBaseArray(self):
115        anchor = builder.buildAnchor
116        baseArray = builder.buildBaseArray(
117            {"a": {2: anchor(300, 80)}, "c": {1: anchor(300, 80), 2: anchor(300, -20)}},
118            numMarkClasses=4,
119            glyphMap=self.GLYPHMAP,
120        )
121        assert getXML(baseArray.toXML) == [
122            "<BaseArray>",
123            "  <!-- BaseCount=2 -->",
124            '  <BaseRecord index="0">',
125            '    <BaseAnchor index="0" empty="1"/>',
126            '    <BaseAnchor index="1" empty="1"/>',
127            '    <BaseAnchor index="2" Format="1">',
128            '      <XCoordinate value="300"/>',
129            '      <YCoordinate value="80"/>',
130            "    </BaseAnchor>",
131            '    <BaseAnchor index="3" empty="1"/>',
132            "  </BaseRecord>",
133            '  <BaseRecord index="1">',
134            '    <BaseAnchor index="0" empty="1"/>',
135            '    <BaseAnchor index="1" Format="1">',
136            '      <XCoordinate value="300"/>',
137            '      <YCoordinate value="80"/>',
138            "    </BaseAnchor>",
139            '    <BaseAnchor index="2" Format="1">',
140            '      <XCoordinate value="300"/>',
141            '      <YCoordinate value="-20"/>',
142            "    </BaseAnchor>",
143            '    <BaseAnchor index="3" empty="1"/>',
144            "  </BaseRecord>",
145            "</BaseArray>",
146        ]
147
148    def test_buildBaseRecord(self):
149        a = builder.buildAnchor
150        rec = builder.buildBaseRecord([a(500, -20), None, a(300, -15)])
151        assert getXML(rec.toXML) == [
152            "<BaseRecord>",
153            '  <BaseAnchor index="0" Format="1">',
154            '    <XCoordinate value="500"/>',
155            '    <YCoordinate value="-20"/>',
156            "  </BaseAnchor>",
157            '  <BaseAnchor index="1" empty="1"/>',
158            '  <BaseAnchor index="2" Format="1">',
159            '    <XCoordinate value="300"/>',
160            '    <YCoordinate value="-15"/>',
161            "  </BaseAnchor>",
162            "</BaseRecord>",
163        ]
164
165    def test_buildCaretValueForCoord(self):
166        caret = builder.buildCaretValueForCoord(500)
167        assert getXML(caret.toXML) == [
168            '<CaretValue Format="1">',
169            '  <Coordinate value="500"/>',
170            "</CaretValue>",
171        ]
172
173    def test_buildCaretValueForPoint(self):
174        caret = builder.buildCaretValueForPoint(23)
175        assert getXML(caret.toXML) == [
176            '<CaretValue Format="2">',
177            '  <CaretValuePoint value="23"/>',
178            "</CaretValue>",
179        ]
180
181    def test_buildComponentRecord(self):
182        a = builder.buildAnchor
183        rec = builder.buildComponentRecord([a(500, -20), None, a(300, -15)])
184        assert getXML(rec.toXML) == [
185            "<ComponentRecord>",
186            '  <LigatureAnchor index="0" Format="1">',
187            '    <XCoordinate value="500"/>',
188            '    <YCoordinate value="-20"/>',
189            "  </LigatureAnchor>",
190            '  <LigatureAnchor index="1" empty="1"/>',
191            '  <LigatureAnchor index="2" Format="1">',
192            '    <XCoordinate value="300"/>',
193            '    <YCoordinate value="-15"/>',
194            "  </LigatureAnchor>",
195            "</ComponentRecord>",
196        ]
197
198    def test_buildComponentRecord_empty(self):
199        assert builder.buildComponentRecord([]) is None
200
201    def test_buildComponentRecord_None(self):
202        assert builder.buildComponentRecord(None) is None
203
204    def test_buildCoverage(self):
205        cov = builder.buildCoverage({"two", "four"}, {"two": 2, "four": 4})
206        assert getXML(cov.toXML) == [
207            "<Coverage>",
208            '  <Glyph value="two"/>',
209            '  <Glyph value="four"/>',
210            "</Coverage>",
211        ]
212
213    def test_buildCursivePos(self):
214        pos = builder.buildCursivePosSubtable(
215            {"two": (self.ANCHOR1, self.ANCHOR2), "four": (self.ANCHOR3, self.ANCHOR1)},
216            self.GLYPHMAP,
217        )
218        assert getXML(pos.toXML) == [
219            '<CursivePos Format="1">',
220            "  <Coverage>",
221            '    <Glyph value="two"/>',
222            '    <Glyph value="four"/>',
223            "  </Coverage>",
224            "  <!-- EntryExitCount=2 -->",
225            '  <EntryExitRecord index="0">',
226            '    <EntryAnchor Format="1">',
227            '      <XCoordinate value="11"/>',
228            '      <YCoordinate value="-11"/>',
229            "    </EntryAnchor>",
230            '    <ExitAnchor Format="1">',
231            '      <XCoordinate value="22"/>',
232            '      <YCoordinate value="-22"/>',
233            "    </ExitAnchor>",
234            "  </EntryExitRecord>",
235            '  <EntryExitRecord index="1">',
236            '    <EntryAnchor Format="1">',
237            '      <XCoordinate value="33"/>',
238            '      <YCoordinate value="-33"/>',
239            "    </EntryAnchor>",
240            '    <ExitAnchor Format="1">',
241            '      <XCoordinate value="11"/>',
242            '      <YCoordinate value="-11"/>',
243            "    </ExitAnchor>",
244            "  </EntryExitRecord>",
245            "</CursivePos>",
246        ]
247
248    def test_buildDevice_format1(self):
249        device = builder.buildDevice({1: 1, 0: 0})
250        assert getXML(device.toXML) == [
251            "<Device>",
252            '  <StartSize value="0"/>',
253            '  <EndSize value="1"/>',
254            '  <DeltaFormat value="1"/>',
255            '  <DeltaValue value="[0, 1]"/>',
256            "</Device>",
257        ]
258
259    def test_buildDevice_format2(self):
260        device = builder.buildDevice({2: 2, 0: 1, 1: 0})
261        assert getXML(device.toXML) == [
262            "<Device>",
263            '  <StartSize value="0"/>',
264            '  <EndSize value="2"/>',
265            '  <DeltaFormat value="2"/>',
266            '  <DeltaValue value="[1, 0, 2]"/>',
267            "</Device>",
268        ]
269
270    def test_buildDevice_format3(self):
271        device = builder.buildDevice({5: 3, 1: 77})
272        assert getXML(device.toXML) == [
273            "<Device>",
274            '  <StartSize value="1"/>',
275            '  <EndSize value="5"/>',
276            '  <DeltaFormat value="3"/>',
277            '  <DeltaValue value="[77, 0, 0, 0, 3]"/>',
278            "</Device>",
279        ]
280
281    def test_buildLigatureArray(self):
282        anchor = builder.buildAnchor
283        ligatureArray = builder.buildLigatureArray(
284            {
285                "f_i": [{2: anchor(300, -20)}, {}],
286                "c_t": [{}, {1: anchor(500, 350), 2: anchor(1300, -20)}],
287            },
288            numMarkClasses=4,
289            glyphMap=self.GLYPHMAP,
290        )
291        assert getXML(ligatureArray.toXML) == [
292            "<LigatureArray>",
293            "  <!-- LigatureCount=2 -->",
294            '  <LigatureAttach index="0">',  # f_i
295            "    <!-- ComponentCount=2 -->",
296            '    <ComponentRecord index="0">',
297            '      <LigatureAnchor index="0" empty="1"/>',
298            '      <LigatureAnchor index="1" empty="1"/>',
299            '      <LigatureAnchor index="2" Format="1">',
300            '        <XCoordinate value="300"/>',
301            '        <YCoordinate value="-20"/>',
302            "      </LigatureAnchor>",
303            '      <LigatureAnchor index="3" empty="1"/>',
304            "    </ComponentRecord>",
305            '    <ComponentRecord index="1">',
306            '      <LigatureAnchor index="0" empty="1"/>',
307            '      <LigatureAnchor index="1" empty="1"/>',
308            '      <LigatureAnchor index="2" empty="1"/>',
309            '      <LigatureAnchor index="3" empty="1"/>',
310            "    </ComponentRecord>",
311            "  </LigatureAttach>",
312            '  <LigatureAttach index="1">',
313            "    <!-- ComponentCount=2 -->",
314            '    <ComponentRecord index="0">',
315            '      <LigatureAnchor index="0" empty="1"/>',
316            '      <LigatureAnchor index="1" empty="1"/>',
317            '      <LigatureAnchor index="2" empty="1"/>',
318            '      <LigatureAnchor index="3" empty="1"/>',
319            "    </ComponentRecord>",
320            '    <ComponentRecord index="1">',
321            '      <LigatureAnchor index="0" empty="1"/>',
322            '      <LigatureAnchor index="1" Format="1">',
323            '        <XCoordinate value="500"/>',
324            '        <YCoordinate value="350"/>',
325            "      </LigatureAnchor>",
326            '      <LigatureAnchor index="2" Format="1">',
327            '        <XCoordinate value="1300"/>',
328            '        <YCoordinate value="-20"/>',
329            "      </LigatureAnchor>",
330            '      <LigatureAnchor index="3" empty="1"/>',
331            "    </ComponentRecord>",
332            "  </LigatureAttach>",
333            "</LigatureArray>",
334        ]
335
336    def test_buildLigatureAttach(self):
337        anchor = builder.buildAnchor
338        attach = builder.buildLigatureAttach(
339            [[anchor(500, -10), None], [None, anchor(300, -20), None]]
340        )
341        assert getXML(attach.toXML) == [
342            "<LigatureAttach>",
343            "  <!-- ComponentCount=2 -->",
344            '  <ComponentRecord index="0">',
345            '    <LigatureAnchor index="0" Format="1">',
346            '      <XCoordinate value="500"/>',
347            '      <YCoordinate value="-10"/>',
348            "    </LigatureAnchor>",
349            '    <LigatureAnchor index="1" empty="1"/>',
350            "  </ComponentRecord>",
351            '  <ComponentRecord index="1">',
352            '    <LigatureAnchor index="0" empty="1"/>',
353            '    <LigatureAnchor index="1" Format="1">',
354            '      <XCoordinate value="300"/>',
355            '      <YCoordinate value="-20"/>',
356            "    </LigatureAnchor>",
357            '    <LigatureAnchor index="2" empty="1"/>',
358            "  </ComponentRecord>",
359            "</LigatureAttach>",
360        ]
361
362    def test_buildLigatureAttach_emptyComponents(self):
363        attach = builder.buildLigatureAttach([[], None])
364        assert getXML(attach.toXML) == [
365            "<LigatureAttach>",
366            "  <!-- ComponentCount=2 -->",
367            '  <ComponentRecord index="0" empty="1"/>',
368            '  <ComponentRecord index="1" empty="1"/>',
369            "</LigatureAttach>",
370        ]
371
372    def test_buildLigatureAttach_noComponents(self):
373        attach = builder.buildLigatureAttach([])
374        assert getXML(attach.toXML) == [
375            "<LigatureAttach>",
376            "  <!-- ComponentCount=0 -->",
377            "</LigatureAttach>",
378        ]
379
380    def test_buildLigCaretList(self):
381        carets = builder.buildLigCaretList(
382            {"f_f_i": [300, 600]}, {"c_t": [42]}, self.GLYPHMAP
383        )
384        assert getXML(carets.toXML) == [
385            "<LigCaretList>",
386            "  <Coverage>",
387            '    <Glyph value="f_f_i"/>',
388            '    <Glyph value="c_t"/>',
389            "  </Coverage>",
390            "  <!-- LigGlyphCount=2 -->",
391            '  <LigGlyph index="0">',
392            "    <!-- CaretCount=2 -->",
393            '    <CaretValue index="0" Format="1">',
394            '      <Coordinate value="300"/>',
395            "    </CaretValue>",
396            '    <CaretValue index="1" Format="1">',
397            '      <Coordinate value="600"/>',
398            "    </CaretValue>",
399            "  </LigGlyph>",
400            '  <LigGlyph index="1">',
401            "    <!-- CaretCount=1 -->",
402            '    <CaretValue index="0" Format="2">',
403            '      <CaretValuePoint value="42"/>',
404            "    </CaretValue>",
405            "  </LigGlyph>",
406            "</LigCaretList>",
407        ]
408
409    def test_buildLigCaretList_bothCoordsAndPointsForSameGlyph(self):
410        carets = builder.buildLigCaretList(
411            {"f_f_i": [300]}, {"f_f_i": [7]}, self.GLYPHMAP
412        )
413        assert getXML(carets.toXML) == [
414            "<LigCaretList>",
415            "  <Coverage>",
416            '    <Glyph value="f_f_i"/>',
417            "  </Coverage>",
418            "  <!-- LigGlyphCount=1 -->",
419            '  <LigGlyph index="0">',
420            "    <!-- CaretCount=2 -->",
421            '    <CaretValue index="0" Format="1">',
422            '      <Coordinate value="300"/>',
423            "    </CaretValue>",
424            '    <CaretValue index="1" Format="2">',
425            '      <CaretValuePoint value="7"/>',
426            "    </CaretValue>",
427            "  </LigGlyph>",
428            "</LigCaretList>",
429        ]
430
431    def test_buildLigCaretList_empty(self):
432        assert builder.buildLigCaretList({}, {}, self.GLYPHMAP) is None
433
434    def test_buildLigCaretList_None(self):
435        assert builder.buildLigCaretList(None, None, self.GLYPHMAP) is None
436
437    def test_buildLigGlyph_coords(self):
438        lig = builder.buildLigGlyph([500, 800], None)
439        assert getXML(lig.toXML) == [
440            "<LigGlyph>",
441            "  <!-- CaretCount=2 -->",
442            '  <CaretValue index="0" Format="1">',
443            '    <Coordinate value="500"/>',
444            "  </CaretValue>",
445            '  <CaretValue index="1" Format="1">',
446            '    <Coordinate value="800"/>',
447            "  </CaretValue>",
448            "</LigGlyph>",
449        ]
450
451    def test_buildLigGlyph_empty(self):
452        assert builder.buildLigGlyph([], []) is None
453
454    def test_buildLigGlyph_None(self):
455        assert builder.buildLigGlyph(None, None) is None
456
457    def test_buildLigGlyph_points(self):
458        lig = builder.buildLigGlyph(None, [2])
459        assert getXML(lig.toXML) == [
460            "<LigGlyph>",
461            "  <!-- CaretCount=1 -->",
462            '  <CaretValue index="0" Format="2">',
463            '    <CaretValuePoint value="2"/>',
464            "  </CaretValue>",
465            "</LigGlyph>",
466        ]
467
468    def test_buildLookup(self):
469        s1 = builder.buildSingleSubstSubtable({"one": "two"})
470        s2 = builder.buildSingleSubstSubtable({"three": "four"})
471        lookup = builder.buildLookup([s1, s2], flags=7)
472        assert getXML(lookup.toXML) == [
473            "<Lookup>",
474            '  <LookupType value="1"/>',
475            '  <LookupFlag value="7"/>',
476            "  <!-- SubTableCount=2 -->",
477            '  <SingleSubst index="0">',
478            '    <Substitution in="one" out="two"/>',
479            "  </SingleSubst>",
480            '  <SingleSubst index="1">',
481            '    <Substitution in="three" out="four"/>',
482            "  </SingleSubst>",
483            "</Lookup>",
484        ]
485
486    def test_buildLookup_badFlags(self):
487        s = builder.buildSingleSubstSubtable({"one": "two"})
488        with pytest.raises(
489            AssertionError,
490            match=(
491                "if markFilterSet is None, flags must not set "
492                "LOOKUP_FLAG_USE_MARK_FILTERING_SET; flags=0x0010"
493            ),
494        ) as excinfo:
495            builder.buildLookup([s], builder.LOOKUP_FLAG_USE_MARK_FILTERING_SET, None)
496        with pytest.raises(
497            AssertionError,
498            match=(
499                "if markFilterSet is not None, flags must set "
500                "LOOKUP_FLAG_USE_MARK_FILTERING_SET; flags=0x0004"
501            ),
502        ) as excinfo:
503            builder.buildLookup([s], builder.LOOKUP_FLAG_IGNORE_LIGATURES, 777)
504
505    def test_buildLookup_conflictingSubtableTypes(self):
506        s1 = builder.buildSingleSubstSubtable({"one": "two"})
507        s2 = builder.buildAlternateSubstSubtable({"one": ["two", "three"]})
508        with pytest.raises(
509            AssertionError, match="all subtables must have the same LookupType"
510        ) as excinfo:
511            builder.buildLookup([s1, s2])
512
513    def test_buildLookup_noSubtables(self):
514        assert builder.buildLookup([]) is None
515        assert builder.buildLookup(None) is None
516        assert builder.buildLookup([None]) is None
517        assert builder.buildLookup([None, None]) is None
518
519    def test_buildLookup_markFilterSet(self):
520        s = builder.buildSingleSubstSubtable({"one": "two"})
521        flags = (
522            builder.LOOKUP_FLAG_RIGHT_TO_LEFT
523            | builder.LOOKUP_FLAG_USE_MARK_FILTERING_SET
524        )
525        lookup = builder.buildLookup([s], flags, markFilterSet=999)
526        assert getXML(lookup.toXML) == [
527            "<Lookup>",
528            '  <LookupType value="1"/>',
529            '  <LookupFlag value="17"/>',
530            "  <!-- SubTableCount=1 -->",
531            '  <SingleSubst index="0">',
532            '    <Substitution in="one" out="two"/>',
533            "  </SingleSubst>",
534            '  <MarkFilteringSet value="999"/>',
535            "</Lookup>",
536        ]
537
538    def test_buildMarkArray(self):
539        markArray = builder.buildMarkArray(
540            {
541                "acute": (7, builder.buildAnchor(300, 800)),
542                "grave": (2, builder.buildAnchor(10, 80)),
543            },
544            self.GLYPHMAP,
545        )
546        assert self.GLYPHMAP["grave"] < self.GLYPHMAP["acute"]
547        assert getXML(markArray.toXML) == [
548            "<MarkArray>",
549            "  <!-- MarkCount=2 -->",
550            '  <MarkRecord index="0">',
551            '    <Class value="2"/>',
552            '    <MarkAnchor Format="1">',
553            '      <XCoordinate value="10"/>',
554            '      <YCoordinate value="80"/>',
555            "    </MarkAnchor>",
556            "  </MarkRecord>",
557            '  <MarkRecord index="1">',
558            '    <Class value="7"/>',
559            '    <MarkAnchor Format="1">',
560            '      <XCoordinate value="300"/>',
561            '      <YCoordinate value="800"/>',
562            "    </MarkAnchor>",
563            "  </MarkRecord>",
564            "</MarkArray>",
565        ]
566
567    def test_buildMarkBasePosSubtable(self):
568        anchor = builder.buildAnchor
569        marks = {
570            "acute": (0, anchor(300, 700)),
571            "cedilla": (1, anchor(300, -100)),
572            "grave": (0, anchor(300, 700)),
573        }
574        bases = {
575            # Make sure we can handle missing entries.
576            "A": {},  # no entry for any markClass
577            "B": {0: anchor(500, 900)},  # only markClass 0 specified
578            "C": {1: anchor(500, -10)},  # only markClass 1 specified
579            "a": {0: anchor(500, 400), 1: anchor(500, -20)},
580            "b": {0: anchor(500, 800), 1: anchor(500, -20)},
581        }
582        table = builder.buildMarkBasePosSubtable(marks, bases, self.GLYPHMAP)
583        assert getXML(table.toXML) == [
584            '<MarkBasePos Format="1">',
585            "  <MarkCoverage>",
586            '    <Glyph value="grave"/>',
587            '    <Glyph value="acute"/>',
588            '    <Glyph value="cedilla"/>',
589            "  </MarkCoverage>",
590            "  <BaseCoverage>",
591            '    <Glyph value="A"/>',
592            '    <Glyph value="B"/>',
593            '    <Glyph value="C"/>',
594            '    <Glyph value="a"/>',
595            '    <Glyph value="b"/>',
596            "  </BaseCoverage>",
597            "  <!-- ClassCount=2 -->",
598            "  <MarkArray>",
599            "    <!-- MarkCount=3 -->",
600            '    <MarkRecord index="0">',  # grave
601            '      <Class value="0"/>',
602            '      <MarkAnchor Format="1">',
603            '        <XCoordinate value="300"/>',
604            '        <YCoordinate value="700"/>',
605            "      </MarkAnchor>",
606            "    </MarkRecord>",
607            '    <MarkRecord index="1">',  # acute
608            '      <Class value="0"/>',
609            '      <MarkAnchor Format="1">',
610            '        <XCoordinate value="300"/>',
611            '        <YCoordinate value="700"/>',
612            "      </MarkAnchor>",
613            "    </MarkRecord>",
614            '    <MarkRecord index="2">',  # cedilla
615            '      <Class value="1"/>',
616            '      <MarkAnchor Format="1">',
617            '        <XCoordinate value="300"/>',
618            '        <YCoordinate value="-100"/>',
619            "      </MarkAnchor>",
620            "    </MarkRecord>",
621            "  </MarkArray>",
622            "  <BaseArray>",
623            "    <!-- BaseCount=5 -->",
624            '    <BaseRecord index="0">',  # A
625            '      <BaseAnchor index="0" empty="1"/>',
626            '      <BaseAnchor index="1" empty="1"/>',
627            "    </BaseRecord>",
628            '    <BaseRecord index="1">',  # B
629            '      <BaseAnchor index="0" Format="1">',
630            '        <XCoordinate value="500"/>',
631            '        <YCoordinate value="900"/>',
632            "      </BaseAnchor>",
633            '      <BaseAnchor index="1" empty="1"/>',
634            "    </BaseRecord>",
635            '    <BaseRecord index="2">',  # C
636            '      <BaseAnchor index="0" empty="1"/>',
637            '      <BaseAnchor index="1" Format="1">',
638            '        <XCoordinate value="500"/>',
639            '        <YCoordinate value="-10"/>',
640            "      </BaseAnchor>",
641            "    </BaseRecord>",
642            '    <BaseRecord index="3">',  # a
643            '      <BaseAnchor index="0" Format="1">',
644            '        <XCoordinate value="500"/>',
645            '        <YCoordinate value="400"/>',
646            "      </BaseAnchor>",
647            '      <BaseAnchor index="1" Format="1">',
648            '        <XCoordinate value="500"/>',
649            '        <YCoordinate value="-20"/>',
650            "      </BaseAnchor>",
651            "    </BaseRecord>",
652            '    <BaseRecord index="4">',  # b
653            '      <BaseAnchor index="0" Format="1">',
654            '        <XCoordinate value="500"/>',
655            '        <YCoordinate value="800"/>',
656            "      </BaseAnchor>",
657            '      <BaseAnchor index="1" Format="1">',
658            '        <XCoordinate value="500"/>',
659            '        <YCoordinate value="-20"/>',
660            "      </BaseAnchor>",
661            "    </BaseRecord>",
662            "  </BaseArray>",
663            "</MarkBasePos>",
664        ]
665
666    def test_buildMarkGlyphSetsDef(self):
667        marksets = builder.buildMarkGlyphSetsDef(
668            [{"acute", "grave"}, {"cedilla", "grave"}], self.GLYPHMAP
669        )
670        assert getXML(marksets.toXML) == [
671            "<MarkGlyphSetsDef>",
672            '  <MarkSetTableFormat value="1"/>',
673            "  <!-- MarkSetCount=2 -->",
674            '  <Coverage index="0">',
675            '    <Glyph value="grave"/>',
676            '    <Glyph value="acute"/>',
677            "  </Coverage>",
678            '  <Coverage index="1">',
679            '    <Glyph value="grave"/>',
680            '    <Glyph value="cedilla"/>',
681            "  </Coverage>",
682            "</MarkGlyphSetsDef>",
683        ]
684
685    def test_buildMarkGlyphSetsDef_empty(self):
686        assert builder.buildMarkGlyphSetsDef([], self.GLYPHMAP) is None
687
688    def test_buildMarkGlyphSetsDef_None(self):
689        assert builder.buildMarkGlyphSetsDef(None, self.GLYPHMAP) is None
690
691    def test_buildMarkLigPosSubtable(self):
692        anchor = builder.buildAnchor
693        marks = {
694            "acute": (0, anchor(300, 700)),
695            "cedilla": (1, anchor(300, -100)),
696            "grave": (0, anchor(300, 700)),
697        }
698        bases = {
699            "f_i": [{}, {0: anchor(200, 400)}],  # nothing on f; only 1 on i
700            "c_t": [
701                {0: anchor(500, 600), 1: anchor(500, -20)},  # c
702                {0: anchor(1300, 800), 1: anchor(1300, -20)},  # t
703            ],
704        }
705        table = builder.buildMarkLigPosSubtable(marks, bases, self.GLYPHMAP)
706        assert getXML(table.toXML) == [
707            '<MarkLigPos Format="1">',
708            "  <MarkCoverage>",
709            '    <Glyph value="grave"/>',
710            '    <Glyph value="acute"/>',
711            '    <Glyph value="cedilla"/>',
712            "  </MarkCoverage>",
713            "  <LigatureCoverage>",
714            '    <Glyph value="f_i"/>',
715            '    <Glyph value="c_t"/>',
716            "  </LigatureCoverage>",
717            "  <!-- ClassCount=2 -->",
718            "  <MarkArray>",
719            "    <!-- MarkCount=3 -->",
720            '    <MarkRecord index="0">',
721            '      <Class value="0"/>',
722            '      <MarkAnchor Format="1">',
723            '        <XCoordinate value="300"/>',
724            '        <YCoordinate value="700"/>',
725            "      </MarkAnchor>",
726            "    </MarkRecord>",
727            '    <MarkRecord index="1">',
728            '      <Class value="0"/>',
729            '      <MarkAnchor Format="1">',
730            '        <XCoordinate value="300"/>',
731            '        <YCoordinate value="700"/>',
732            "      </MarkAnchor>",
733            "    </MarkRecord>",
734            '    <MarkRecord index="2">',
735            '      <Class value="1"/>',
736            '      <MarkAnchor Format="1">',
737            '        <XCoordinate value="300"/>',
738            '        <YCoordinate value="-100"/>',
739            "      </MarkAnchor>",
740            "    </MarkRecord>",
741            "  </MarkArray>",
742            "  <LigatureArray>",
743            "    <!-- LigatureCount=2 -->",
744            '    <LigatureAttach index="0">',
745            "      <!-- ComponentCount=2 -->",
746            '      <ComponentRecord index="0">',
747            '        <LigatureAnchor index="0" empty="1"/>',
748            '        <LigatureAnchor index="1" empty="1"/>',
749            "      </ComponentRecord>",
750            '      <ComponentRecord index="1">',
751            '        <LigatureAnchor index="0" Format="1">',
752            '          <XCoordinate value="200"/>',
753            '          <YCoordinate value="400"/>',
754            "        </LigatureAnchor>",
755            '        <LigatureAnchor index="1" empty="1"/>',
756            "      </ComponentRecord>",
757            "    </LigatureAttach>",
758            '    <LigatureAttach index="1">',
759            "      <!-- ComponentCount=2 -->",
760            '      <ComponentRecord index="0">',
761            '        <LigatureAnchor index="0" Format="1">',
762            '          <XCoordinate value="500"/>',
763            '          <YCoordinate value="600"/>',
764            "        </LigatureAnchor>",
765            '        <LigatureAnchor index="1" Format="1">',
766            '          <XCoordinate value="500"/>',
767            '          <YCoordinate value="-20"/>',
768            "        </LigatureAnchor>",
769            "      </ComponentRecord>",
770            '      <ComponentRecord index="1">',
771            '        <LigatureAnchor index="0" Format="1">',
772            '          <XCoordinate value="1300"/>',
773            '          <YCoordinate value="800"/>',
774            "        </LigatureAnchor>",
775            '        <LigatureAnchor index="1" Format="1">',
776            '          <XCoordinate value="1300"/>',
777            '          <YCoordinate value="-20"/>',
778            "        </LigatureAnchor>",
779            "      </ComponentRecord>",
780            "    </LigatureAttach>",
781            "  </LigatureArray>",
782            "</MarkLigPos>",
783        ]
784
785    def test_buildMarkRecord(self):
786        rec = builder.buildMarkRecord(17, builder.buildAnchor(500, -20))
787        assert getXML(rec.toXML) == [
788            "<MarkRecord>",
789            '  <Class value="17"/>',
790            '  <MarkAnchor Format="1">',
791            '    <XCoordinate value="500"/>',
792            '    <YCoordinate value="-20"/>',
793            "  </MarkAnchor>",
794            "</MarkRecord>",
795        ]
796
797    def test_buildMark2Record(self):
798        a = builder.buildAnchor
799        rec = builder.buildMark2Record([a(500, -20), None, a(300, -15)])
800        assert getXML(rec.toXML) == [
801            "<Mark2Record>",
802            '  <Mark2Anchor index="0" Format="1">',
803            '    <XCoordinate value="500"/>',
804            '    <YCoordinate value="-20"/>',
805            "  </Mark2Anchor>",
806            '  <Mark2Anchor index="1" empty="1"/>',
807            '  <Mark2Anchor index="2" Format="1">',
808            '    <XCoordinate value="300"/>',
809            '    <YCoordinate value="-15"/>',
810            "  </Mark2Anchor>",
811            "</Mark2Record>",
812        ]
813
814    def test_buildPairPosClassesSubtable(self):
815        d20 = builder.buildValue({"XPlacement": -20})
816        d50 = builder.buildValue({"XPlacement": -50})
817        d0 = builder.buildValue({})
818        d8020 = builder.buildValue({"XPlacement": -80, "YPlacement": -20})
819        subtable = builder.buildPairPosClassesSubtable(
820            {
821                (tuple("A"), tuple(["zero"])): (d0, d50),
822                (tuple("A"), tuple(["one", "two"])): (None, d20),
823                (tuple(["B", "C"]), tuple(["zero"])): (d8020, d50),
824            },
825            self.GLYPHMAP,
826        )
827        assert getXML(subtable.toXML) == [
828            '<PairPos Format="2">',
829            "  <Coverage>",
830            '    <Glyph value="A"/>',
831            '    <Glyph value="B"/>',
832            '    <Glyph value="C"/>',
833            "  </Coverage>",
834            '  <ValueFormat1 value="3"/>',
835            '  <ValueFormat2 value="1"/>',
836            "  <ClassDef1>",
837            '    <ClassDef glyph="A" class="1"/>',
838            "  </ClassDef1>",
839            "  <ClassDef2>",
840            '    <ClassDef glyph="one" class="1"/>',
841            '    <ClassDef glyph="two" class="1"/>',
842            '    <ClassDef glyph="zero" class="2"/>',
843            "  </ClassDef2>",
844            "  <!-- Class1Count=2 -->",
845            "  <!-- Class2Count=3 -->",
846            '  <Class1Record index="0">',
847            '    <Class2Record index="0">',
848            "    </Class2Record>",
849            '    <Class2Record index="1">',
850            "    </Class2Record>",
851            '    <Class2Record index="2">',
852            '      <Value1 XPlacement="-80" YPlacement="-20"/>',
853            '      <Value2 XPlacement="-50"/>',
854            "    </Class2Record>",
855            "  </Class1Record>",
856            '  <Class1Record index="1">',
857            '    <Class2Record index="0">',
858            "    </Class2Record>",
859            '    <Class2Record index="1">',
860            '      <Value2 XPlacement="-20"/>',
861            "    </Class2Record>",
862            '    <Class2Record index="2">',
863            "      <Value1/>",
864            '      <Value2 XPlacement="-50"/>',
865            "    </Class2Record>",
866            "  </Class1Record>",
867            "</PairPos>",
868        ]
869
870    def test_buildPairPosGlyphs(self):
871        d50 = builder.buildValue({"XPlacement": -50})
872        d8020 = builder.buildValue({"XPlacement": -80, "YPlacement": -20})
873        subtables = builder.buildPairPosGlyphs(
874            {("A", "zero"): (None, d50), ("A", "one"): (d8020, d50)}, self.GLYPHMAP
875        )
876        assert sum([getXML(t.toXML) for t in subtables], []) == [
877            '<PairPos Format="1">',
878            "  <Coverage>",
879            '    <Glyph value="A"/>',
880            "  </Coverage>",
881            '  <ValueFormat1 value="0"/>',
882            '  <ValueFormat2 value="1"/>',
883            "  <!-- PairSetCount=1 -->",
884            '  <PairSet index="0">',
885            "    <!-- PairValueCount=1 -->",
886            '    <PairValueRecord index="0">',
887            '      <SecondGlyph value="zero"/>',
888            '      <Value2 XPlacement="-50"/>',
889            "    </PairValueRecord>",
890            "  </PairSet>",
891            "</PairPos>",
892            '<PairPos Format="1">',
893            "  <Coverage>",
894            '    <Glyph value="A"/>',
895            "  </Coverage>",
896            '  <ValueFormat1 value="3"/>',
897            '  <ValueFormat2 value="1"/>',
898            "  <!-- PairSetCount=1 -->",
899            '  <PairSet index="0">',
900            "    <!-- PairValueCount=1 -->",
901            '    <PairValueRecord index="0">',
902            '      <SecondGlyph value="one"/>',
903            '      <Value1 XPlacement="-80" YPlacement="-20"/>',
904            '      <Value2 XPlacement="-50"/>',
905            "    </PairValueRecord>",
906            "  </PairSet>",
907            "</PairPos>",
908        ]
909
910    def test_buildPairPosGlyphsSubtable(self):
911        d20 = builder.buildValue({"XPlacement": -20})
912        d50 = builder.buildValue({"XPlacement": -50})
913        d0 = builder.buildValue({})
914        d8020 = builder.buildValue({"XPlacement": -80, "YPlacement": -20})
915        subtable = builder.buildPairPosGlyphsSubtable(
916            {
917                ("A", "zero"): (d0, d50),
918                ("A", "one"): (None, d20),
919                ("B", "five"): (d8020, d50),
920            },
921            self.GLYPHMAP,
922        )
923        assert getXML(subtable.toXML) == [
924            '<PairPos Format="1">',
925            "  <Coverage>",
926            '    <Glyph value="A"/>',
927            '    <Glyph value="B"/>',
928            "  </Coverage>",
929            '  <ValueFormat1 value="3"/>',
930            '  <ValueFormat2 value="1"/>',
931            "  <!-- PairSetCount=2 -->",
932            '  <PairSet index="0">',
933            "    <!-- PairValueCount=2 -->",
934            '    <PairValueRecord index="0">',
935            '      <SecondGlyph value="zero"/>',
936            '      <Value2 XPlacement="-50"/>',
937            "    </PairValueRecord>",
938            '    <PairValueRecord index="1">',
939            '      <SecondGlyph value="one"/>',
940            '      <Value2 XPlacement="-20"/>',
941            "    </PairValueRecord>",
942            "  </PairSet>",
943            '  <PairSet index="1">',
944            "    <!-- PairValueCount=1 -->",
945            '    <PairValueRecord index="0">',
946            '      <SecondGlyph value="five"/>',
947            '      <Value1 XPlacement="-80" YPlacement="-20"/>',
948            '      <Value2 XPlacement="-50"/>',
949            "    </PairValueRecord>",
950            "  </PairSet>",
951            "</PairPos>",
952        ]
953
954    def test_buildSinglePos(self):
955        subtables = builder.buildSinglePos(
956            {
957                "one": builder.buildValue({"XPlacement": 500}),
958                "two": builder.buildValue({"XPlacement": 500}),
959                "three": builder.buildValue({"XPlacement": 200}),
960                "four": builder.buildValue({"XPlacement": 400}),
961                "five": builder.buildValue({"XPlacement": 500}),
962                "six": builder.buildValue({"YPlacement": -6}),
963            },
964            self.GLYPHMAP,
965        )
966        assert sum([getXML(t.toXML) for t in subtables], []) == [
967            '<SinglePos Format="2">',
968            "  <Coverage>",
969            '    <Glyph value="one"/>',
970            '    <Glyph value="two"/>',
971            '    <Glyph value="three"/>',
972            '    <Glyph value="four"/>',
973            '    <Glyph value="five"/>',
974            "  </Coverage>",
975            '  <ValueFormat value="1"/>',
976            "  <!-- ValueCount=5 -->",
977            '  <Value index="0" XPlacement="500"/>',
978            '  <Value index="1" XPlacement="500"/>',
979            '  <Value index="2" XPlacement="200"/>',
980            '  <Value index="3" XPlacement="400"/>',
981            '  <Value index="4" XPlacement="500"/>',
982            "</SinglePos>",
983            '<SinglePos Format="1">',
984            "  <Coverage>",
985            '    <Glyph value="six"/>',
986            "  </Coverage>",
987            '  <ValueFormat value="2"/>',
988            '  <Value YPlacement="-6"/>',
989            "</SinglePos>",
990        ]
991
992    def test_buildSinglePos_ValueFormat0(self):
993        subtables = builder.buildSinglePos(
994            {"zero": builder.buildValue({})}, self.GLYPHMAP
995        )
996        assert sum([getXML(t.toXML) for t in subtables], []) == [
997            '<SinglePos Format="1">',
998            "  <Coverage>",
999            '    <Glyph value="zero"/>',
1000            "  </Coverage>",
1001            '  <ValueFormat value="0"/>',
1002            "</SinglePos>",
1003        ]
1004
1005    def test_buildSinglePosSubtable_format1(self):
1006        subtable = builder.buildSinglePosSubtable(
1007            {
1008                "one": builder.buildValue({"XPlacement": 777}),
1009                "two": builder.buildValue({"XPlacement": 777}),
1010            },
1011            self.GLYPHMAP,
1012        )
1013        assert getXML(subtable.toXML) == [
1014            '<SinglePos Format="1">',
1015            "  <Coverage>",
1016            '    <Glyph value="one"/>',
1017            '    <Glyph value="two"/>',
1018            "  </Coverage>",
1019            '  <ValueFormat value="1"/>',
1020            '  <Value XPlacement="777"/>',
1021            "</SinglePos>",
1022        ]
1023
1024    def test_buildSinglePosSubtable_format2(self):
1025        subtable = builder.buildSinglePosSubtable(
1026            {
1027                "one": builder.buildValue({"XPlacement": 777}),
1028                "two": builder.buildValue({"YPlacement": -888}),
1029            },
1030            self.GLYPHMAP,
1031        )
1032        assert getXML(subtable.toXML) == [
1033            '<SinglePos Format="2">',
1034            "  <Coverage>",
1035            '    <Glyph value="one"/>',
1036            '    <Glyph value="two"/>',
1037            "  </Coverage>",
1038            '  <ValueFormat value="3"/>',
1039            "  <!-- ValueCount=2 -->",
1040            '  <Value index="0" XPlacement="777"/>',
1041            '  <Value index="1" YPlacement="-888"/>',
1042            "</SinglePos>",
1043        ]
1044
1045    def test_buildValue(self):
1046        value = builder.buildValue({"XPlacement": 7, "YPlacement": 23})
1047        func = lambda writer, font: value.toXML(writer, font, valueName="Val")
1048        assert getXML(func) == ['<Val XPlacement="7" YPlacement="23"/>']
1049
1050    def test_getLigatureKey(self):
1051        components = lambda s: [tuple(word) for word in s.split()]
1052        c = components("fi fl ff ffi fff")
1053        c.sort(key=builder._getLigatureKey)
1054        assert c == components("fff ffi ff fi fl")
1055
1056    def test_getSinglePosValueKey(self):
1057        device = builder.buildDevice({10: 1, 11: 3})
1058        a1 = builder.buildValue({"XPlacement": 500, "XPlaDevice": device})
1059        a2 = builder.buildValue({"XPlacement": 500, "XPlaDevice": device})
1060        b = builder.buildValue({"XPlacement": 500})
1061        keyA1 = builder._getSinglePosValueKey(a1)
1062        keyA2 = builder._getSinglePosValueKey(a1)
1063        keyB = builder._getSinglePosValueKey(b)
1064        assert keyA1 == keyA2
1065        assert hash(keyA1) == hash(keyA2)
1066        assert keyA1 != keyB
1067        assert hash(keyA1) != hash(keyB)
1068
1069
1070class ClassDefBuilderTest(object):
1071    def test_build_usingClass0(self):
1072        b = builder.ClassDefBuilder(useClass0=True)
1073        b.add({"aa", "bb"})
1074        b.add({"a", "b"})
1075        b.add({"c"})
1076        b.add({"e", "f", "g", "h"})
1077        cdef = b.build()
1078        assert isinstance(cdef, otTables.ClassDef)
1079        assert cdef.classDefs == {"a": 2, "b": 2, "c": 3, "aa": 1, "bb": 1}
1080
1081    def test_build_notUsingClass0(self):
1082        b = builder.ClassDefBuilder(useClass0=False)
1083        b.add({"a", "b"})
1084        b.add({"c"})
1085        b.add({"e", "f", "g", "h"})
1086        cdef = b.build()
1087        assert isinstance(cdef, otTables.ClassDef)
1088        assert cdef.classDefs == {
1089            "a": 2,
1090            "b": 2,
1091            "c": 3,
1092            "e": 1,
1093            "f": 1,
1094            "g": 1,
1095            "h": 1,
1096        }
1097
1098    def test_canAdd(self):
1099        b = builder.ClassDefBuilder(useClass0=True)
1100        b.add({"a", "b", "c", "d"})
1101        b.add({"e", "f"})
1102        assert b.canAdd({"a", "b", "c", "d"})
1103        assert b.canAdd({"e", "f"})
1104        assert b.canAdd({"g", "h", "i"})
1105        assert not b.canAdd({"b", "c", "d"})
1106        assert not b.canAdd({"a", "b", "c", "d", "e", "f"})
1107        assert not b.canAdd({"d", "e", "f"})
1108        assert not b.canAdd({"f"})
1109
1110
1111if __name__ == "__main__":
1112    import sys
1113
1114    sys.exit(pytest.main(sys.argv))
1115