1import io 2import struct 3from fontTools.misc.fixedTools import floatToFixed, fixedToFloat 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"), {"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 self.GLYPHMAP, 923 ) 924 925 assert getXML(subtable.toXML) == [ 926 '<PairPos Format="1">', 927 " <Coverage>", 928 ' <Glyph value="A"/>', 929 ' <Glyph value="B"/>', 930 " </Coverage>", 931 ' <ValueFormat1 value="3"/>', 932 ' <ValueFormat2 value="1"/>', 933 " <!-- PairSetCount=2 -->", 934 ' <PairSet index="0">', 935 " <!-- PairValueCount=2 -->", 936 ' <PairValueRecord index="0">', 937 ' <SecondGlyph value="zero"/>', 938 ' <Value1 XPlacement="0" YPlacement="0"/>', 939 ' <Value2 XPlacement="-50"/>', 940 " </PairValueRecord>", 941 ' <PairValueRecord index="1">', 942 ' <SecondGlyph value="one"/>', 943 ' <Value1 XPlacement="0" YPlacement="0"/>', 944 ' <Value2 XPlacement="-20"/>', 945 " </PairValueRecord>", 946 " </PairSet>", 947 ' <PairSet index="1">', 948 " <!-- PairValueCount=1 -->", 949 ' <PairValueRecord index="0">', 950 ' <SecondGlyph value="five"/>', 951 ' <Value1 XPlacement="-80" YPlacement="-20"/>', 952 ' <Value2 XPlacement="-50"/>', 953 " </PairValueRecord>", 954 " </PairSet>", 955 "</PairPos>", 956 ] 957 958 def test_buildSinglePos(self): 959 subtables = builder.buildSinglePos( 960 { 961 "one": builder.buildValue({"XPlacement": 500}), 962 "two": builder.buildValue({"XPlacement": 500}), 963 "three": builder.buildValue({"XPlacement": 200}), 964 "four": builder.buildValue({"XPlacement": 400}), 965 "five": builder.buildValue({"XPlacement": 500}), 966 "six": builder.buildValue({"YPlacement": -6}), 967 }, 968 self.GLYPHMAP, 969 ) 970 assert sum([getXML(t.toXML) for t in subtables], []) == [ 971 '<SinglePos Format="2">', 972 " <Coverage>", 973 ' <Glyph value="one"/>', 974 ' <Glyph value="two"/>', 975 ' <Glyph value="three"/>', 976 ' <Glyph value="four"/>', 977 ' <Glyph value="five"/>', 978 " </Coverage>", 979 ' <ValueFormat value="1"/>', 980 " <!-- ValueCount=5 -->", 981 ' <Value index="0" XPlacement="500"/>', 982 ' <Value index="1" XPlacement="500"/>', 983 ' <Value index="2" XPlacement="200"/>', 984 ' <Value index="3" XPlacement="400"/>', 985 ' <Value index="4" XPlacement="500"/>', 986 "</SinglePos>", 987 '<SinglePos Format="1">', 988 " <Coverage>", 989 ' <Glyph value="six"/>', 990 " </Coverage>", 991 ' <ValueFormat value="2"/>', 992 ' <Value YPlacement="-6"/>', 993 "</SinglePos>", 994 ] 995 996 def test_buildSinglePos_ValueFormat0(self): 997 subtables = builder.buildSinglePos( 998 {"zero": builder.buildValue({})}, self.GLYPHMAP 999 ) 1000 assert sum([getXML(t.toXML) for t in subtables], []) == [ 1001 '<SinglePos Format="1">', 1002 " <Coverage>", 1003 ' <Glyph value="zero"/>', 1004 " </Coverage>", 1005 ' <ValueFormat value="0"/>', 1006 "</SinglePos>", 1007 ] 1008 1009 def test_buildSinglePosSubtable_format1(self): 1010 subtable = builder.buildSinglePosSubtable( 1011 { 1012 "one": builder.buildValue({"XPlacement": 777}), 1013 "two": builder.buildValue({"XPlacement": 777}), 1014 }, 1015 self.GLYPHMAP, 1016 ) 1017 assert getXML(subtable.toXML) == [ 1018 '<SinglePos Format="1">', 1019 " <Coverage>", 1020 ' <Glyph value="one"/>', 1021 ' <Glyph value="two"/>', 1022 " </Coverage>", 1023 ' <ValueFormat value="1"/>', 1024 ' <Value XPlacement="777"/>', 1025 "</SinglePos>", 1026 ] 1027 1028 def test_buildSinglePosSubtable_format2(self): 1029 subtable = builder.buildSinglePosSubtable( 1030 { 1031 "one": builder.buildValue({"XPlacement": 777}), 1032 "two": builder.buildValue({"YPlacement": -888}), 1033 }, 1034 self.GLYPHMAP, 1035 ) 1036 assert getXML(subtable.toXML) == [ 1037 '<SinglePos Format="2">', 1038 " <Coverage>", 1039 ' <Glyph value="one"/>', 1040 ' <Glyph value="two"/>', 1041 " </Coverage>", 1042 ' <ValueFormat value="3"/>', 1043 " <!-- ValueCount=2 -->", 1044 ' <Value index="0" XPlacement="777" YPlacement="0"/>', 1045 ' <Value index="1" XPlacement="0" YPlacement="-888"/>', 1046 "</SinglePos>", 1047 ] 1048 1049 def test_buildValue(self): 1050 value = builder.buildValue({"XPlacement": 7, "YPlacement": 23}) 1051 func = lambda writer, font: value.toXML(writer, font, valueName="Val") 1052 assert getXML(func) == ['<Val XPlacement="7" YPlacement="23"/>'] 1053 1054 def test_getLigatureSortKey(self): 1055 components = lambda s: [tuple(word) for word in s.split()] 1056 c = components("fi fl ff ffi fff") 1057 c.sort(key=otTables.LigatureSubst._getLigatureSortKey) 1058 assert c == components("ffi fff fi fl ff") 1059 1060 def test_getSinglePosValueKey(self): 1061 device = builder.buildDevice({10: 1, 11: 3}) 1062 a1 = builder.buildValue({"XPlacement": 500, "XPlaDevice": device}) 1063 a2 = builder.buildValue({"XPlacement": 500, "XPlaDevice": device}) 1064 b = builder.buildValue({"XPlacement": 500}) 1065 keyA1 = builder._getSinglePosValueKey(a1) 1066 keyA2 = builder._getSinglePosValueKey(a1) 1067 keyB = builder._getSinglePosValueKey(b) 1068 assert keyA1 == keyA2 1069 assert hash(keyA1) == hash(keyA2) 1070 assert keyA1 != keyB 1071 assert hash(keyA1) != hash(keyB) 1072 1073 1074class ClassDefBuilderTest(object): 1075 def test_build_usingClass0(self): 1076 b = builder.ClassDefBuilder(useClass0=True) 1077 b.add({"aa", "bb"}) 1078 b.add({"a", "b"}) 1079 b.add({"c"}) 1080 b.add({"e", "f", "g", "h"}) 1081 cdef = b.build() 1082 assert isinstance(cdef, otTables.ClassDef) 1083 assert cdef.classDefs == {"a": 1, "b": 1, "c": 3, "aa": 2, "bb": 2} 1084 1085 def test_build_notUsingClass0(self): 1086 b = builder.ClassDefBuilder(useClass0=False) 1087 b.add({"a", "b"}) 1088 b.add({"c"}) 1089 b.add({"e", "f", "g", "h"}) 1090 cdef = b.build() 1091 assert isinstance(cdef, otTables.ClassDef) 1092 assert cdef.classDefs == { 1093 "a": 2, 1094 "b": 2, 1095 "c": 3, 1096 "e": 1, 1097 "f": 1, 1098 "g": 1, 1099 "h": 1, 1100 } 1101 1102 def test_canAdd(self): 1103 b = builder.ClassDefBuilder(useClass0=True) 1104 b.add({"a", "b", "c", "d"}) 1105 b.add({"e", "f"}) 1106 assert b.canAdd({"a", "b", "c", "d"}) 1107 assert b.canAdd({"e", "f"}) 1108 assert b.canAdd({"g", "h", "i"}) 1109 assert not b.canAdd({"b", "c", "d"}) 1110 assert not b.canAdd({"a", "b", "c", "d", "e", "f"}) 1111 assert not b.canAdd({"d", "e", "f"}) 1112 assert not b.canAdd({"f"}) 1113 1114 def test_add_exception(self): 1115 b = builder.ClassDefBuilder(useClass0=True) 1116 b.add({"a", "b", "c"}) 1117 with pytest.raises(error.OpenTypeLibError): 1118 b.add({"a", "d"}) 1119 1120 1121buildStatTable_test_data = [ 1122 ( 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"), 1131 ], 1132 ) 1133 ], 1134 None, 1135 "Regular", 1136 [ 1137 " <STAT>", 1138 ' <Version value="0x00010001"/>', 1139 ' <DesignAxisRecordSize value="8"/>', 1140 " <!-- DesignAxisCount=1 -->", 1141 " <DesignAxisRecord>", 1142 ' <Axis index="0">', 1143 ' <AxisTag value="wght"/>', 1144 ' <AxisNameID value="257"/> <!-- Weight -->', 1145 ' <AxisOrdering value="0"/>', 1146 " </Axis>", 1147 " </DesignAxisRecord>", 1148 " <!-- AxisValueCount=3 -->", 1149 " <AxisValueArray>", 1150 ' <AxisValue index="0" Format="1">', 1151 ' <AxisIndex value="0"/>', 1152 ' <Flags value="0"/>', 1153 ' <ValueNameID value="258"/> <!-- Thin -->', 1154 ' <Value value="100.0"/>', 1155 " </AxisValue>", 1156 ' <AxisValue index="1" Format="1">', 1157 ' <AxisIndex value="0"/>', 1158 ' <Flags value="2"/> <!-- ElidableAxisValueName -->', 1159 ' <ValueNameID value="256"/> <!-- Regular -->', 1160 ' <Value value="400.0"/>', 1161 " </AxisValue>", 1162 ' <AxisValue index="2" Format="1">', 1163 ' <AxisIndex value="0"/>', 1164 ' <Flags value="0"/>', 1165 ' <ValueNameID value="259"/> <!-- Black -->', 1166 ' <Value value="900.0"/>', 1167 " </AxisValue>", 1168 " </AxisValueArray>", 1169 ' <ElidedFallbackNameID value="256"/> <!-- Regular -->', 1170 " </STAT>", 1171 ], 1172 ), 1173 ( 1174 [ 1175 dict( 1176 tag="wght", 1177 name=dict(en="Weight", nl="Gewicht"), 1178 values=[ 1179 dict(value=100, name=dict(en="Thin", nl="Dun")), 1180 dict(value=400, name="Regular", flags=0x2), 1181 dict(value=900, name="Black"), 1182 ], 1183 ), 1184 dict( 1185 tag="wdth", 1186 name="Width", 1187 values=[ 1188 dict(value=50, name="Condensed"), 1189 dict(value=100, name="Regular", flags=0x2), 1190 dict(value=200, name="Extended"), 1191 ], 1192 ), 1193 ], 1194 None, 1195 2, 1196 [ 1197 " <STAT>", 1198 ' <Version value="0x00010001"/>', 1199 ' <DesignAxisRecordSize value="8"/>', 1200 " <!-- DesignAxisCount=2 -->", 1201 " <DesignAxisRecord>", 1202 ' <Axis index="0">', 1203 ' <AxisTag value="wght"/>', 1204 ' <AxisNameID value="256"/> <!-- Weight -->', 1205 ' <AxisOrdering value="0"/>', 1206 " </Axis>", 1207 ' <Axis index="1">', 1208 ' <AxisTag value="wdth"/>', 1209 ' <AxisNameID value="260"/> <!-- Width -->', 1210 ' <AxisOrdering value="1"/>', 1211 " </Axis>", 1212 " </DesignAxisRecord>", 1213 " <!-- AxisValueCount=6 -->", 1214 " <AxisValueArray>", 1215 ' <AxisValue index="0" Format="1">', 1216 ' <AxisIndex value="0"/>', 1217 ' <Flags value="0"/>', 1218 ' <ValueNameID value="257"/> <!-- Thin -->', 1219 ' <Value value="100.0"/>', 1220 " </AxisValue>", 1221 ' <AxisValue index="1" Format="1">', 1222 ' <AxisIndex value="0"/>', 1223 ' <Flags value="2"/> <!-- ElidableAxisValueName -->', 1224 ' <ValueNameID value="258"/> <!-- Regular -->', 1225 ' <Value value="400.0"/>', 1226 " </AxisValue>", 1227 ' <AxisValue index="2" Format="1">', 1228 ' <AxisIndex value="0"/>', 1229 ' <Flags value="0"/>', 1230 ' <ValueNameID value="259"/> <!-- Black -->', 1231 ' <Value value="900.0"/>', 1232 " </AxisValue>", 1233 ' <AxisValue index="3" Format="1">', 1234 ' <AxisIndex value="1"/>', 1235 ' <Flags value="0"/>', 1236 ' <ValueNameID value="261"/> <!-- Condensed -->', 1237 ' <Value value="50.0"/>', 1238 " </AxisValue>", 1239 ' <AxisValue index="4" Format="1">', 1240 ' <AxisIndex value="1"/>', 1241 ' <Flags value="2"/> <!-- ElidableAxisValueName -->', 1242 ' <ValueNameID value="258"/> <!-- Regular -->', 1243 ' <Value value="100.0"/>', 1244 " </AxisValue>", 1245 ' <AxisValue index="5" Format="1">', 1246 ' <AxisIndex value="1"/>', 1247 ' <Flags value="0"/>', 1248 ' <ValueNameID value="262"/> <!-- Extended -->', 1249 ' <Value value="200.0"/>', 1250 " </AxisValue>", 1251 " </AxisValueArray>", 1252 ' <ElidedFallbackNameID value="2"/> <!-- missing from name table -->', 1253 " </STAT>", 1254 ], 1255 ), 1256 ( 1257 [ 1258 dict( 1259 tag="wght", 1260 name="Weight", 1261 values=[ 1262 dict(value=400, name="Regular", flags=0x2), 1263 dict(value=600, linkedValue=650, name="Bold"), 1264 ], 1265 ) 1266 ], 1267 None, 1268 18, 1269 [ 1270 " <STAT>", 1271 ' <Version value="0x00010001"/>', 1272 ' <DesignAxisRecordSize value="8"/>', 1273 " <!-- DesignAxisCount=1 -->", 1274 " <DesignAxisRecord>", 1275 ' <Axis index="0">', 1276 ' <AxisTag value="wght"/>', 1277 ' <AxisNameID value="256"/> <!-- Weight -->', 1278 ' <AxisOrdering value="0"/>', 1279 " </Axis>", 1280 " </DesignAxisRecord>", 1281 " <!-- AxisValueCount=2 -->", 1282 " <AxisValueArray>", 1283 ' <AxisValue index="0" Format="1">', 1284 ' <AxisIndex value="0"/>', 1285 ' <Flags value="2"/> <!-- ElidableAxisValueName -->', 1286 ' <ValueNameID value="257"/> <!-- Regular -->', 1287 ' <Value value="400.0"/>', 1288 " </AxisValue>", 1289 ' <AxisValue index="1" Format="3">', 1290 ' <AxisIndex value="0"/>', 1291 ' <Flags value="0"/>', 1292 ' <ValueNameID value="258"/> <!-- Bold -->', 1293 ' <Value value="600.0"/>', 1294 ' <LinkedValue value="650.0"/>', 1295 " </AxisValue>", 1296 " </AxisValueArray>", 1297 ' <ElidedFallbackNameID value="18"/> <!-- missing from name table -->', 1298 " </STAT>", 1299 ], 1300 ), 1301 ( 1302 [ 1303 dict( 1304 tag="opsz", 1305 name="Optical Size", 1306 values=[ 1307 dict(nominalValue=6, rangeMaxValue=10, name="Small"), 1308 dict( 1309 rangeMinValue=10, 1310 nominalValue=14, 1311 rangeMaxValue=24, 1312 name="Text", 1313 flags=0x2, 1314 ), 1315 dict(rangeMinValue=24, nominalValue=600, name="Display"), 1316 ], 1317 ) 1318 ], 1319 None, 1320 2, 1321 [ 1322 " <STAT>", 1323 ' <Version value="0x00010001"/>', 1324 ' <DesignAxisRecordSize value="8"/>', 1325 " <!-- DesignAxisCount=1 -->", 1326 " <DesignAxisRecord>", 1327 ' <Axis index="0">', 1328 ' <AxisTag value="opsz"/>', 1329 ' <AxisNameID value="256"/> <!-- Optical Size -->', 1330 ' <AxisOrdering value="0"/>', 1331 " </Axis>", 1332 " </DesignAxisRecord>", 1333 " <!-- AxisValueCount=3 -->", 1334 " <AxisValueArray>", 1335 ' <AxisValue index="0" Format="2">', 1336 ' <AxisIndex value="0"/>', 1337 ' <Flags value="0"/>', 1338 ' <ValueNameID value="257"/> <!-- Small -->', 1339 ' <NominalValue value="6.0"/>', 1340 ' <RangeMinValue value="-32768.0"/>', 1341 ' <RangeMaxValue value="10.0"/>', 1342 " </AxisValue>", 1343 ' <AxisValue index="1" Format="2">', 1344 ' <AxisIndex value="0"/>', 1345 ' <Flags value="2"/> <!-- ElidableAxisValueName -->', 1346 ' <ValueNameID value="258"/> <!-- Text -->', 1347 ' <NominalValue value="14.0"/>', 1348 ' <RangeMinValue value="10.0"/>', 1349 ' <RangeMaxValue value="24.0"/>', 1350 " </AxisValue>", 1351 ' <AxisValue index="2" Format="2">', 1352 ' <AxisIndex value="0"/>', 1353 ' <Flags value="0"/>', 1354 ' <ValueNameID value="259"/> <!-- Display -->', 1355 ' <NominalValue value="600.0"/>', 1356 ' <RangeMinValue value="24.0"/>', 1357 ' <RangeMaxValue value="32767.99998"/>', 1358 " </AxisValue>", 1359 " </AxisValueArray>", 1360 ' <ElidedFallbackNameID value="2"/> <!-- missing from name table -->', 1361 " </STAT>", 1362 ], 1363 ), 1364 ( 1365 [ 1366 dict(tag="wght", name="Weight", ordering=1, values=[]), 1367 dict( 1368 tag="ABCD", 1369 name="ABCDTest", 1370 ordering=0, 1371 values=[dict(value=100, name="Regular", flags=0x2)], 1372 ), 1373 ], 1374 [dict(location=dict(wght=300, ABCD=100), name="Regular ABCD")], 1375 18, 1376 [ 1377 " <STAT>", 1378 ' <Version value="0x00010002"/>', 1379 ' <DesignAxisRecordSize value="8"/>', 1380 " <!-- DesignAxisCount=2 -->", 1381 " <DesignAxisRecord>", 1382 ' <Axis index="0">', 1383 ' <AxisTag value="wght"/>', 1384 ' <AxisNameID value="256"/> <!-- Weight -->', 1385 ' <AxisOrdering value="1"/>', 1386 " </Axis>", 1387 ' <Axis index="1">', 1388 ' <AxisTag value="ABCD"/>', 1389 ' <AxisNameID value="257"/> <!-- ABCDTest -->', 1390 ' <AxisOrdering value="0"/>', 1391 " </Axis>", 1392 " </DesignAxisRecord>", 1393 " <!-- AxisValueCount=2 -->", 1394 " <AxisValueArray>", 1395 ' <AxisValue index="0" Format="4">', 1396 " <!-- AxisCount=2 -->", 1397 ' <Flags value="0"/>', 1398 ' <ValueNameID value="259"/> <!-- Regular ABCD -->', 1399 ' <AxisValueRecord index="0">', 1400 ' <AxisIndex value="0"/>', 1401 ' <Value value="300.0"/>', 1402 " </AxisValueRecord>", 1403 ' <AxisValueRecord index="1">', 1404 ' <AxisIndex value="1"/>', 1405 ' <Value value="100.0"/>', 1406 " </AxisValueRecord>", 1407 " </AxisValue>", 1408 ' <AxisValue index="1" Format="1">', 1409 ' <AxisIndex value="1"/>', 1410 ' <Flags value="2"/> <!-- ElidableAxisValueName -->', 1411 ' <ValueNameID value="258"/> <!-- Regular -->', 1412 ' <Value value="100.0"/>', 1413 " </AxisValue>", 1414 " </AxisValueArray>", 1415 ' <ElidedFallbackNameID value="18"/> <!-- missing from name table -->', 1416 " </STAT>", 1417 ], 1418 ), 1419] 1420 1421 1422@pytest.mark.parametrize( 1423 "axes, axisValues, elidedFallbackName, expected_ttx", buildStatTable_test_data 1424) 1425def test_buildStatTable(axes, axisValues, elidedFallbackName, expected_ttx): 1426 font = ttLib.TTFont() 1427 font["name"] = ttLib.newTable("name") 1428 font["name"].names = [] 1429 # https://github.com/fonttools/fonttools/issues/1985 1430 # Add nameID < 256 that matches a test axis name, to test whether 1431 # the nameID is not reused: AxisNameIDs must be > 255 according 1432 # to the spec. 1433 font["name"].addMultilingualName(dict(en="ABCDTest"), nameID=6) 1434 builder.buildStatTable(font, axes, axisValues, elidedFallbackName) 1435 f = io.StringIO() 1436 font.saveXML(f, tables=["STAT"]) 1437 ttx = f.getvalue().splitlines() 1438 ttx = ttx[3:-2] # strip XML header and <ttFont> element 1439 assert expected_ttx == ttx 1440 # Compile and round-trip 1441 f = io.BytesIO() 1442 font.save(f) 1443 font = ttLib.TTFont(f) 1444 f = io.StringIO() 1445 font.saveXML(f, tables=["STAT"]) 1446 ttx = f.getvalue().splitlines() 1447 ttx = ttx[3:-2] # strip XML header and <ttFont> element 1448 assert expected_ttx == ttx 1449 1450 1451def test_buildStatTable_platform_specific_names(): 1452 # PR: https://github.com/fonttools/fonttools/pull/2528 1453 # Introduce new 'platform' feature for creating a STAT table. 1454 # Set windowsNames and or macNames to create name table entries 1455 # in the specified platforms 1456 font_obj = ttLib.TTFont() 1457 font_obj["name"] = ttLib.newTable("name") 1458 font_obj["name"].names = [] 1459 1460 wght_values = [ 1461 dict(nominalValue=200, rangeMinValue=200, rangeMaxValue=250, name="ExtraLight"), 1462 dict(nominalValue=300, rangeMinValue=250, rangeMaxValue=350, name="Light"), 1463 dict( 1464 nominalValue=400, 1465 rangeMinValue=350, 1466 rangeMaxValue=450, 1467 name="Regular", 1468 flags=0x2, 1469 ), 1470 dict(nominalValue=500, rangeMinValue=450, rangeMaxValue=650, name="Medium"), 1471 dict(nominalValue=700, rangeMinValue=650, rangeMaxValue=750, name="Bold"), 1472 dict(nominalValue=800, rangeMinValue=750, rangeMaxValue=850, name="ExtraBold"), 1473 dict(nominalValue=900, rangeMinValue=850, rangeMaxValue=900, name="Black"), 1474 ] 1475 1476 AXES = [ 1477 dict( 1478 tag="wght", 1479 name="Weight", 1480 ordering=1, 1481 values=wght_values, 1482 ), 1483 ] 1484 1485 font_obj["name"].setName("ExtraLight", 260, 3, 1, 0x409) 1486 font_obj["name"].setName("Light", 261, 3, 1, 0x409) 1487 font_obj["name"].setName("Regular", 262, 3, 1, 0x409) 1488 font_obj["name"].setName("Medium", 263, 3, 1, 0x409) 1489 font_obj["name"].setName("Bold", 264, 3, 1, 0x409) 1490 font_obj["name"].setName("ExtraBold", 265, 3, 1, 0x409) 1491 font_obj["name"].setName("Black", 266, 3, 1, 0x409) 1492 1493 font_obj["name"].setName("Weight", 270, 3, 1, 0x409) 1494 1495 expected_names = [x.string for x in font_obj["name"].names] 1496 1497 builder.buildStatTable(font_obj, AXES, windowsNames=True, macNames=False) 1498 actual_names = [x.string for x in font_obj["name"].names] 1499 1500 # no new name records were added by buildStatTable 1501 # because windows-only names with the same strings were already present 1502 assert expected_names == actual_names 1503 1504 font_obj["name"].removeNames(nameID=270) 1505 expected_names = [x.string for x in font_obj["name"].names] + ["Weight"] 1506 1507 builder.buildStatTable(font_obj, AXES, windowsNames=True, macNames=False) 1508 actual_names = [x.string for x in font_obj["name"].names] 1509 # One new name records 'Weight' were added by buildStatTable 1510 assert expected_names == actual_names 1511 1512 builder.buildStatTable(font_obj, AXES, windowsNames=True, macNames=True) 1513 actual_names = [x.string for x in font_obj["name"].names] 1514 expected_names = [ 1515 "Weight", 1516 "Weight", 1517 "Weight", 1518 "ExtraLight", 1519 "ExtraLight", 1520 "ExtraLight", 1521 "Light", 1522 "Light", 1523 "Light", 1524 "Regular", 1525 "Regular", 1526 "Regular", 1527 "Medium", 1528 "Medium", 1529 "Medium", 1530 "Bold", 1531 "Bold", 1532 "Bold", 1533 "ExtraBold", 1534 "ExtraBold", 1535 "ExtraBold", 1536 "Black", 1537 "Black", 1538 "Black", 1539 ] 1540 # Because there is an inconsistency in the names add new name IDs 1541 # for each platform -> windowsNames=True, macNames=True 1542 assert sorted(expected_names) == sorted(actual_names) 1543 1544 1545def test_stat_infinities(): 1546 negInf = floatToFixed(builder.AXIS_VALUE_NEGATIVE_INFINITY, 16) 1547 assert struct.pack(">l", negInf) == b"\x80\x00\x00\x00" 1548 posInf = floatToFixed(builder.AXIS_VALUE_POSITIVE_INFINITY, 16) 1549 assert struct.pack(">l", posInf) == b"\x7f\xff\xff\xff" 1550 1551 1552def test_buildMathTable_empty(): 1553 ttFont = ttLib.TTFont() 1554 ttFont.setGlyphOrder([]) 1555 builder.buildMathTable(ttFont) 1556 1557 assert "MATH" in ttFont 1558 mathTable = ttFont["MATH"].table 1559 assert mathTable.Version == 0x00010000 1560 1561 assert mathTable.MathConstants is None 1562 assert mathTable.MathGlyphInfo is None 1563 assert mathTable.MathVariants is None 1564 1565 1566def test_buildMathTable_constants(): 1567 ttFont = ttLib.TTFont() 1568 ttFont.setGlyphOrder([]) 1569 constants = { 1570 "AccentBaseHeight": 516, 1571 "AxisHeight": 262, 1572 "DelimitedSubFormulaMinHeight": 1500, 1573 "DisplayOperatorMinHeight": 2339, 1574 "FlattenedAccentBaseHeight": 698, 1575 "FractionDenomDisplayStyleGapMin": 198, 1576 "FractionDenominatorDisplayStyleShiftDown": 698, 1577 "FractionDenominatorGapMin": 66, 1578 "FractionDenominatorShiftDown": 465, 1579 "FractionNumDisplayStyleGapMin": 198, 1580 "FractionNumeratorDisplayStyleShiftUp": 774, 1581 "FractionNumeratorGapMin": 66, 1582 "FractionNumeratorShiftUp": 516, 1583 "FractionRuleThickness": 66, 1584 "LowerLimitBaselineDropMin": 585, 1585 "LowerLimitGapMin": 132, 1586 "MathLeading": 300, 1587 "OverbarExtraAscender": 66, 1588 "OverbarRuleThickness": 66, 1589 "OverbarVerticalGap": 198, 1590 "RadicalDegreeBottomRaisePercent": 75, 1591 "RadicalDisplayStyleVerticalGap": 195, 1592 "RadicalExtraAscender": 66, 1593 "RadicalKernAfterDegree": -556, 1594 "RadicalKernBeforeDegree": 278, 1595 "RadicalRuleThickness": 66, 1596 "RadicalVerticalGap": 82, 1597 "ScriptPercentScaleDown": 70, 1598 "ScriptScriptPercentScaleDown": 55, 1599 "SkewedFractionHorizontalGap": 66, 1600 "SkewedFractionVerticalGap": 77, 1601 "SpaceAfterScript": 42, 1602 "StackBottomDisplayStyleShiftDown": 698, 1603 "StackBottomShiftDown": 465, 1604 "StackDisplayStyleGapMin": 462, 1605 "StackGapMin": 198, 1606 "StackTopDisplayStyleShiftUp": 774, 1607 "StackTopShiftUp": 516, 1608 "StretchStackBottomShiftDown": 585, 1609 "StretchStackGapAboveMin": 132, 1610 "StretchStackGapBelowMin": 132, 1611 "StretchStackTopShiftUp": 165, 1612 "SubSuperscriptGapMin": 264, 1613 "SubscriptBaselineDropMin": 105, 1614 "SubscriptShiftDown": 140, 1615 "SubscriptTopMax": 413, 1616 "SuperscriptBaselineDropMax": 221, 1617 "SuperscriptBottomMaxWithSubscript": 413, 1618 "SuperscriptBottomMin": 129, 1619 "SuperscriptShiftUp": 477, 1620 "SuperscriptShiftUpCramped": 358, 1621 "UnderbarExtraDescender": 66, 1622 "UnderbarRuleThickness": 66, 1623 "UnderbarVerticalGap": 198, 1624 "UpperLimitBaselineRiseMin": 165, 1625 "UpperLimitGapMin": 132, 1626 } 1627 builder.buildMathTable(ttFont, constants=constants) 1628 mathTable = ttFont["MATH"].table 1629 assert mathTable.MathConstants 1630 assert mathTable.MathGlyphInfo is None 1631 assert mathTable.MathVariants is None 1632 for k, v in constants.items(): 1633 r = getattr(mathTable.MathConstants, k) 1634 try: 1635 r = r.Value 1636 except AttributeError: 1637 pass 1638 assert r == v 1639 1640 1641def test_buildMathTable_italicsCorrection(): 1642 ttFont = ttLib.TTFont() 1643 ttFont.setGlyphOrder(["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"]) 1644 italicsCorrections = {"A": 100, "C": 300, "D": 400, "E": 500} 1645 builder.buildMathTable(ttFont, italicsCorrections=italicsCorrections) 1646 mathTable = ttFont["MATH"].table 1647 assert mathTable.MathConstants is None 1648 assert mathTable.MathGlyphInfo 1649 assert mathTable.MathVariants is None 1650 assert set( 1651 mathTable.MathGlyphInfo.MathItalicsCorrectionInfo.Coverage.glyphs 1652 ) == set(italicsCorrections.keys()) 1653 for glyph, correction in zip( 1654 mathTable.MathGlyphInfo.MathItalicsCorrectionInfo.Coverage.glyphs, 1655 mathTable.MathGlyphInfo.MathItalicsCorrectionInfo.ItalicsCorrection, 1656 ): 1657 assert correction.Value == italicsCorrections[glyph] 1658 1659 1660def test_buildMathTable_topAccentAttachment(): 1661 ttFont = ttLib.TTFont() 1662 ttFont.setGlyphOrder(["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"]) 1663 topAccentAttachments = {"A": 10, "B": 20, "C": 30, "E": 50} 1664 builder.buildMathTable(ttFont, topAccentAttachments=topAccentAttachments) 1665 mathTable = ttFont["MATH"].table 1666 assert mathTable.MathConstants is None 1667 assert mathTable.MathGlyphInfo 1668 assert mathTable.MathVariants is None 1669 assert set( 1670 mathTable.MathGlyphInfo.MathTopAccentAttachment.TopAccentCoverage.glyphs 1671 ) == set(topAccentAttachments.keys()) 1672 for glyph, attachment in zip( 1673 mathTable.MathGlyphInfo.MathTopAccentAttachment.TopAccentCoverage.glyphs, 1674 mathTable.MathGlyphInfo.MathTopAccentAttachment.TopAccentAttachment, 1675 ): 1676 assert attachment.Value == topAccentAttachments[glyph] 1677 1678 1679def test_buildMathTable_extendedShape(): 1680 ttFont = ttLib.TTFont() 1681 ttFont.setGlyphOrder(["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"]) 1682 extendedShapes = {"A", "C", "E", "F"} 1683 builder.buildMathTable(ttFont, extendedShapes=extendedShapes) 1684 mathTable = ttFont["MATH"].table 1685 assert mathTable.MathConstants is None 1686 assert mathTable.MathGlyphInfo 1687 assert mathTable.MathVariants is None 1688 assert set(mathTable.MathGlyphInfo.ExtendedShapeCoverage.glyphs) == extendedShapes 1689 1690 1691def test_buildMathTable_mathKern(): 1692 ttFont = ttLib.TTFont() 1693 ttFont.setGlyphOrder(["A", "B"]) 1694 mathKerns = { 1695 "A": { 1696 "TopRight": ([10, 20], [10, 20, 30]), 1697 "BottomRight": ([], [10]), 1698 "TopLeft": ([10], [0, 20]), 1699 "BottomLeft": ([-10, 0], [0, 10, 20]), 1700 }, 1701 } 1702 builder.buildMathTable(ttFont, mathKerns=mathKerns) 1703 mathTable = ttFont["MATH"].table 1704 assert mathTable.MathConstants is None 1705 assert mathTable.MathGlyphInfo 1706 assert mathTable.MathVariants is None 1707 assert set(mathTable.MathGlyphInfo.MathKernInfo.MathKernCoverage.glyphs) == set( 1708 mathKerns.keys() 1709 ) 1710 for glyph, record in zip( 1711 mathTable.MathGlyphInfo.MathKernInfo.MathKernCoverage.glyphs, 1712 mathTable.MathGlyphInfo.MathKernInfo.MathKernInfoRecords, 1713 ): 1714 h, k = mathKerns[glyph]["TopRight"] 1715 assert [v.Value for v in record.TopRightMathKern.CorrectionHeight] == h 1716 assert [v.Value for v in record.TopRightMathKern.KernValue] == k 1717 h, k = mathKerns[glyph]["BottomRight"] 1718 assert [v.Value for v in record.BottomRightMathKern.CorrectionHeight] == h 1719 assert [v.Value for v in record.BottomRightMathKern.KernValue] == k 1720 h, k = mathKerns[glyph]["TopLeft"] 1721 assert [v.Value for v in record.TopLeftMathKern.CorrectionHeight] == h 1722 assert [v.Value for v in record.TopLeftMathKern.KernValue] == k 1723 h, k = mathKerns[glyph]["BottomLeft"] 1724 assert [v.Value for v in record.BottomLeftMathKern.CorrectionHeight] == h 1725 assert [v.Value for v in record.BottomLeftMathKern.KernValue] == k 1726 1727 1728def test_buildMathTable_vertVariants(): 1729 ttFont = ttLib.TTFont() 1730 ttFont.setGlyphOrder(["A", "A.size1", "A.size2"]) 1731 vertGlyphVariants = {"A": [("A.size1", 100), ("A.size2", 200)]} 1732 builder.buildMathTable(ttFont, vertGlyphVariants=vertGlyphVariants) 1733 mathTable = ttFont["MATH"].table 1734 assert mathTable.MathConstants is None 1735 assert mathTable.MathGlyphInfo is None 1736 assert mathTable.MathVariants 1737 assert set(mathTable.MathVariants.VertGlyphCoverage.glyphs) == set( 1738 vertGlyphVariants.keys() 1739 ) 1740 for glyph, construction in zip( 1741 mathTable.MathVariants.VertGlyphCoverage.glyphs, 1742 mathTable.MathVariants.VertGlyphConstruction, 1743 ): 1744 assert [ 1745 (r.VariantGlyph, r.AdvanceMeasurement) 1746 for r in construction.MathGlyphVariantRecord 1747 ] == vertGlyphVariants[glyph] 1748 1749 1750def test_buildMathTable_horizVariants(): 1751 ttFont = ttLib.TTFont() 1752 ttFont.setGlyphOrder(["A", "A.size1", "A.size2"]) 1753 horizGlyphVariants = {"A": [("A.size1", 100), ("A.size2", 200)]} 1754 builder.buildMathTable(ttFont, horizGlyphVariants=horizGlyphVariants) 1755 mathTable = ttFont["MATH"].table 1756 assert mathTable.MathConstants is None 1757 assert mathTable.MathGlyphInfo is None 1758 assert mathTable.MathVariants 1759 assert set(mathTable.MathVariants.HorizGlyphCoverage.glyphs) == set( 1760 horizGlyphVariants.keys() 1761 ) 1762 for glyph, construction in zip( 1763 mathTable.MathVariants.HorizGlyphCoverage.glyphs, 1764 mathTable.MathVariants.HorizGlyphConstruction, 1765 ): 1766 assert [ 1767 (r.VariantGlyph, r.AdvanceMeasurement) 1768 for r in construction.MathGlyphVariantRecord 1769 ] == horizGlyphVariants[glyph] 1770 1771 1772def test_buildMathTable_vertAssembly(): 1773 ttFont = ttLib.TTFont() 1774 ttFont.setGlyphOrder(["A", "A.top", "A.middle", "A.bottom", "A.extender"]) 1775 vertGlyphAssembly = { 1776 "A": [ 1777 [ 1778 ("A.bottom", 0, 0, 100, 200), 1779 ("A.extender", 1, 50, 50, 100), 1780 ("A.middle", 0, 100, 100, 200), 1781 ("A.extender", 1, 50, 50, 100), 1782 ("A.top", 0, 100, 0, 200), 1783 ], 1784 10, 1785 ], 1786 } 1787 builder.buildMathTable(ttFont, vertGlyphAssembly=vertGlyphAssembly) 1788 mathTable = ttFont["MATH"].table 1789 assert mathTable.MathConstants is None 1790 assert mathTable.MathGlyphInfo is None 1791 assert mathTable.MathVariants 1792 assert set(mathTable.MathVariants.VertGlyphCoverage.glyphs) == set( 1793 vertGlyphAssembly.keys() 1794 ) 1795 for glyph, construction in zip( 1796 mathTable.MathVariants.VertGlyphCoverage.glyphs, 1797 mathTable.MathVariants.VertGlyphConstruction, 1798 ): 1799 assert [ 1800 [ 1801 ( 1802 r.glyph, 1803 r.PartFlags, 1804 r.StartConnectorLength, 1805 r.EndConnectorLength, 1806 r.FullAdvance, 1807 ) 1808 for r in construction.GlyphAssembly.PartRecords 1809 ], 1810 construction.GlyphAssembly.ItalicsCorrection.Value, 1811 ] == vertGlyphAssembly[glyph] 1812 1813 1814def test_buildMathTable_horizAssembly(): 1815 ttFont = ttLib.TTFont() 1816 ttFont.setGlyphOrder(["A", "A.top", "A.middle", "A.bottom", "A.extender"]) 1817 horizGlyphAssembly = { 1818 "A": [ 1819 [ 1820 ("A.bottom", 0, 0, 100, 200), 1821 ("A.extender", 1, 50, 50, 100), 1822 ("A.middle", 0, 100, 100, 200), 1823 ("A.extender", 1, 50, 50, 100), 1824 ("A.top", 0, 100, 0, 200), 1825 ], 1826 10, 1827 ], 1828 } 1829 builder.buildMathTable(ttFont, horizGlyphAssembly=horizGlyphAssembly) 1830 mathTable = ttFont["MATH"].table 1831 assert mathTable.MathConstants is None 1832 assert mathTable.MathGlyphInfo is None 1833 assert mathTable.MathVariants 1834 assert set(mathTable.MathVariants.HorizGlyphCoverage.glyphs) == set( 1835 horizGlyphAssembly.keys() 1836 ) 1837 for glyph, construction in zip( 1838 mathTable.MathVariants.HorizGlyphCoverage.glyphs, 1839 mathTable.MathVariants.HorizGlyphConstruction, 1840 ): 1841 assert [ 1842 [ 1843 ( 1844 r.glyph, 1845 r.PartFlags, 1846 r.StartConnectorLength, 1847 r.EndConnectorLength, 1848 r.FullAdvance, 1849 ) 1850 for r in construction.GlyphAssembly.PartRecords 1851 ], 1852 construction.GlyphAssembly.ItalicsCorrection.Value, 1853 ] == horizGlyphAssembly[glyph] 1854 1855 1856class ChainContextualRulesetTest(object): 1857 def test_makeRulesets(self): 1858 font = ttLib.TTFont() 1859 font.setGlyphOrder(["a", "b", "c", "d", "A", "B", "C", "D", "E"]) 1860 sb = builder.ChainContextSubstBuilder(font, None) 1861 prefix, input_, suffix, lookups = [["a"], ["b"]], [["c"]], [], [None] 1862 sb.rules.append(builder.ChainContextualRule(prefix, input_, suffix, lookups)) 1863 1864 prefix, input_, suffix, lookups = [["a"], ["d"]], [["c"]], [], [None] 1865 sb.rules.append(builder.ChainContextualRule(prefix, input_, suffix, lookups)) 1866 1867 sb.add_subtable_break(None) 1868 1869 # Second subtable has some glyph classes 1870 prefix, input_, suffix, lookups = [["A"]], [["E"]], [], [None] 1871 sb.rules.append(builder.ChainContextualRule(prefix, input_, suffix, lookups)) 1872 prefix, input_, suffix, lookups = [["A"]], [["C", "D"]], [], [None] 1873 sb.rules.append(builder.ChainContextualRule(prefix, input_, suffix, lookups)) 1874 prefix, input_, suffix, lookups = [["A", "B"]], [["E"]], [], [None] 1875 sb.rules.append(builder.ChainContextualRule(prefix, input_, suffix, lookups)) 1876 1877 sb.add_subtable_break(None) 1878 1879 # Third subtable has no pre/post context 1880 prefix, input_, suffix, lookups = [], [["E"]], [], [None] 1881 sb.rules.append(builder.ChainContextualRule(prefix, input_, suffix, lookups)) 1882 prefix, input_, suffix, lookups = [], [["C", "D"]], [], [None] 1883 sb.rules.append(builder.ChainContextualRule(prefix, input_, suffix, lookups)) 1884 1885 rulesets = sb.rulesets() 1886 assert len(rulesets) == 3 1887 assert rulesets[0].hasPrefixOrSuffix 1888 assert not rulesets[0].hasAnyGlyphClasses 1889 cd = rulesets[0].format2ClassDefs() 1890 assert set(cd[0].classes()[1:]) == set([("d",), ("b",), ("a",)]) 1891 assert set(cd[1].classes()[1:]) == set([("c",)]) 1892 assert set(cd[2].classes()[1:]) == set() 1893 1894 assert rulesets[1].hasPrefixOrSuffix 1895 assert rulesets[1].hasAnyGlyphClasses 1896 assert not rulesets[1].format2ClassDefs() 1897 1898 assert not rulesets[2].hasPrefixOrSuffix 1899 assert rulesets[2].hasAnyGlyphClasses 1900 assert rulesets[2].format2ClassDefs() 1901 cd = rulesets[2].format2ClassDefs() 1902 assert set(cd[0].classes()[1:]) == set() 1903 assert set(cd[1].classes()[1:]) == set([("C", "D"), ("E",)]) 1904 assert set(cd[2].classes()[1:]) == set() 1905 1906 1907if __name__ == "__main__": 1908 import sys 1909 1910 sys.exit(pytest.main(sys.argv)) 1911