xref: /aosp_15_r20/external/fonttools/Tests/ttLib/tables/otConverters_test.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1from fontTools.misc.loggingTools import CapturingLogHandler
2from fontTools.misc.testTools import FakeFont, makeXMLWriter
3from fontTools.misc.textTools import deHexStr
4import fontTools.ttLib.tables.otConverters as otConverters
5from fontTools.ttLib import newTable
6from fontTools.ttLib.tables.otBase import OTTableReader, OTTableWriter
7import unittest
8
9
10class Char64Test(unittest.TestCase):
11    font = FakeFont([])
12    converter = otConverters.Char64("char64", 0, None, None)
13
14    def test_read(self):
15        reader = OTTableReader(b"Hello\0junk after zero byte" + 100 * b"\0")
16        self.assertEqual(self.converter.read(reader, self.font, {}), "Hello")
17        self.assertEqual(reader.pos, 64)
18
19    def test_read_replace_not_ascii(self):
20        reader = OTTableReader(b"Hello \xE4 world" + 100 * b"\0")
21        with CapturingLogHandler(otConverters.log, "WARNING") as captor:
22            data = self.converter.read(reader, self.font, {})
23        self.assertEqual(data, "Hello � world")
24        self.assertEqual(reader.pos, 64)
25        self.assertIn(
26            'replaced non-ASCII characters in "Hello � world"',
27            [r.msg for r in captor.records],
28        )
29
30    def test_write(self):
31        writer = OTTableWriter()
32        self.converter.write(writer, self.font, {}, "Hello world")
33        self.assertEqual(writer.getData(), b"Hello world" + 53 * b"\0")
34
35    def test_write_replace_not_ascii(self):
36        writer = OTTableWriter()
37        with CapturingLogHandler(otConverters.log, "WARNING") as captor:
38            self.converter.write(writer, self.font, {}, "Hello ☃")
39        self.assertEqual(writer.getData(), b"Hello ?" + 57 * b"\0")
40        self.assertIn(
41            'replacing non-ASCII characters in "Hello ☃"',
42            [r.msg for r in captor.records],
43        )
44
45    def test_write_truncated(self):
46        writer = OTTableWriter()
47        with CapturingLogHandler(otConverters.log, "WARNING") as captor:
48            self.converter.write(writer, self.font, {}, "A" * 80)
49        self.assertEqual(writer.getData(), b"A" * 64)
50        self.assertIn(
51            'truncating overlong "' + "A" * 80 + '" to 64 bytes',
52            [r.msg for r in captor.records],
53        )
54
55    def test_xmlRead(self):
56        value = self.converter.xmlRead({"value": "Foo"}, [], self.font)
57        self.assertEqual(value, "Foo")
58
59    def test_xmlWrite(self):
60        writer = makeXMLWriter()
61        self.converter.xmlWrite(
62            writer, self.font, "Hello world", "Element", [("attr", "v")]
63        )
64        xml = writer.file.getvalue().decode("utf-8").rstrip()
65        self.assertEqual(xml, '<Element attr="v" value="Hello world"/>')
66
67
68class GlyphIDTest(unittest.TestCase):
69    font = FakeFont(".notdef A B C".split())
70    converter = otConverters.GlyphID("GlyphID", 0, None, None)
71
72    def test_readArray(self):
73        reader = OTTableReader(deHexStr("0002 0001 DEAD 0002"))
74        self.assertEqual(
75            self.converter.readArray(reader, self.font, {}, 4),
76            ["B", "A", "glyph57005", "B"],
77        )
78        self.assertEqual(reader.pos, 8)
79
80    def test_read(self):
81        reader = OTTableReader(deHexStr("0003"))
82        self.assertEqual(self.converter.read(reader, self.font, {}), "C")
83        self.assertEqual(reader.pos, 2)
84
85    def test_write(self):
86        writer = OTTableWriter()
87        self.converter.write(writer, self.font, {}, "B")
88        self.assertEqual(writer.getData(), deHexStr("0002"))
89
90
91class LongTest(unittest.TestCase):
92    font = FakeFont([])
93    converter = otConverters.Long("Long", 0, None, None)
94
95    def test_read(self):
96        reader = OTTableReader(deHexStr("FF0000EE"))
97        self.assertEqual(self.converter.read(reader, self.font, {}), -16776978)
98        self.assertEqual(reader.pos, 4)
99
100    def test_write(self):
101        writer = OTTableWriter()
102        self.converter.write(writer, self.font, {}, -16777213)
103        self.assertEqual(writer.getData(), deHexStr("FF000003"))
104
105    def test_xmlRead(self):
106        value = self.converter.xmlRead({"value": "314159"}, [], self.font)
107        self.assertEqual(value, 314159)
108
109    def test_xmlWrite(self):
110        writer = makeXMLWriter()
111        self.converter.xmlWrite(writer, self.font, 291, "Foo", [("attr", "v")])
112        xml = writer.file.getvalue().decode("utf-8").rstrip()
113        self.assertEqual(xml, '<Foo attr="v" value="291"/>')
114
115
116class NameIDTest(unittest.TestCase):
117    converter = otConverters.NameID("NameID", 0, None, None)
118
119    def makeFont(self):
120        nameTable = newTable("name")
121        nameTable.setName("Demibold Condensed", 0x123, 3, 0, 0x409)
122        nameTable.setName("Copyright 2018", 0, 3, 0, 0x409)
123        return {"name": nameTable}
124
125    def test_read(self):
126        font = self.makeFont()
127        reader = OTTableReader(deHexStr("0123"))
128        self.assertEqual(self.converter.read(reader, font, {}), 0x123)
129
130    def test_write(self):
131        writer = OTTableWriter()
132        self.converter.write(writer, self.makeFont(), {}, 0x123)
133        self.assertEqual(writer.getData(), deHexStr("0123"))
134
135    def test_xmlWrite(self):
136        writer = makeXMLWriter()
137        self.converter.xmlWrite(
138            writer, self.makeFont(), 291, "FooNameID", [("attr", "val")]
139        )
140        xml = writer.file.getvalue().decode("utf-8").rstrip()
141        self.assertEqual(
142            xml, '<FooNameID attr="val" value="291"/>  <!-- Demibold Condensed -->'
143        )
144
145    def test_xmlWrite_missingID(self):
146        writer = makeXMLWriter()
147        with CapturingLogHandler(otConverters.log, "WARNING") as captor:
148            self.converter.xmlWrite(
149                writer, self.makeFont(), 666, "Entity", [("attrib", "val")]
150            )
151        self.assertIn(
152            "name id 666 missing from name table", [r.msg for r in captor.records]
153        )
154        xml = writer.file.getvalue().decode("utf-8").rstrip()
155        self.assertEqual(
156            xml,
157            '<Entity attrib="val"' ' value="666"/>  <!-- missing from name table -->',
158        )
159
160    def test_xmlWrite_NULL(self):
161        writer = makeXMLWriter()
162        self.converter.xmlWrite(
163            writer, self.makeFont(), 0, "FooNameID", [("attr", "val")]
164        )
165        xml = writer.file.getvalue().decode("utf-8").rstrip()
166        self.assertEqual(xml, '<FooNameID attr="val" value="0"/>')
167
168
169class UInt8Test(unittest.TestCase):
170    font = FakeFont([])
171    converter = otConverters.UInt8("UInt8", 0, None, None)
172
173    def test_read(self):
174        reader = OTTableReader(deHexStr("FE"))
175        self.assertEqual(self.converter.read(reader, self.font, {}), 254)
176        self.assertEqual(reader.pos, 1)
177
178    def test_write(self):
179        writer = OTTableWriter()
180        self.converter.write(writer, self.font, {}, 253)
181        self.assertEqual(writer.getData(), deHexStr("FD"))
182
183    def test_xmlRead(self):
184        value = self.converter.xmlRead({"value": "254"}, [], self.font)
185        self.assertEqual(value, 254)
186
187    def test_xmlWrite(self):
188        writer = makeXMLWriter()
189        self.converter.xmlWrite(writer, self.font, 251, "Foo", [("attr", "v")])
190        xml = writer.file.getvalue().decode("utf-8").rstrip()
191        self.assertEqual(xml, '<Foo attr="v" value="251"/>')
192
193
194class AATLookupTest(unittest.TestCase):
195    font = FakeFont(".notdef A B C D E F G H A.alt B.alt".split())
196    converter = otConverters.AATLookup(
197        "AATLookup", 0, None, tableClass=otConverters.GlyphID
198    )
199
200    def __init__(self, methodName):
201        unittest.TestCase.__init__(self, methodName)
202        # Python 3 renamed assertRaisesRegexp to assertRaisesRegex,
203        # and fires deprecation warnings if a program uses the old name.
204        if not hasattr(self, "assertRaisesRegex"):
205            self.assertRaisesRegex = self.assertRaisesRegexp
206
207    def test_readFormat0(self):
208        reader = OTTableReader(deHexStr("0000 0000 0001 0002 0000 7D00 0001"))
209        self.assertEqual(
210            self.converter.read(reader, self.font, None),
211            {
212                ".notdef": ".notdef",
213                "A": "A",
214                "B": "B",
215                "C": ".notdef",
216                "D": "glyph32000",
217                "E": "A",
218            },
219        )
220
221    def test_readFormat2(self):
222        reader = OTTableReader(
223            deHexStr(
224                "0002 0006 0002 000C 0001 0006 "
225                "0002 0001 0003 "  # glyph A..B: map to C
226                "0007 0005 0008 "  # glyph E..G: map to H
227                "FFFF FFFF FFFF"
228            )
229        )  # end of search table
230        self.assertEqual(
231            self.converter.read(reader, self.font, None),
232            {
233                "A": "C",
234                "B": "C",
235                "E": "H",
236                "F": "H",
237                "G": "H",
238            },
239        )
240
241    def test_readFormat4(self):
242        reader = OTTableReader(
243            deHexStr(
244                "0004 0006 0003 000C 0001 0006 "
245                "0002 0001 001E "  # glyph 1..2: mapping at offset 0x1E
246                "0005 0004 001E "  # glyph 4..5: mapping at offset 0x1E
247                "FFFF FFFF FFFF "  # end of search table
248                "0007 0008"
249            )
250        )  # offset 0x18: glyphs [7, 8] = [G, H]
251        self.assertEqual(
252            self.converter.read(reader, self.font, None),
253            {
254                "A": "G",
255                "B": "H",
256                "D": "G",
257                "E": "H",
258            },
259        )
260
261    def test_readFormat6(self):
262        reader = OTTableReader(
263            deHexStr(
264                "0006 0004 0002 0008 0001 0004 "
265                "0003 0001 "  # C --> A
266                "0005 0002 "  # E --> B
267                "FFFF FFFF"
268            )
269        )  # end of search table
270        self.assertEqual(
271            self.converter.read(reader, self.font, None),
272            {
273                "C": "A",
274                "E": "B",
275            },
276        )
277
278    def test_readFormat8(self):
279        reader = OTTableReader(
280            deHexStr("0008 " "0003 0003 " "0007 0001 0002")  # first: C, count: 3
281        )  # [G, A, B]
282        self.assertEqual(
283            self.converter.read(reader, self.font, None),
284            {
285                "C": "G",
286                "D": "A",
287                "E": "B",
288            },
289        )
290
291    def test_readUnknownFormat(self):
292        reader = OTTableReader(deHexStr("0009"))
293        self.assertRaisesRegex(
294            AssertionError,
295            "unsupported lookup format: 9",
296            self.converter.read,
297            reader,
298            self.font,
299            None,
300        )
301
302    def test_writeFormat0(self):
303        writer = OTTableWriter()
304        font = FakeFont(".notdef A B C".split())
305        self.converter.write(
306            writer, font, {}, {".notdef": ".notdef", "A": "C", "B": "C", "C": "A"}
307        )
308        self.assertEqual(writer.getData(), deHexStr("0000 0000 0003 0003 0001"))
309
310    def test_writeFormat2(self):
311        writer = OTTableWriter()
312        font = FakeFont(".notdef A B C D E F G H".split())
313        self.converter.write(
314            writer,
315            font,
316            {},
317            {
318                "B": "C",
319                "C": "C",
320                "D": "C",
321                "E": "C",
322                "G": "A",
323                "H": "A",
324            },
325        )
326        self.assertEqual(
327            writer.getData(),
328            deHexStr(
329                "0002 "  # format=2
330                "0006 "  # binSrchHeader.unitSize=6
331                "0002 "  # binSrchHeader.nUnits=2
332                "000C "  # binSrchHeader.searchRange=12
333                "0001 "  # binSrchHeader.entrySelector=1
334                "0000 "  # binSrchHeader.rangeShift=0
335                "0005 0002 0003 "  # segments[0].lastGlyph=E, firstGlyph=B, value=C
336                "0008 0007 0001 "  # segments[1].lastGlyph=H, firstGlyph=G, value=A
337                "FFFF FFFF 0000 "  # segments[2]=<END>
338            ),
339        )
340
341    def test_writeFormat6(self):
342        writer = OTTableWriter()
343        font = FakeFont(".notdef A B C D E".split())
344        self.converter.write(
345            writer,
346            font,
347            {},
348            {
349                "A": "C",
350                "C": "B",
351                "D": "D",
352                "E": "E",
353            },
354        )
355        self.assertEqual(
356            writer.getData(),
357            deHexStr(
358                "0006 "  # format=6
359                "0004 "  # binSrchHeader.unitSize=4
360                "0004 "  # binSrchHeader.nUnits=4
361                "0010 "  # binSrchHeader.searchRange=16
362                "0002 "  # binSrchHeader.entrySelector=2
363                "0000 "  # binSrchHeader.rangeShift=0
364                "0001 0003 "  # entries[0].glyph=A, .value=C
365                "0003 0002 "  # entries[1].glyph=C, .value=B
366                "0004 0004 "  # entries[2].glyph=D, .value=D
367                "0005 0005 "  # entries[3].glyph=E, .value=E
368                "FFFF 0000 "  # entries[4]=<END>
369            ),
370        )
371
372    def test_writeFormat8(self):
373        writer = OTTableWriter()
374        font = FakeFont(".notdef A B C D E F G H".split())
375        self.converter.write(
376            writer,
377            font,
378            {},
379            {
380                "B": "B",
381                "C": "A",
382                "D": "B",
383                "E": "C",
384                "F": "B",
385                "G": "A",
386            },
387        )
388        self.assertEqual(
389            writer.getData(),
390            deHexStr(
391                "0008 "  # format=8
392                "0002 "  # firstGlyph=B
393                "0006 "  # glyphCount=6
394                "0002 0001 0002 0003 0002 0001"  # valueArray=[B, A, B, C, B, A]
395            ),
396        )
397
398    def test_xmlRead(self):
399        value = self.converter.xmlRead(
400            {},
401            [
402                ("Lookup", {"glyph": "A", "value": "A.alt"}, []),
403                ("Lookup", {"glyph": "B", "value": "B.alt"}, []),
404            ],
405            self.font,
406        )
407        self.assertEqual(value, {"A": "A.alt", "B": "B.alt"})
408
409    def test_xmlWrite(self):
410        writer = makeXMLWriter()
411        self.converter.xmlWrite(
412            writer,
413            self.font,
414            value={"A": "A.alt", "B": "B.alt"},
415            name="Foo",
416            attrs=[("attr", "val")],
417        )
418        xml = writer.file.getvalue().decode("utf-8").splitlines()
419        self.assertEqual(
420            xml,
421            [
422                '<Foo attr="val">',
423                '  <Lookup glyph="A" value="A.alt"/>',
424                '  <Lookup glyph="B" value="B.alt"/>',
425                "</Foo>",
426            ],
427        )
428
429
430class LazyListTest(unittest.TestCase):
431    def test_slice(self):
432        ll = otConverters._LazyList([10, 11, 12, 13])
433        sl = ll[:]
434
435        self.assertIsNot(sl, ll)
436        self.assertIsInstance(sl, list)
437        self.assertEqual([10, 11, 12, 13], sl)
438
439        self.assertEqual([11, 12], ll[1:3])
440
441    def test_getitem(self):
442        count = 2
443        reader = OTTableReader(b"\x00\xFE\xFF\x00\x00\x00", offset=1)
444        converter = otConverters.UInt8("UInt8", 0, None, None)
445        recordSize = converter.staticSize
446        l = otConverters._LazyList()
447        l.reader = reader
448        l.pos = l.reader.pos
449        l.font = None
450        l.conv = converter
451        l.recordSize = recordSize
452        l.extend(otConverters._MissingItem([i]) for i in range(count))
453        reader.advance(count * recordSize)
454
455        self.assertEqual(l[0], 254)
456        self.assertEqual(l[1], 255)
457
458    def test_add_both_LazyList(self):
459        ll1 = otConverters._LazyList([1])
460        ll2 = otConverters._LazyList([2])
461
462        l3 = ll1 + ll2
463
464        self.assertIsInstance(l3, list)
465        self.assertEqual([1, 2], l3)
466
467    def test_add_LazyList_and_list(self):
468        ll1 = otConverters._LazyList([1])
469        l2 = [2]
470
471        l3 = ll1 + l2
472
473        self.assertIsInstance(l3, list)
474        self.assertEqual([1, 2], l3)
475
476    def test_add_not_implemented(self):
477        with self.assertRaises(TypeError):
478            otConverters._LazyList() + 0
479        with self.assertRaises(TypeError):
480            otConverters._LazyList() + tuple()
481
482    def test_radd_list_and_LazyList(self):
483        l1 = [1]
484        ll2 = otConverters._LazyList([2])
485
486        l3 = l1 + ll2
487
488        self.assertIsInstance(l3, list)
489        self.assertEqual([1, 2], l3)
490
491    def test_radd_not_implemented(self):
492        with self.assertRaises(TypeError):
493            0 + otConverters._LazyList()
494        with self.assertRaises(TypeError):
495            tuple() + otConverters._LazyList()
496
497
498if __name__ == "__main__":
499    import sys
500
501    sys.exit(unittest.main())
502