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