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