1from copy import deepcopy 2from fontTools.ttLib import newTable 3from fontTools.ttLib.tables import otTables as ot 4from fontTools.colorLib import builder 5from fontTools.colorLib.geometry import round_start_circle_stable_containment, Circle 6from fontTools.colorLib.builder import LayerListBuilder 7from fontTools.colorLib.table_builder import TableBuilder 8from fontTools.colorLib.errors import ColorLibError 9import pytest 10from typing import List 11 12 13def _build(cls, source): 14 return LayerListBuilder().tableBuilder.build(cls, source) 15 16 17def _buildPaint(source): 18 return LayerListBuilder().buildPaint(source) 19 20 21def test_buildCOLR_v0(): 22 color_layer_lists = { 23 "a": [("a.color0", 0), ("a.color1", 1)], 24 "b": [("b.color1", 1), ("b.color0", 0)], 25 } 26 27 colr = builder.buildCOLR(color_layer_lists) 28 29 assert colr.tableTag == "COLR" 30 assert colr.version == 0 31 assert colr.ColorLayers["a"][0].name == "a.color0" 32 assert colr.ColorLayers["a"][0].colorID == 0 33 assert colr.ColorLayers["a"][1].name == "a.color1" 34 assert colr.ColorLayers["a"][1].colorID == 1 35 assert colr.ColorLayers["b"][0].name == "b.color1" 36 assert colr.ColorLayers["b"][0].colorID == 1 37 assert colr.ColorLayers["b"][1].name == "b.color0" 38 assert colr.ColorLayers["b"][1].colorID == 0 39 40 41def test_buildCOLR_v0_layer_as_list(): 42 # when COLRv0 layers are encoded as plist in UFO lib, both python tuples and 43 # lists are encoded as plist array elements; but the latter are always decoded 44 # as python lists, thus after roundtripping a plist tuples become lists. 45 # Before FontTools 4.17.0 we were treating tuples and lists as equivalent; 46 # with 4.17.0, a paint of type list is used to identify a PaintColrLayers. 47 # This broke backward compatibility as ufo2ft is simply passing through the 48 # color layers as read from the UFO lib plist, and as such the latter use lists 49 # instead of tuples for COLRv0 layers (layerGlyph, paletteIndex) combo. 50 # We restore backward compat by accepting either tuples or lists (of length 2 51 # and only containing a str and an int) as individual top-level layers. 52 # https://github.com/googlefonts/ufo2ft/issues/426 53 color_layer_lists = { 54 "a": [["a.color0", 0], ["a.color1", 1]], 55 "b": [["b.color1", 1], ["b.color0", 0]], 56 } 57 58 colr = builder.buildCOLR(color_layer_lists) 59 60 assert colr.tableTag == "COLR" 61 assert colr.version == 0 62 assert colr.ColorLayers["a"][0].name == "a.color0" 63 assert colr.ColorLayers["a"][0].colorID == 0 64 assert colr.ColorLayers["a"][1].name == "a.color1" 65 assert colr.ColorLayers["a"][1].colorID == 1 66 assert colr.ColorLayers["b"][0].name == "b.color1" 67 assert colr.ColorLayers["b"][0].colorID == 1 68 assert colr.ColorLayers["b"][1].name == "b.color0" 69 assert colr.ColorLayers["b"][1].colorID == 0 70 71 72def test_buildCPAL_v0(): 73 palettes = [ 74 [(0.68, 0.20, 0.32, 1.0), (0.45, 0.68, 0.21, 1.0)], 75 [(0.68, 0.20, 0.32, 0.6), (0.45, 0.68, 0.21, 0.6)], 76 [(0.68, 0.20, 0.32, 0.3), (0.45, 0.68, 0.21, 0.3)], 77 ] 78 79 cpal = builder.buildCPAL(palettes) 80 81 assert cpal.tableTag == "CPAL" 82 assert cpal.version == 0 83 assert cpal.numPaletteEntries == 2 84 85 assert len(cpal.palettes) == 3 86 assert [tuple(c) for c in cpal.palettes[0]] == [ 87 (82, 51, 173, 255), 88 (54, 173, 115, 255), 89 ] 90 assert [tuple(c) for c in cpal.palettes[1]] == [ 91 (82, 51, 173, 153), 92 (54, 173, 115, 153), 93 ] 94 assert [tuple(c) for c in cpal.palettes[2]] == [ 95 (82, 51, 173, 76), 96 (54, 173, 115, 76), 97 ] 98 99 100def test_buildCPAL_palettes_different_lengths(): 101 with pytest.raises(ColorLibError, match="have different lengths"): 102 builder.buildCPAL([[(1, 1, 1, 1)], [(0, 0, 0, 1), (0.5, 0.5, 0.5, 1)]]) 103 104 105def test_buildPaletteLabels(): 106 name_table = newTable("name") 107 name_table.names = [] 108 109 name_ids = builder.buildPaletteLabels( 110 [None, "hi", {"en": "hello", "de": "hallo"}], name_table 111 ) 112 113 assert name_ids == [0xFFFF, 256, 257] 114 115 assert len(name_table.names) == 3 116 assert str(name_table.names[0]) == "hi" 117 assert name_table.names[0].nameID == 256 118 119 assert str(name_table.names[1]) == "hallo" 120 assert name_table.names[1].nameID == 257 121 122 assert str(name_table.names[2]) == "hello" 123 assert name_table.names[2].nameID == 257 124 125 126def test_build_CPAL_v1_types_no_labels(): 127 palettes = [ 128 [(0.1, 0.2, 0.3, 1.0), (0.4, 0.5, 0.6, 1.0)], 129 [(0.1, 0.2, 0.3, 0.6), (0.4, 0.5, 0.6, 0.6)], 130 [(0.1, 0.2, 0.3, 0.3), (0.4, 0.5, 0.6, 0.3)], 131 ] 132 paletteTypes = [ 133 builder.ColorPaletteType.USABLE_WITH_LIGHT_BACKGROUND, 134 builder.ColorPaletteType.USABLE_WITH_DARK_BACKGROUND, 135 builder.ColorPaletteType.USABLE_WITH_LIGHT_BACKGROUND 136 | builder.ColorPaletteType.USABLE_WITH_DARK_BACKGROUND, 137 ] 138 139 cpal = builder.buildCPAL(palettes, paletteTypes=paletteTypes) 140 141 assert cpal.tableTag == "CPAL" 142 assert cpal.version == 1 143 assert cpal.numPaletteEntries == 2 144 assert len(cpal.palettes) == 3 145 146 assert cpal.paletteTypes == paletteTypes 147 assert cpal.paletteLabels == [cpal.NO_NAME_ID] * len(palettes) 148 assert cpal.paletteEntryLabels == [cpal.NO_NAME_ID] * cpal.numPaletteEntries 149 150 151def test_build_CPAL_v1_labels(): 152 palettes = [ 153 [(0.1, 0.2, 0.3, 1.0), (0.4, 0.5, 0.6, 1.0)], 154 [(0.1, 0.2, 0.3, 0.6), (0.4, 0.5, 0.6, 0.6)], 155 [(0.1, 0.2, 0.3, 0.3), (0.4, 0.5, 0.6, 0.3)], 156 ] 157 paletteLabels = ["First", {"en": "Second", "it": "Seconda"}, None] 158 paletteEntryLabels = ["Foo", "Bar"] 159 160 with pytest.raises(TypeError, match="nameTable is required"): 161 builder.buildCPAL(palettes, paletteLabels=paletteLabels) 162 with pytest.raises(TypeError, match="nameTable is required"): 163 builder.buildCPAL(palettes, paletteEntryLabels=paletteEntryLabels) 164 165 name_table = newTable("name") 166 name_table.names = [] 167 168 cpal = builder.buildCPAL( 169 palettes, 170 paletteLabels=paletteLabels, 171 paletteEntryLabels=paletteEntryLabels, 172 nameTable=name_table, 173 ) 174 175 assert cpal.tableTag == "CPAL" 176 assert cpal.version == 1 177 assert cpal.numPaletteEntries == 2 178 assert len(cpal.palettes) == 3 179 180 assert cpal.paletteTypes == [cpal.DEFAULT_PALETTE_TYPE] * len(palettes) 181 assert cpal.paletteLabels == [256, 257, cpal.NO_NAME_ID] 182 assert cpal.paletteEntryLabels == [258, 259] 183 184 assert name_table.getDebugName(256) == "First" 185 assert name_table.getDebugName(257) == "Second" 186 assert name_table.getDebugName(258) == "Foo" 187 assert name_table.getDebugName(259) == "Bar" 188 189 190def test_invalid_ColorPaletteType(): 191 with pytest.raises(ValueError, match="not a valid ColorPaletteType"): 192 builder.ColorPaletteType(-1) 193 with pytest.raises(ValueError, match="not a valid ColorPaletteType"): 194 builder.ColorPaletteType(4) 195 with pytest.raises(ValueError, match="not a valid ColorPaletteType"): 196 builder.ColorPaletteType("abc") 197 198 199def test_buildCPAL_v1_invalid_args_length(): 200 with pytest.raises(ColorLibError, match="Expected 2 paletteTypes, got 1"): 201 builder.buildCPAL([[(0, 0, 0, 0)], [(1, 1, 1, 1)]], paletteTypes=[1]) 202 203 with pytest.raises(ColorLibError, match="Expected 2 paletteLabels, got 1"): 204 builder.buildCPAL( 205 [[(0, 0, 0, 0)], [(1, 1, 1, 1)]], 206 paletteLabels=["foo"], 207 nameTable=newTable("name"), 208 ) 209 210 with pytest.raises(ColorLibError, match="Expected 1 paletteEntryLabels, got 0"): 211 cpal = builder.buildCPAL( 212 [[(0, 0, 0, 0)], [(1, 1, 1, 1)]], 213 paletteEntryLabels=[], 214 nameTable=newTable("name"), 215 ) 216 217 218def test_buildCPAL_invalid_color(): 219 with pytest.raises( 220 ColorLibError, 221 match=r"In palette\[0\]\[1\]: expected \(R, G, B, A\) tuple, got \(1, 1, 1\)", 222 ): 223 builder.buildCPAL([[(1, 1, 1, 1), (1, 1, 1)]]) 224 225 with pytest.raises( 226 ColorLibError, 227 match=( 228 r"palette\[1\]\[0\] has invalid out-of-range " 229 r"\[0..1\] color: \(1, 1, -1, 2\)" 230 ), 231 ): 232 builder.buildCPAL([[(0, 0, 0, 0)], [(1, 1, -1, 2)]]) 233 234 235def test_buildPaintSolid(): 236 p = _buildPaint((ot.PaintFormat.PaintSolid, 0)) 237 assert p.Format == ot.PaintFormat.PaintSolid 238 assert p.PaletteIndex == 0 239 assert p.Alpha == 1.0 240 241 242def test_buildPaintSolid_Alpha(): 243 p = _buildPaint((ot.PaintFormat.PaintSolid, 1, 0.5)) 244 assert p.Format == ot.PaintFormat.PaintSolid 245 assert p.PaletteIndex == 1 246 assert p.Alpha == 0.5 247 248 249def test_buildPaintVarSolid(): 250 p = _buildPaint((ot.PaintFormat.PaintVarSolid, 3, 0.5, 2)) 251 assert p.Format == ot.PaintFormat.PaintVarSolid 252 assert p.PaletteIndex == 3 253 assert p.Alpha == 0.5 254 assert p.VarIndexBase == 2 255 256 257def test_buildVarColorStop_DefaultAlpha(): 258 s = _build(ot.ColorStop, (0.1, 2)) 259 assert s.StopOffset == 0.1 260 assert s.PaletteIndex == 2 261 assert s.Alpha == builder._DEFAULT_ALPHA 262 263 264def test_buildVarColorStop_DefaultAlpha(): 265 s = _build(ot.VarColorStop, (0.1, 2)) 266 assert s.StopOffset == 0.1 267 assert s.PaletteIndex == 2 268 assert s.Alpha == builder._DEFAULT_ALPHA 269 270 271def test_buildColorStop(): 272 s = _build(ot.ColorStop, {"StopOffset": 0.2, "PaletteIndex": 3, "Alpha": 0.4}) 273 assert s.StopOffset == 0.2 274 assert s.PaletteIndex == 3 275 assert s.Alpha == 0.4 276 277 278def test_buildColorStop_Variable(): 279 s = _build( 280 ot.VarColorStop, 281 { 282 "StopOffset": 0.0, 283 "PaletteIndex": 0, 284 "Alpha": 0.3, 285 "VarIndexBase": 1, 286 }, 287 ) 288 assert s.StopOffset == 0.0 289 assert s.PaletteIndex == 0 290 assert s.Alpha == 0.3 291 assert s.VarIndexBase == 1 292 293 294def test_buildColorLine_StopList(): 295 stops = [(0.0, 0), (0.5, 1), (1.0, 2)] 296 297 cline = _build(ot.ColorLine, {"ColorStop": stops}) 298 assert cline.Extend == builder.ExtendMode.PAD 299 assert cline.StopCount == 3 300 assert [(cs.StopOffset, cs.PaletteIndex) for cs in cline.ColorStop] == stops 301 302 cline = _build(ot.ColorLine, {"Extend": "pad", "ColorStop": stops}) 303 assert cline.Extend == builder.ExtendMode.PAD 304 305 cline = _build( 306 ot.ColorLine, {"ColorStop": stops, "Extend": builder.ExtendMode.REPEAT} 307 ) 308 assert cline.Extend == builder.ExtendMode.REPEAT 309 310 cline = _build( 311 ot.ColorLine, {"ColorStop": stops, "Extend": builder.ExtendMode.REFLECT} 312 ) 313 assert cline.Extend == builder.ExtendMode.REFLECT 314 315 cline = _build( 316 ot.ColorLine, {"ColorStop": [_build(ot.ColorStop, s) for s in stops]} 317 ) 318 assert [(cs.StopOffset, cs.PaletteIndex) for cs in cline.ColorStop] == stops 319 320 321def test_buildVarColorLine_StopMap(): 322 stops = [ 323 {"StopOffset": 0.0, "PaletteIndex": 0, "Alpha": 0.5, "VarIndexBase": 1}, 324 {"StopOffset": 1.0, "PaletteIndex": 1, "Alpha": 0.3, "VarIndexBase": 3}, 325 ] 326 cline = _build(ot.VarColorLine, {"ColorStop": stops}) 327 assert [ 328 { 329 "StopOffset": cs.StopOffset, 330 "PaletteIndex": cs.PaletteIndex, 331 "Alpha": cs.Alpha, 332 "VarIndexBase": cs.VarIndexBase, 333 } 334 for cs in cline.ColorStop 335 ] == stops 336 337 338def checkBuildAffine2x3(cls, variable=False): 339 matrix = _build(cls, (1.5, 0, 0.5, 2.0, 1.0, -3.0)) 340 assert matrix.xx == 1.5 341 assert matrix.yx == 0.0 342 assert matrix.xy == 0.5 343 assert matrix.yy == 2.0 344 assert matrix.dx == 1.0 345 assert matrix.dy == -3.0 346 if variable: 347 assert matrix.VarIndexBase == 0xFFFFFFFF 348 349 350def test_buildAffine2x3(): 351 checkBuildAffine2x3(ot.Affine2x3) 352 353 354def test_buildVarAffine2x3(): 355 checkBuildAffine2x3(ot.VarAffine2x3, variable=True) 356 357 358def _sample_stops(variable): 359 cls = ot.ColorStop if not variable else ot.VarColorStop 360 stop_sources = [ 361 {"StopOffset": 0.0, "PaletteIndex": 0}, 362 {"StopOffset": 0.5, "PaletteIndex": 1}, 363 {"StopOffset": 1.0, "PaletteIndex": 2, "Alpha": 0.8}, 364 ] 365 if variable: 366 for i, src in enumerate(stop_sources, start=123): 367 src["VarIndexBase"] = i 368 return [_build(cls, src) for src in stop_sources] 369 370 371def _is_var(fmt): 372 return fmt.name.startswith("PaintVar") 373 374 375def _is_around_center(fmt): 376 return fmt.name.endswith("AroundCenter") 377 378 379def _is_uniform_scale(fmt): 380 return "ScaleUniform" in fmt.name 381 382 383def checkBuildPaintLinearGradient(fmt): 384 variable = _is_var(fmt) 385 color_stops = _sample_stops(variable) 386 387 x0, y0, x1, y1, x2, y2 = (1, 2, 3, 4, 5, 6) 388 source = { 389 "Format": fmt, 390 "ColorLine": {"ColorStop": color_stops}, 391 "x0": x0, 392 "y0": y0, 393 "x1": x1, 394 "y1": y1, 395 "x2": x2, 396 "y2": y2, 397 } 398 if variable: 399 source["VarIndexBase"] = 7 400 gradient = _buildPaint(source) 401 assert gradient.ColorLine.Extend == builder.ExtendMode.PAD 402 assert gradient.ColorLine.ColorStop == color_stops 403 404 gradient = _buildPaint(gradient) 405 assert (gradient.x0, gradient.y0) == (1, 2) 406 assert (gradient.x1, gradient.y1) == (3, 4) 407 assert (gradient.x2, gradient.y2) == (5, 6) 408 if variable: 409 assert gradient.VarIndexBase == 7 410 411 412def test_buildPaintLinearGradient(): 413 assert not _is_var(ot.PaintFormat.PaintLinearGradient) 414 checkBuildPaintLinearGradient(ot.PaintFormat.PaintLinearGradient) 415 416 417def test_buildPaintVarLinearGradient(): 418 assert _is_var(ot.PaintFormat.PaintVarLinearGradient) 419 checkBuildPaintLinearGradient(ot.PaintFormat.PaintVarLinearGradient) 420 421 422def checkBuildPaintRadialGradient(fmt): 423 variable = _is_var(fmt) 424 color_stops = _sample_stops(variable) 425 line_cls = ot.VarColorLine if variable else ot.ColorLine 426 427 color_line = _build( 428 line_cls, {"ColorStop": color_stops, "Extend": builder.ExtendMode.REPEAT} 429 ) 430 c0 = (100, 200) 431 c1 = (150, 250) 432 r0 = 10 433 r1 = 5 434 varIndexBase = 0 435 436 source = [fmt, color_line, *c0, r0, *c1, r1] 437 if variable: 438 source.append(varIndexBase) 439 440 gradient = _build(ot.Paint, tuple(source)) 441 assert gradient.Format == fmt 442 assert gradient.ColorLine == color_line 443 assert (gradient.x0, gradient.y0) == c0 444 assert (gradient.x1, gradient.y1) == c1 445 assert gradient.r0 == r0 446 assert gradient.r1 == r1 447 if variable: 448 assert gradient.VarIndexBase == varIndexBase 449 450 source = { 451 "Format": fmt, 452 "ColorLine": {"ColorStop": color_stops}, 453 "x0": c0[0], 454 "y0": c0[1], 455 "x1": c1[0], 456 "y1": c1[1], 457 "r0": r0, 458 "r1": r1, 459 } 460 if variable: 461 source["VarIndexBase"] = varIndexBase 462 gradient = _build(ot.Paint, source) 463 assert gradient.ColorLine.Extend == builder.ExtendMode.PAD 464 assert gradient.ColorLine.ColorStop == color_stops 465 assert (gradient.x0, gradient.y0) == c0 466 assert (gradient.x1, gradient.y1) == c1 467 assert gradient.r0 == r0 468 assert gradient.r1 == r1 469 if variable: 470 assert gradient.VarIndexBase == varIndexBase 471 472 473def test_buildPaintRadialGradient(): 474 assert not _is_var(ot.PaintFormat.PaintRadialGradient) 475 checkBuildPaintRadialGradient(ot.PaintFormat.PaintRadialGradient) 476 477 478def test_buildPaintVarRadialGradient(): 479 assert _is_var(ot.PaintFormat.PaintVarRadialGradient) 480 checkBuildPaintRadialGradient(ot.PaintFormat.PaintVarRadialGradient) 481 482 483def checkPaintSweepGradient(fmt): 484 variable = _is_var(fmt) 485 source = { 486 "Format": fmt, 487 "ColorLine": {"ColorStop": _sample_stops(variable)}, 488 "centerX": 127, 489 "centerY": 129, 490 "startAngle": 15, 491 "endAngle": 42, 492 } 493 if variable: 494 source["VarIndexBase"] = 666 495 paint = _buildPaint(source) 496 497 assert paint.Format == fmt 498 assert paint.centerX == 127 499 assert paint.centerY == 129 500 assert paint.startAngle == 15 501 assert paint.endAngle == 42 502 if variable: 503 assert paint.VarIndexBase == 666 504 505 506def test_buildPaintSweepGradient(): 507 assert not _is_var(ot.PaintFormat.PaintSweepGradient) 508 checkPaintSweepGradient(ot.PaintFormat.PaintSweepGradient) 509 510 511def test_buildPaintVarSweepGradient(): 512 assert _is_var(ot.PaintFormat.PaintVarSweepGradient) 513 checkPaintSweepGradient(ot.PaintFormat.PaintVarSweepGradient) 514 515 516def test_buildPaintGlyph_Solid(): 517 layer = _build( 518 ot.Paint, 519 ( 520 ot.PaintFormat.PaintGlyph, 521 ( 522 ot.PaintFormat.PaintSolid, 523 2, 524 ), 525 "a", 526 ), 527 ) 528 assert layer.Format == ot.PaintFormat.PaintGlyph 529 assert layer.Glyph == "a" 530 assert layer.Paint.Format == ot.PaintFormat.PaintSolid 531 assert layer.Paint.PaletteIndex == 2 532 533 layer = _build( 534 ot.Paint, 535 ( 536 ot.PaintFormat.PaintGlyph, 537 (ot.PaintFormat.PaintSolid, 3, 0.9), 538 "a", 539 ), 540 ) 541 assert layer.Paint.Format == ot.PaintFormat.PaintSolid 542 assert layer.Paint.PaletteIndex == 3 543 assert layer.Paint.Alpha == 0.9 544 545 546def test_buildPaintGlyph_VarLinearGradient(): 547 layer = _build( 548 ot.Paint, 549 { 550 "Format": ot.PaintFormat.PaintGlyph, 551 "Glyph": "a", 552 "Paint": { 553 "Format": ot.PaintFormat.PaintVarLinearGradient, 554 "ColorLine": {"ColorStop": [(0.0, 3), (1.0, 4)]}, 555 "x0": 100, 556 "y0": 200, 557 "x1": 150, 558 "y1": 250, 559 }, 560 }, 561 ) 562 563 assert layer.Format == ot.PaintFormat.PaintGlyph 564 assert layer.Glyph == "a" 565 assert layer.Paint.Format == ot.PaintFormat.PaintVarLinearGradient 566 assert layer.Paint.ColorLine.ColorStop[0].StopOffset == 0.0 567 assert layer.Paint.ColorLine.ColorStop[0].PaletteIndex == 3 568 assert layer.Paint.ColorLine.ColorStop[1].StopOffset == 1.0 569 assert layer.Paint.ColorLine.ColorStop[1].PaletteIndex == 4 570 assert layer.Paint.x0 == 100 571 assert layer.Paint.y0 == 200 572 assert layer.Paint.x1 == 150 573 assert layer.Paint.y1 == 250 574 575 576def test_buildPaintGlyph_RadialGradient(): 577 layer = _build( 578 ot.Paint, 579 ( 580 int(ot.PaintFormat.PaintGlyph), 581 ( 582 ot.PaintFormat.PaintRadialGradient, 583 ( 584 "pad", 585 [ 586 (0.0, 5), 587 {"StopOffset": 0.5, "PaletteIndex": 6, "Alpha": 0.8}, 588 (1.0, 7), 589 ], 590 ), 591 50, 592 50, 593 30, 594 75, 595 75, 596 10, 597 ), 598 "a", 599 ), 600 ) 601 assert layer.Format == ot.PaintFormat.PaintGlyph 602 assert layer.Paint.Format == ot.PaintFormat.PaintRadialGradient 603 assert layer.Paint.ColorLine.ColorStop[0].StopOffset == 0.0 604 assert layer.Paint.ColorLine.ColorStop[0].PaletteIndex == 5 605 assert layer.Paint.ColorLine.ColorStop[1].StopOffset == 0.5 606 assert layer.Paint.ColorLine.ColorStop[1].PaletteIndex == 6 607 assert layer.Paint.ColorLine.ColorStop[1].Alpha == 0.8 608 assert layer.Paint.ColorLine.ColorStop[2].StopOffset == 1.0 609 assert layer.Paint.ColorLine.ColorStop[2].PaletteIndex == 7 610 assert layer.Paint.x0 == 50 611 assert layer.Paint.y0 == 50 612 assert layer.Paint.r0 == 30 613 assert layer.Paint.x1 == 75 614 assert layer.Paint.y1 == 75 615 assert layer.Paint.r1 == 10 616 617 618def test_buildPaintGlyph_Dict_Solid(): 619 layer = _build( 620 ot.Paint, 621 ( 622 int(ot.PaintFormat.PaintGlyph), 623 (int(ot.PaintFormat.PaintSolid), 1), 624 "a", 625 ), 626 ) 627 assert layer.Format == ot.PaintFormat.PaintGlyph 628 assert layer.Format == ot.PaintFormat.PaintGlyph 629 assert layer.Glyph == "a" 630 assert layer.Paint.Format == ot.PaintFormat.PaintSolid 631 assert layer.Paint.PaletteIndex == 1 632 633 634def test_buildPaintGlyph_Dict_VarLinearGradient(): 635 layer = _build( 636 ot.Paint, 637 { 638 "Format": ot.PaintFormat.PaintGlyph, 639 "Glyph": "a", 640 "Paint": { 641 "Format": int(ot.PaintFormat.PaintVarLinearGradient), 642 "ColorLine": {"ColorStop": [(0.0, 0), (1.0, 1)]}, 643 "x0": 0, 644 "y0": 0, 645 "x1": 10, 646 "y1": 10, 647 }, 648 }, 649 ) 650 assert layer.Format == ot.PaintFormat.PaintGlyph 651 assert layer.Glyph == "a" 652 assert layer.Paint.Format == ot.PaintFormat.PaintVarLinearGradient 653 assert layer.Paint.ColorLine.ColorStop[0].StopOffset == 0.0 654 655 656def test_buildPaintGlyph_Dict_RadialGradient(): 657 layer = _buildPaint( 658 { 659 "Glyph": "a", 660 "Paint": { 661 "Format": int(ot.PaintFormat.PaintRadialGradient), 662 "ColorLine": {"ColorStop": [(0.0, 0), (1.0, 1)]}, 663 "x0": 0, 664 "y0": 0, 665 "r0": 4, 666 "x1": 10, 667 "y1": 10, 668 "r1": 0, 669 }, 670 "Format": int(ot.PaintFormat.PaintGlyph), 671 }, 672 ) 673 assert layer.Paint.Format == ot.PaintFormat.PaintRadialGradient 674 assert layer.Paint.r0 == 4 675 676 677def test_buildPaintColrGlyph(): 678 paint = _buildPaint((int(ot.PaintFormat.PaintColrGlyph), "a")) 679 assert paint.Format == ot.PaintFormat.PaintColrGlyph 680 assert paint.Glyph == "a" 681 682 683def checkBuildPaintTransform(fmt): 684 variable = _is_var(fmt) 685 if variable: 686 affine_cls = ot.VarAffine2x3 687 else: 688 affine_cls = ot.Affine2x3 689 690 affine_src = [1, 2, 3, 4, 5, 6] 691 if variable: 692 affine_src.append(7) 693 694 paint = _buildPaint( 695 ( 696 int(fmt), 697 (ot.PaintFormat.PaintGlyph, (ot.PaintFormat.PaintSolid, 0, 1.0), "a"), 698 _build(affine_cls, tuple(affine_src)), 699 ), 700 ) 701 702 assert paint.Format == fmt 703 assert paint.Paint.Format == ot.PaintFormat.PaintGlyph 704 assert paint.Paint.Paint.Format == ot.PaintFormat.PaintSolid 705 706 assert paint.Transform.xx == 1.0 707 assert paint.Transform.yx == 2.0 708 assert paint.Transform.xy == 3.0 709 assert paint.Transform.yy == 4.0 710 assert paint.Transform.dx == 5.0 711 assert paint.Transform.dy == 6.0 712 if variable: 713 assert paint.Transform.VarIndexBase == 7 714 715 affine_src = [1, 2, 3, 0.3333, 10, 10] 716 if variable: 717 affine_src.append(456) # VarIndexBase 718 paint = _build( 719 ot.Paint, 720 { 721 "Format": fmt, 722 "Transform": tuple(affine_src), 723 "Paint": { 724 "Format": int(ot.PaintFormat.PaintRadialGradient), 725 "ColorLine": {"ColorStop": [(0.0, 0), (1.0, 1)]}, 726 "x0": 100, 727 "y0": 101, 728 "x1": 102, 729 "y1": 103, 730 "r0": 0, 731 "r1": 50, 732 }, 733 }, 734 ) 735 736 assert paint.Format == fmt 737 assert paint.Transform.xx == 1.0 738 assert paint.Transform.yx == 2.0 739 assert paint.Transform.xy == 3.0 740 assert paint.Transform.yy == 0.3333 741 assert paint.Transform.dx == 10 742 assert paint.Transform.dy == 10 743 if variable: 744 assert paint.Transform.VarIndexBase == 456 745 assert paint.Paint.Format == ot.PaintFormat.PaintRadialGradient 746 747 748def test_buildPaintTransform(): 749 assert not _is_var(ot.PaintFormat.PaintTransform) 750 checkBuildPaintTransform(ot.PaintFormat.PaintTransform) 751 752 753def test_buildPaintVarTransform(): 754 assert _is_var(ot.PaintFormat.PaintVarTransform) 755 checkBuildPaintTransform(ot.PaintFormat.PaintVarTransform) 756 757 758def test_buildPaintComposite(): 759 composite = _build( 760 ot.Paint, 761 { 762 "Format": int(ot.PaintFormat.PaintComposite), 763 "CompositeMode": "src_over", 764 "SourcePaint": { 765 "Format": ot.PaintFormat.PaintComposite, 766 "CompositeMode": "src_over", 767 "SourcePaint": { 768 "Format": int(ot.PaintFormat.PaintGlyph), 769 "Glyph": "c", 770 "Paint": (ot.PaintFormat.PaintSolid, 2), 771 }, 772 "BackdropPaint": { 773 "Format": int(ot.PaintFormat.PaintGlyph), 774 "Glyph": "b", 775 "Paint": (ot.PaintFormat.PaintSolid, 1), 776 }, 777 }, 778 "BackdropPaint": { 779 "Format": ot.PaintFormat.PaintGlyph, 780 "Glyph": "a", 781 "Paint": { 782 "Format": ot.PaintFormat.PaintSolid, 783 "PaletteIndex": 0, 784 "Alpha": 0.5, 785 }, 786 }, 787 }, 788 ) 789 790 assert composite.Format == ot.PaintFormat.PaintComposite 791 assert composite.SourcePaint.Format == ot.PaintFormat.PaintComposite 792 assert composite.SourcePaint.SourcePaint.Format == ot.PaintFormat.PaintGlyph 793 assert composite.SourcePaint.SourcePaint.Glyph == "c" 794 assert composite.SourcePaint.SourcePaint.Paint.Format == ot.PaintFormat.PaintSolid 795 assert composite.SourcePaint.SourcePaint.Paint.PaletteIndex == 2 796 assert composite.SourcePaint.CompositeMode == ot.CompositeMode.SRC_OVER 797 assert composite.SourcePaint.BackdropPaint.Format == ot.PaintFormat.PaintGlyph 798 assert composite.SourcePaint.BackdropPaint.Glyph == "b" 799 assert composite.SourcePaint.BackdropPaint.Paint.Format == ot.PaintFormat.PaintSolid 800 assert composite.SourcePaint.BackdropPaint.Paint.PaletteIndex == 1 801 assert composite.CompositeMode == ot.CompositeMode.SRC_OVER 802 assert composite.BackdropPaint.Format == ot.PaintFormat.PaintGlyph 803 assert composite.BackdropPaint.Glyph == "a" 804 assert composite.BackdropPaint.Paint.Format == ot.PaintFormat.PaintSolid 805 assert composite.BackdropPaint.Paint.PaletteIndex == 0 806 assert composite.BackdropPaint.Paint.Alpha == 0.5 807 808 809def checkBuildPaintTranslate(fmt): 810 variable = _is_var(fmt) 811 812 source = { 813 "Format": fmt, 814 "Paint": ( 815 ot.PaintFormat.PaintGlyph, 816 (ot.PaintFormat.PaintSolid, 0, 1.0), 817 "a", 818 ), 819 "dx": 123, 820 "dy": -345, 821 } 822 if variable: 823 source["VarIndexBase"] = 678 824 825 paint = _build(ot.Paint, source) 826 827 assert paint.Format == fmt 828 assert paint.Paint.Format == ot.PaintFormat.PaintGlyph 829 assert paint.dx == 123 830 assert paint.dy == -345 831 if variable: 832 assert paint.VarIndexBase == 678 833 834 835def test_buildPaintTranslate(): 836 assert not _is_var(ot.PaintFormat.PaintTranslate) 837 checkBuildPaintTranslate(ot.PaintFormat.PaintTranslate) 838 839 840def test_buildPaintVarTranslate(): 841 assert _is_var(ot.PaintFormat.PaintVarTranslate) 842 checkBuildPaintTranslate(ot.PaintFormat.PaintVarTranslate) 843 844 845def checkBuildPaintScale(fmt): 846 variable = _is_var(fmt) 847 around_center = _is_around_center(fmt) 848 uniform = _is_uniform_scale(fmt) 849 850 source = { 851 "Format": fmt, 852 "Paint": ( 853 ot.PaintFormat.PaintGlyph, 854 (ot.PaintFormat.PaintSolid, 0, 1.0), 855 "a", 856 ), 857 } 858 if uniform: 859 source["scale"] = 1.5 860 else: 861 source["scaleX"] = 1.0 862 source["scaleY"] = 2.0 863 if around_center: 864 source["centerX"] = 127 865 source["centerY"] = 129 866 if variable: 867 source["VarIndexBase"] = 666 868 869 paint = _build(ot.Paint, source) 870 871 assert paint.Format == fmt 872 assert paint.Paint.Format == ot.PaintFormat.PaintGlyph 873 if uniform: 874 assert paint.scale == 1.5 875 else: 876 assert paint.scaleX == 1.0 877 assert paint.scaleY == 2.0 878 if around_center: 879 assert paint.centerX == 127 880 assert paint.centerY == 129 881 if variable: 882 assert paint.VarIndexBase == 666 883 884 885def test_buildPaintScale(): 886 assert not _is_var(ot.PaintFormat.PaintScale) 887 assert not _is_uniform_scale(ot.PaintFormat.PaintScale) 888 assert not _is_around_center(ot.PaintFormat.PaintScale) 889 checkBuildPaintScale(ot.PaintFormat.PaintScale) 890 891 892def test_buildPaintVarScale(): 893 assert _is_var(ot.PaintFormat.PaintVarScale) 894 assert not _is_uniform_scale(ot.PaintFormat.PaintVarScale) 895 assert not _is_around_center(ot.PaintFormat.PaintVarScale) 896 checkBuildPaintScale(ot.PaintFormat.PaintVarScale) 897 898 899def test_buildPaintScaleAroundCenter(): 900 assert not _is_var(ot.PaintFormat.PaintScaleAroundCenter) 901 assert not _is_uniform_scale(ot.PaintFormat.PaintScaleAroundCenter) 902 assert _is_around_center(ot.PaintFormat.PaintScaleAroundCenter) 903 checkBuildPaintScale(ot.PaintFormat.PaintScaleAroundCenter) 904 905 906def test_buildPaintVarScaleAroundCenter(): 907 assert _is_var(ot.PaintFormat.PaintVarScaleAroundCenter) 908 assert not _is_uniform_scale(ot.PaintFormat.PaintScaleAroundCenter) 909 assert _is_around_center(ot.PaintFormat.PaintVarScaleAroundCenter) 910 checkBuildPaintScale(ot.PaintFormat.PaintVarScaleAroundCenter) 911 912 913def test_buildPaintScaleUniform(): 914 assert not _is_var(ot.PaintFormat.PaintScaleUniform) 915 assert _is_uniform_scale(ot.PaintFormat.PaintScaleUniform) 916 assert not _is_around_center(ot.PaintFormat.PaintScaleUniform) 917 checkBuildPaintScale(ot.PaintFormat.PaintScaleUniform) 918 919 920def test_buildPaintVarScaleUniform(): 921 assert _is_var(ot.PaintFormat.PaintVarScaleUniform) 922 assert _is_uniform_scale(ot.PaintFormat.PaintVarScaleUniform) 923 assert not _is_around_center(ot.PaintFormat.PaintVarScaleUniform) 924 checkBuildPaintScale(ot.PaintFormat.PaintVarScaleUniform) 925 926 927def test_buildPaintScaleUniformAroundCenter(): 928 assert not _is_var(ot.PaintFormat.PaintScaleUniformAroundCenter) 929 assert _is_uniform_scale(ot.PaintFormat.PaintScaleUniformAroundCenter) 930 assert _is_around_center(ot.PaintFormat.PaintScaleUniformAroundCenter) 931 checkBuildPaintScale(ot.PaintFormat.PaintScaleUniformAroundCenter) 932 933 934def test_buildPaintVarScaleUniformAroundCenter(): 935 assert _is_var(ot.PaintFormat.PaintVarScaleUniformAroundCenter) 936 assert _is_uniform_scale(ot.PaintFormat.PaintVarScaleUniformAroundCenter) 937 assert _is_around_center(ot.PaintFormat.PaintVarScaleUniformAroundCenter) 938 checkBuildPaintScale(ot.PaintFormat.PaintVarScaleUniformAroundCenter) 939 940 941def checkBuildPaintRotate(fmt): 942 variable = _is_var(fmt) 943 around_center = _is_around_center(fmt) 944 945 source = { 946 "Format": fmt, 947 "Paint": ( 948 ot.PaintFormat.PaintGlyph, 949 (ot.PaintFormat.PaintSolid, 0, 1.0), 950 "a", 951 ), 952 "angle": 15, 953 } 954 if around_center: 955 source["centerX"] = 127 956 source["centerY"] = 129 957 958 paint = _build(ot.Paint, source) 959 960 assert paint.Format == fmt 961 assert paint.Paint.Format == ot.PaintFormat.PaintGlyph 962 assert paint.angle == 15 963 if around_center: 964 assert paint.centerX == 127 965 assert paint.centerY == 129 966 if variable: 967 assert paint.VarIndexBase == 0xFFFFFFFF 968 969 970def test_buildPaintRotate(): 971 assert not _is_var(ot.PaintFormat.PaintRotate) 972 assert not _is_around_center(ot.PaintFormat.PaintRotate) 973 checkBuildPaintRotate(ot.PaintFormat.PaintRotate) 974 975 976def test_buildPaintVarRotate(): 977 assert _is_var(ot.PaintFormat.PaintVarRotate) 978 assert not _is_around_center(ot.PaintFormat.PaintVarRotate) 979 checkBuildPaintRotate(ot.PaintFormat.PaintVarRotate) 980 981 982def test_buildPaintRotateAroundCenter(): 983 assert not _is_var(ot.PaintFormat.PaintRotateAroundCenter) 984 assert _is_around_center(ot.PaintFormat.PaintRotateAroundCenter) 985 checkBuildPaintRotate(ot.PaintFormat.PaintRotateAroundCenter) 986 987 988def test_buildPaintVarRotateAroundCenter(): 989 assert _is_var(ot.PaintFormat.PaintVarRotateAroundCenter) 990 assert _is_around_center(ot.PaintFormat.PaintVarRotateAroundCenter) 991 checkBuildPaintRotate(ot.PaintFormat.PaintVarRotateAroundCenter) 992 993 994def checkBuildPaintSkew(fmt): 995 variable = _is_var(fmt) 996 around_center = _is_around_center(fmt) 997 998 source = { 999 "Format": fmt, 1000 "Paint": ( 1001 ot.PaintFormat.PaintGlyph, 1002 (ot.PaintFormat.PaintSolid, 0, 1.0), 1003 "a", 1004 ), 1005 "xSkewAngle": 15, 1006 "ySkewAngle": 42, 1007 } 1008 if around_center: 1009 source["centerX"] = 127 1010 source["centerY"] = 129 1011 if variable: 1012 source["VarIndexBase"] = 0 1013 1014 paint = _build(ot.Paint, source) 1015 1016 assert paint.Format == fmt 1017 assert paint.Paint.Format == ot.PaintFormat.PaintGlyph 1018 assert paint.xSkewAngle == 15 1019 assert paint.ySkewAngle == 42 1020 if around_center: 1021 assert paint.centerX == 127 1022 assert paint.centerY == 129 1023 if variable: 1024 assert paint.VarIndexBase == 0 1025 1026 1027def test_buildPaintSkew(): 1028 assert not _is_var(ot.PaintFormat.PaintSkew) 1029 assert not _is_around_center(ot.PaintFormat.PaintSkew) 1030 checkBuildPaintSkew(ot.PaintFormat.PaintSkew) 1031 1032 1033def test_buildPaintVarSkew(): 1034 assert _is_var(ot.PaintFormat.PaintVarSkew) 1035 assert not _is_around_center(ot.PaintFormat.PaintVarSkew) 1036 checkBuildPaintSkew(ot.PaintFormat.PaintVarSkew) 1037 1038 1039def test_buildPaintSkewAroundCenter(): 1040 assert not _is_var(ot.PaintFormat.PaintSkewAroundCenter) 1041 assert _is_around_center(ot.PaintFormat.PaintSkewAroundCenter) 1042 checkBuildPaintSkew(ot.PaintFormat.PaintSkewAroundCenter) 1043 1044 1045def test_buildPaintVarSkewAroundCenter(): 1046 assert _is_var(ot.PaintFormat.PaintVarSkewAroundCenter) 1047 assert _is_around_center(ot.PaintFormat.PaintVarSkewAroundCenter) 1048 checkBuildPaintSkew(ot.PaintFormat.PaintVarSkewAroundCenter) 1049 1050 1051def test_buildColrV1(): 1052 colorGlyphs = { 1053 "a": ( 1054 ot.PaintFormat.PaintColrLayers, 1055 [ 1056 (ot.PaintFormat.PaintGlyph, (ot.PaintFormat.PaintSolid, 0), "b"), 1057 (ot.PaintFormat.PaintGlyph, (ot.PaintFormat.PaintVarSolid, 1), "c"), 1058 ], 1059 ), 1060 "d": ( 1061 ot.PaintFormat.PaintColrLayers, 1062 [ 1063 ( 1064 ot.PaintFormat.PaintGlyph, 1065 { 1066 "Format": int(ot.PaintFormat.PaintSolid), 1067 "PaletteIndex": 2, 1068 "Alpha": 0.8, 1069 }, 1070 "e", 1071 ), 1072 ( 1073 ot.PaintFormat.PaintGlyph, 1074 { 1075 "Format": int(ot.PaintFormat.PaintVarRadialGradient), 1076 "ColorLine": { 1077 "ColorStop": [(0.0, 3), (1.0, 4)], 1078 "Extend": "reflect", 1079 }, 1080 "x0": 0, 1081 "y0": 0, 1082 "x1": 0, 1083 "y1": 0, 1084 "r0": 10, 1085 "r1": 0, 1086 }, 1087 "f", 1088 ), 1089 ], 1090 ), 1091 "g": ( 1092 ot.PaintFormat.PaintColrLayers, 1093 [(ot.PaintFormat.PaintGlyph, (ot.PaintFormat.PaintSolid, 5), "h")], 1094 ), 1095 } 1096 glyphMap = { 1097 ".notdef": 0, 1098 "a": 4, 1099 "b": 3, 1100 "c": 2, 1101 "d": 1, 1102 "e": 5, 1103 "f": 6, 1104 "g": 7, 1105 "h": 8, 1106 } 1107 1108 # TODO(anthrotype) should we split into two tests? - seems two distinct validations 1109 layers, baseGlyphs = builder.buildColrV1(colorGlyphs, glyphMap) 1110 assert baseGlyphs.BaseGlyphCount == len(colorGlyphs) 1111 assert baseGlyphs.BaseGlyphPaintRecord[0].BaseGlyph == "d" 1112 assert baseGlyphs.BaseGlyphPaintRecord[1].BaseGlyph == "a" 1113 assert baseGlyphs.BaseGlyphPaintRecord[2].BaseGlyph == "g" 1114 1115 layers, baseGlyphs = builder.buildColrV1(colorGlyphs) 1116 assert baseGlyphs.BaseGlyphCount == len(colorGlyphs) 1117 assert baseGlyphs.BaseGlyphPaintRecord[0].BaseGlyph == "a" 1118 assert baseGlyphs.BaseGlyphPaintRecord[1].BaseGlyph == "d" 1119 assert baseGlyphs.BaseGlyphPaintRecord[2].BaseGlyph == "g" 1120 1121 1122def test_buildColrV1_more_than_255_paints(): 1123 num_paints = 364 1124 colorGlyphs = { 1125 "a": ( 1126 ot.PaintFormat.PaintColrLayers, 1127 [ 1128 { 1129 "Format": int(ot.PaintFormat.PaintGlyph), 1130 "Paint": (ot.PaintFormat.PaintSolid, 0), 1131 "Glyph": name, 1132 } 1133 for name in (f"glyph{i}" for i in range(num_paints)) 1134 ], 1135 ), 1136 } 1137 layers, baseGlyphs = builder.buildColrV1(colorGlyphs) 1138 paints = layers.Paint 1139 1140 assert len(paints) == num_paints + 1 1141 1142 assert all(paints[i].Format == ot.PaintFormat.PaintGlyph for i in range(255)) 1143 1144 assert paints[255].Format == ot.PaintFormat.PaintColrLayers 1145 assert paints[255].FirstLayerIndex == 0 1146 assert paints[255].NumLayers == 255 1147 1148 assert all( 1149 paints[i].Format == ot.PaintFormat.PaintGlyph 1150 for i in range(256, num_paints + 1) 1151 ) 1152 1153 assert baseGlyphs.BaseGlyphCount == len(colorGlyphs) 1154 assert baseGlyphs.BaseGlyphPaintRecord[0].BaseGlyph == "a" 1155 assert ( 1156 baseGlyphs.BaseGlyphPaintRecord[0].Paint.Format 1157 == ot.PaintFormat.PaintColrLayers 1158 ) 1159 assert baseGlyphs.BaseGlyphPaintRecord[0].Paint.FirstLayerIndex == 255 1160 assert baseGlyphs.BaseGlyphPaintRecord[0].Paint.NumLayers == num_paints + 1 - 255 1161 1162 1163def test_split_color_glyphs_by_version(): 1164 layerBuilder = LayerListBuilder() 1165 colorGlyphs = { 1166 "a": [ 1167 ("b", 0), 1168 ("c", 1), 1169 ("d", 2), 1170 ("e", 3), 1171 ] 1172 } 1173 1174 colorGlyphsV0, colorGlyphsV1 = builder._split_color_glyphs_by_version(colorGlyphs) 1175 1176 assert colorGlyphsV0 == {"a": [("b", 0), ("c", 1), ("d", 2), ("e", 3)]} 1177 assert not colorGlyphsV1 1178 1179 colorGlyphs = {"a": (ot.PaintFormat.PaintGlyph, 0, "b")} 1180 1181 colorGlyphsV0, colorGlyphsV1 = builder._split_color_glyphs_by_version(colorGlyphs) 1182 1183 assert not colorGlyphsV0 1184 assert colorGlyphsV1 == colorGlyphs 1185 1186 colorGlyphs = { 1187 "a": [("b", 0)], 1188 "c": [ 1189 ("d", 1), 1190 ( 1191 "e", 1192 { 1193 "format": 3, 1194 "colorLine": {"stops": [(0.0, 2), (1.0, 3)]}, 1195 "p0": (0, 0), 1196 "p1": (10, 10), 1197 }, 1198 ), 1199 ], 1200 } 1201 1202 colorGlyphsV0, colorGlyphsV1 = builder._split_color_glyphs_by_version(colorGlyphs) 1203 1204 assert colorGlyphsV0 == {"a": [("b", 0)]} 1205 assert "a" not in colorGlyphsV1 1206 assert "c" in colorGlyphsV1 1207 assert len(colorGlyphsV1["c"]) == 2 1208 1209 1210def assertIsColrV1(colr): 1211 assert colr.version == 1 1212 assert not hasattr(colr, "ColorLayers") 1213 assert hasattr(colr, "table") 1214 assert isinstance(colr.table, ot.COLR) 1215 1216 1217def assertNoV0Content(colr): 1218 assert colr.table.BaseGlyphRecordCount == 0 1219 assert colr.table.BaseGlyphRecordArray is None 1220 assert colr.table.LayerRecordCount == 0 1221 assert colr.table.LayerRecordArray is None 1222 1223 1224def test_build_layerv1list_empty(): 1225 # Nobody uses PaintColrLayers, no layerlist 1226 colr = builder.buildCOLR( 1227 { 1228 # BaseGlyph, tuple form 1229 "a": ( 1230 int(ot.PaintFormat.PaintGlyph), 1231 (int(ot.PaintFormat.PaintSolid), 2, 0.8), 1232 "b", 1233 ), 1234 # BaseGlyph, map form 1235 "b": { 1236 "Format": int(ot.PaintFormat.PaintGlyph), 1237 "Paint": { 1238 "Format": int(ot.PaintFormat.PaintLinearGradient), 1239 "ColorLine": { 1240 "ColorStop": [(0.0, 2), (1.0, 3)], 1241 "Extend": "reflect", 1242 }, 1243 "x0": 1, 1244 "y0": 2, 1245 "x1": 3, 1246 "y1": 4, 1247 "x2": 2, 1248 "y2": 2, 1249 }, 1250 "Glyph": "bb", 1251 }, 1252 }, 1253 version=1, 1254 ) 1255 1256 assertIsColrV1(colr) 1257 assertNoV0Content(colr) 1258 1259 # 2 v1 glyphs, none in LayerList 1260 assert colr.table.BaseGlyphList.BaseGlyphCount == 2 1261 assert len(colr.table.BaseGlyphList.BaseGlyphPaintRecord) == 2 1262 assert colr.table.LayerList is None 1263 1264 1265def _paint_names(paints) -> List[str]: 1266 # prints a predictable string from a paint list to enable 1267 # semi-readable assertions on a LayerList order. 1268 result = [] 1269 for paint in paints: 1270 if paint.Format == int(ot.PaintFormat.PaintGlyph): 1271 result.append(paint.Glyph) 1272 elif paint.Format == int(ot.PaintFormat.PaintColrLayers): 1273 result.append( 1274 f"Layers[{paint.FirstLayerIndex}:{paint.FirstLayerIndex+paint.NumLayers}]" 1275 ) 1276 return result 1277 1278 1279def test_build_layerv1list_simple(): 1280 # Two colr glyphs, each with two layers the first of which is common 1281 # All layers use the same solid paint 1282 solid_paint = { 1283 "Format": int(ot.PaintFormat.PaintSolid), 1284 "PaletteIndex": 2, 1285 "Alpha": 0.8, 1286 } 1287 backdrop = { 1288 "Format": int(ot.PaintFormat.PaintGlyph), 1289 "Paint": solid_paint, 1290 "Glyph": "back", 1291 } 1292 a_foreground = { 1293 "Format": int(ot.PaintFormat.PaintGlyph), 1294 "Paint": solid_paint, 1295 "Glyph": "a_fore", 1296 } 1297 b_foreground = { 1298 "Format": int(ot.PaintFormat.PaintGlyph), 1299 "Paint": solid_paint, 1300 "Glyph": "b_fore", 1301 } 1302 1303 # list => PaintColrLayers, contents should land in LayerList 1304 colr = builder.buildCOLR( 1305 { 1306 "a": ( 1307 ot.PaintFormat.PaintColrLayers, 1308 [ 1309 backdrop, 1310 a_foreground, 1311 ], 1312 ), 1313 "b": { 1314 "Format": ot.PaintFormat.PaintColrLayers, 1315 "Layers": [ 1316 backdrop, 1317 b_foreground, 1318 ], 1319 }, 1320 }, 1321 version=1, 1322 ) 1323 1324 assertIsColrV1(colr) 1325 assertNoV0Content(colr) 1326 1327 # 2 v1 glyphs, 4 paints in LayerList 1328 # A single shared backdrop isn't worth accessing by slice 1329 assert colr.table.BaseGlyphList.BaseGlyphCount == 2 1330 assert len(colr.table.BaseGlyphList.BaseGlyphPaintRecord) == 2 1331 assert colr.table.LayerList.LayerCount == 4 1332 assert _paint_names(colr.table.LayerList.Paint) == [ 1333 "back", 1334 "a_fore", 1335 "back", 1336 "b_fore", 1337 ] 1338 1339 1340def test_build_layerv1list_with_sharing(): 1341 # Three colr glyphs, each with two layers in common 1342 solid_paint = { 1343 "Format": int(ot.PaintFormat.PaintSolid), 1344 "PaletteIndex": 2, 1345 "Alpha": 0.8, 1346 } 1347 backdrop = [ 1348 { 1349 "Format": int(ot.PaintFormat.PaintGlyph), 1350 "Paint": solid_paint, 1351 "Glyph": "back1", 1352 }, 1353 { 1354 "Format": ot.PaintFormat.PaintGlyph, 1355 "Paint": solid_paint, 1356 "Glyph": "back2", 1357 }, 1358 ] 1359 a_foreground = { 1360 "Format": ot.PaintFormat.PaintGlyph, 1361 "Paint": solid_paint, 1362 "Glyph": "a_fore", 1363 } 1364 b_background = { 1365 "Format": ot.PaintFormat.PaintGlyph, 1366 "Paint": solid_paint, 1367 "Glyph": "b_back", 1368 } 1369 b_foreground = { 1370 "Format": ot.PaintFormat.PaintGlyph, 1371 "Paint": solid_paint, 1372 "Glyph": "b_fore", 1373 } 1374 c_background = { 1375 "Format": ot.PaintFormat.PaintGlyph, 1376 "Paint": solid_paint, 1377 "Glyph": "c_back", 1378 } 1379 1380 # list => PaintColrLayers, which means contents should be in LayerList 1381 colr = builder.buildCOLR( 1382 { 1383 "a": (ot.PaintFormat.PaintColrLayers, backdrop + [a_foreground]), 1384 "b": ( 1385 ot.PaintFormat.PaintColrLayers, 1386 [b_background] + backdrop + [b_foreground], 1387 ), 1388 "c": (ot.PaintFormat.PaintColrLayers, [c_background] + backdrop), 1389 }, 1390 version=1, 1391 ) 1392 1393 assertIsColrV1(colr) 1394 assertNoV0Content(colr) 1395 1396 # 2 v1 glyphs, 4 paints in LayerList 1397 # A single shared backdrop isn't worth accessing by slice 1398 baseGlyphs = colr.table.BaseGlyphList.BaseGlyphPaintRecord 1399 assert colr.table.BaseGlyphList.BaseGlyphCount == 3 1400 assert len(baseGlyphs) == 3 1401 assert _paint_names([b.Paint for b in baseGlyphs]) == [ 1402 "Layers[0:3]", 1403 "Layers[3:6]", 1404 "Layers[6:8]", 1405 ] 1406 assert _paint_names(colr.table.LayerList.Paint) == [ 1407 "back1", 1408 "back2", 1409 "a_fore", 1410 "b_back", 1411 "Layers[0:2]", 1412 "b_fore", 1413 "c_back", 1414 "Layers[0:2]", 1415 ] 1416 assert colr.table.LayerList.LayerCount == 8 1417 1418 1419def test_build_layerv1list_with_overlaps(): 1420 paints = [ 1421 { 1422 "Format": ot.PaintFormat.PaintGlyph, 1423 "Paint": { 1424 "Format": ot.PaintFormat.PaintSolid, 1425 "PaletteIndex": 2, 1426 "Alpha": 0.8, 1427 }, 1428 "Glyph": c, 1429 } 1430 for c in "abcdefghi" 1431 ] 1432 1433 # list => PaintColrLayers, which means contents should be in LayerList 1434 colr = builder.buildCOLR( 1435 { 1436 "a": (ot.PaintFormat.PaintColrLayers, paints[0:4]), 1437 "b": (ot.PaintFormat.PaintColrLayers, paints[0:6]), 1438 "c": (ot.PaintFormat.PaintColrLayers, paints[2:8]), 1439 }, 1440 version=1, 1441 ) 1442 1443 assertIsColrV1(colr) 1444 assertNoV0Content(colr) 1445 1446 baseGlyphs = colr.table.BaseGlyphList.BaseGlyphPaintRecord 1447 # assert colr.table.BaseGlyphList.BaseGlyphCount == 2 1448 1449 assert _paint_names(colr.table.LayerList.Paint) == [ 1450 "a", 1451 "b", 1452 "c", 1453 "d", 1454 "Layers[0:4]", 1455 "e", 1456 "f", 1457 "Layers[2:4]", 1458 "Layers[5:7]", 1459 "g", 1460 "h", 1461 ] 1462 assert _paint_names([b.Paint for b in baseGlyphs]) == [ 1463 "Layers[0:4]", 1464 "Layers[4:7]", 1465 "Layers[7:11]", 1466 ] 1467 assert colr.table.LayerList.LayerCount == 11 1468 1469 1470def test_explicit_version_1(): 1471 colr = builder.buildCOLR( 1472 { 1473 "a": ( 1474 ot.PaintFormat.PaintColrLayers, 1475 [ 1476 (ot.PaintFormat.PaintGlyph, (ot.PaintFormat.PaintSolid, 0), "b"), 1477 (ot.PaintFormat.PaintGlyph, (ot.PaintFormat.PaintSolid, 1), "c"), 1478 ], 1479 ) 1480 }, 1481 version=1, 1482 ) 1483 assert colr.version == 1 1484 assert not hasattr(colr, "ColorLayers") 1485 assert hasattr(colr, "table") 1486 assert isinstance(colr.table, ot.COLR) 1487 assert colr.table.VarStore is None 1488 1489 1490class BuildCOLRTest(object): 1491 def test_automatic_version_all_solid_color_glyphs(self): 1492 colr = builder.buildCOLR({"a": [("b", 0), ("c", 1)]}) 1493 assert colr.version == 0 1494 assert hasattr(colr, "ColorLayers") 1495 assert colr.ColorLayers["a"][0].name == "b" 1496 assert colr.ColorLayers["a"][1].name == "c" 1497 1498 def test_automatic_version_no_solid_color_glyphs(self): 1499 colr = builder.buildCOLR( 1500 { 1501 "a": ( 1502 ot.PaintFormat.PaintColrLayers, 1503 [ 1504 ( 1505 ot.PaintFormat.PaintGlyph, 1506 { 1507 "Format": int(ot.PaintFormat.PaintRadialGradient), 1508 "ColorLine": { 1509 "ColorStop": [(0.0, 0), (1.0, 1)], 1510 "Extend": "repeat", 1511 }, 1512 "x0": 1, 1513 "y0": 0, 1514 "x1": 10, 1515 "y1": 0, 1516 "r0": 4, 1517 "r1": 2, 1518 }, 1519 "b", 1520 ), 1521 ( 1522 ot.PaintFormat.PaintGlyph, 1523 { 1524 "Format": ot.PaintFormat.PaintSolid, 1525 "PaletteIndex": 2, 1526 "Alpha": 0.8, 1527 }, 1528 "c", 1529 ), 1530 ], 1531 ), 1532 "d": ( 1533 ot.PaintFormat.PaintColrLayers, 1534 [ 1535 { 1536 "Format": ot.PaintFormat.PaintGlyph, 1537 "Glyph": "e", 1538 "Paint": { 1539 "Format": ot.PaintFormat.PaintLinearGradient, 1540 "ColorLine": { 1541 "ColorStop": [(0.0, 2), (1.0, 3)], 1542 "Extend": "reflect", 1543 }, 1544 "x0": 1, 1545 "y0": 2, 1546 "x1": 3, 1547 "y1": 4, 1548 "x2": 2, 1549 "y2": 2, 1550 }, 1551 } 1552 ], 1553 ), 1554 } 1555 ) 1556 assertIsColrV1(colr) 1557 assert colr.table.BaseGlyphRecordCount == 0 1558 assert colr.table.BaseGlyphRecordArray is None 1559 assert colr.table.LayerRecordCount == 0 1560 assert colr.table.LayerRecordArray is None 1561 1562 def test_automatic_version_mixed_solid_and_gradient_glyphs(self): 1563 colr = builder.buildCOLR( 1564 { 1565 "a": [("b", 0), ("c", 1)], 1566 "d": ( 1567 ot.PaintFormat.PaintColrLayers, 1568 [ 1569 ( 1570 ot.PaintFormat.PaintGlyph, 1571 { 1572 "Format": ot.PaintFormat.PaintLinearGradient, 1573 "ColorLine": {"ColorStop": [(0.0, 2), (1.0, 3)]}, 1574 "x0": 1, 1575 "y0": 2, 1576 "x1": 3, 1577 "y1": 4, 1578 "x2": 2, 1579 "y2": 2, 1580 }, 1581 "e", 1582 ), 1583 ( 1584 ot.PaintFormat.PaintGlyph, 1585 (ot.PaintFormat.PaintSolid, 2, 0.8), 1586 "f", 1587 ), 1588 ], 1589 ), 1590 } 1591 ) 1592 assertIsColrV1(colr) 1593 assert colr.table.VarStore is None 1594 1595 assert colr.table.BaseGlyphRecordCount == 1 1596 assert isinstance(colr.table.BaseGlyphRecordArray, ot.BaseGlyphRecordArray) 1597 assert colr.table.LayerRecordCount == 2 1598 assert isinstance(colr.table.LayerRecordArray, ot.LayerRecordArray) 1599 1600 assert isinstance(colr.table.BaseGlyphList, ot.BaseGlyphList) 1601 assert colr.table.BaseGlyphList.BaseGlyphCount == 1 1602 assert isinstance( 1603 colr.table.BaseGlyphList.BaseGlyphPaintRecord[0], ot.BaseGlyphPaintRecord 1604 ) 1605 assert colr.table.BaseGlyphList.BaseGlyphPaintRecord[0].BaseGlyph == "d" 1606 assert isinstance(colr.table.LayerList, ot.LayerList) 1607 assert colr.table.LayerList.Paint[0].Glyph == "e" 1608 1609 def test_explicit_version_0(self): 1610 colr = builder.buildCOLR({"a": [("b", 0), ("c", 1)]}, version=0) 1611 assert colr.version == 0 1612 assert hasattr(colr, "ColorLayers") 1613 1614 def test_explicit_version_1(self): 1615 colr = builder.buildCOLR( 1616 { 1617 "a": ( 1618 ot.PaintFormat.PaintColrLayers, 1619 [ 1620 ( 1621 ot.PaintFormat.PaintGlyph, 1622 (ot.PaintFormat.PaintSolid, 0), 1623 "b", 1624 ), 1625 ( 1626 ot.PaintFormat.PaintGlyph, 1627 (ot.PaintFormat.PaintSolid, 1), 1628 "c", 1629 ), 1630 ], 1631 ) 1632 }, 1633 version=1, 1634 ) 1635 assert colr.version == 1 1636 assert not hasattr(colr, "ColorLayers") 1637 assert hasattr(colr, "table") 1638 assert isinstance(colr.table, ot.COLR) 1639 assert colr.table.VarStore is None 1640 1641 def test_paint_one_colr_layers(self): 1642 # A set of one layers should flip to just that layer 1643 colr = builder.buildCOLR( 1644 { 1645 "a": ( 1646 ot.PaintFormat.PaintColrLayers, 1647 [ 1648 ( 1649 ot.PaintFormat.PaintGlyph, 1650 (ot.PaintFormat.PaintSolid, 0), 1651 "b", 1652 ), 1653 ], 1654 ) 1655 }, 1656 ) 1657 1658 assert colr.table.LayerList is None, "PaintColrLayers should be gone" 1659 assert colr.table.BaseGlyphList.BaseGlyphCount == 1 1660 paint = colr.table.BaseGlyphList.BaseGlyphPaintRecord[0].Paint 1661 assert paint.Format == ot.PaintFormat.PaintGlyph 1662 assert paint.Paint.Format == ot.PaintFormat.PaintSolid 1663 1664 def test_build_clip_list(self): 1665 colr = builder.buildCOLR( 1666 { 1667 "a": ( 1668 ot.PaintFormat.PaintGlyph, 1669 (ot.PaintFormat.PaintSolid, 0), 1670 "b", 1671 ), 1672 "c": ( 1673 ot.PaintFormat.PaintGlyph, 1674 (ot.PaintFormat.PaintSolid, 1), 1675 "d", 1676 ), 1677 }, 1678 clipBoxes={ 1679 "a": (0, 0, 1000, 1000, 0), # optional 5th: varIndexBase 1680 "c": (-100.8, -200.4, 1100.1, 1200.5), # floats get rounded 1681 "e": (0, 0, 10, 10), # 'e' does _not_ get ignored despite being missing 1682 }, 1683 ) 1684 1685 assert colr.table.ClipList.Format == 1 1686 clipBoxes = colr.table.ClipList.clips 1687 assert [ 1688 (baseGlyph, clipBox.as_tuple()) for baseGlyph, clipBox in clipBoxes.items() 1689 ] == [ 1690 ("a", (0, 0, 1000, 1000, 0)), 1691 ("c", (-101, -201, 1101, 1201)), 1692 ("e", (0, 0, 10, 10)), 1693 ] 1694 assert clipBoxes["a"].Format == 2 1695 assert clipBoxes["c"].Format == 1 1696 assert clipBoxes["e"].Format == 1 1697 1698 def test_duplicate_base_glyphs(self): 1699 # If > 1 base glyphs refer to equivalent list of layers we expect them to share 1700 # the same PaintColrLayers. 1701 layers = { 1702 "Format": ot.PaintFormat.PaintColrLayers, 1703 "Layers": [ 1704 (ot.PaintFormat.PaintGlyph, (ot.PaintFormat.PaintSolid, 0), "d"), 1705 (ot.PaintFormat.PaintGlyph, (ot.PaintFormat.PaintSolid, 1), "e"), 1706 ], 1707 } 1708 # I copy the layers to ensure equality is by content, not by identity 1709 colr = builder.buildCOLR( 1710 {"a": layers, "b": deepcopy(layers), "c": deepcopy(layers)} 1711 ).table 1712 1713 baseGlyphs = colr.BaseGlyphList.BaseGlyphPaintRecord 1714 assert len(baseGlyphs) == 3 1715 1716 assert baseGlyphs[0].BaseGlyph == "a" 1717 assert baseGlyphs[1].BaseGlyph == "b" 1718 assert baseGlyphs[2].BaseGlyph == "c" 1719 1720 expected = {"Format": 1, "FirstLayerIndex": 0, "NumLayers": 2} 1721 assert baseGlyphs[0].Paint.__dict__ == expected 1722 assert baseGlyphs[1].Paint.__dict__ == expected 1723 assert baseGlyphs[2].Paint.__dict__ == expected 1724 1725 1726class TrickyRadialGradientTest: 1727 @staticmethod 1728 def circle_inside_circle(c0, r0, c1, r1, rounded=False): 1729 if rounded: 1730 return Circle(c0, r0).round().inside(Circle(c1, r1).round()) 1731 else: 1732 return Circle(c0, r0).inside(Circle(c1, r1)) 1733 1734 def round_start_circle(self, c0, r0, c1, r1, inside=True): 1735 assert self.circle_inside_circle(c0, r0, c1, r1) is inside 1736 assert self.circle_inside_circle(c0, r0, c1, r1, rounded=True) is not inside 1737 r = round_start_circle_stable_containment(c0, r0, c1, r1) 1738 assert ( 1739 self.circle_inside_circle(r.centre, r.radius, c1, r1, rounded=True) 1740 is inside 1741 ) 1742 return r.centre, r.radius 1743 1744 def test_noto_emoji_mosquito_u1f99f(self): 1745 # https://github.com/googlefonts/picosvg/issues/158 1746 c0 = (385.23508, 70.56727999999998) 1747 r0 = 0 1748 c1 = (642.99108, 104.70327999999995) 1749 r1 = 260.0072 1750 assert self.round_start_circle(c0, r0, c1, r1, inside=True) == ((386, 71), 0) 1751 1752 def test_noto_emoji_horns_sign_u1f918_1f3fc(self): 1753 # This radial gradient is taken from noto-emoji's 'SIGNS OF THE HORNS' 1754 # (1f918_1f3fc). We check that c0 is inside c1 both before and after rounding. 1755 c0 = (-437.6789059060543, -2116.9237094478003) 1756 r0 = 0.0 1757 c1 = (-488.7330118252256, -1876.5036857045086) 1758 r1 = 245.77147821915673 1759 assert self.circle_inside_circle(c0, r0, c1, r1) 1760 assert self.circle_inside_circle(c0, r0, c1, r1, rounded=True) 1761 1762 @pytest.mark.parametrize( 1763 "c0, r0, c1, r1, inside, expected", 1764 [ 1765 # inside before round, outside after round 1766 ((1.4, 0), 0, (2.6, 0), 1.3, True, ((2, 0), 0)), 1767 ((1, 0), 0.6, (2.8, 0), 2.45, True, ((2, 0), 1)), 1768 ((6.49, 6.49), 0, (0.49, 0.49), 8.49, True, ((5, 5), 0)), 1769 # outside before round, inside after round 1770 ((0, 0), 0, (2, 0), 1.5, False, ((-1, 0), 0)), 1771 ((0, -0.5), 0, (0, -2.5), 1.5, False, ((0, 1), 0)), 1772 # the following ones require two nudges to round correctly 1773 ((0.5, 0), 0, (9.4, 0), 8.8, False, ((-1, 0), 0)), 1774 ((1.5, 1.5), 0, (0.49, 0.49), 1.49, True, ((0, 0), 0)), 1775 # limit case when circle almost exactly overlap 1776 ((0.5000001, 0), 0.5000001, (0.499999, 0), 0.4999999, True, ((0, 0), 0)), 1777 # concentrical circles, r0 > r1 1778 ((0, 0), 1.49, (0, 0), 1, False, ((0, 0), 2)), 1779 ], 1780 ) 1781 def test_nudge_start_circle_position(self, c0, r0, c1, r1, inside, expected): 1782 assert self.round_start_circle(c0, r0, c1, r1, inside) == expected 1783