diff options
Diffstat (limited to 'Tests/varLib/merger_test.py')
-rw-r--r-- | Tests/varLib/merger_test.py | 1844 |
1 files changed, 1844 insertions, 0 deletions
diff --git a/Tests/varLib/merger_test.py b/Tests/varLib/merger_test.py new file mode 100644 index 00000000..aa7a6998 --- /dev/null +++ b/Tests/varLib/merger_test.py @@ -0,0 +1,1844 @@ +from copy import deepcopy +import string +from fontTools.colorLib.builder import LayerListBuilder, buildCOLR, buildClipList +from fontTools.misc.testTools import getXML +from fontTools.varLib.merger import COLRVariationMerger +from fontTools.varLib.models import VariationModel +from fontTools.ttLib import TTFont +from fontTools.ttLib.tables import otTables as ot +from fontTools.ttLib.tables.otBase import OTTableReader, OTTableWriter +import pytest + + +NO_VARIATION_INDEX = ot.NO_VARIATION_INDEX + + +def dump_xml(table, ttFont=None): + xml = getXML(table.toXML, ttFont) + print("[") + for line in xml: + print(f" {line!r},") + print("]") + return xml + + +def compile_decompile(table, ttFont): + writer = OTTableWriter(tableTag="COLR") + # compile itself may modify a table, safer to copy it first + table = deepcopy(table) + table.compile(writer, ttFont) + data = writer.getAllData() + + reader = OTTableReader(data, tableTag="COLR") + table2 = table.__class__() + table2.decompile(reader, ttFont) + + return table2 + + +@pytest.fixture +def ttFont(): + font = TTFont() + font.setGlyphOrder([".notdef"] + list(string.ascii_letters)) + return font + + +def build_paint(data): + return LayerListBuilder().buildPaint(data) + + +class COLRVariationMergerTest: + @pytest.mark.parametrize( + "paints, expected_xml, expected_varIdxes", + [ + pytest.param( + [ + { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 0, + "Alpha": 1.0, + }, + { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 0, + "Alpha": 1.0, + }, + ], + [ + '<Paint Format="2"><!-- PaintSolid -->', + ' <PaletteIndex value="0"/>', + ' <Alpha value="1.0"/>', + "</Paint>", + ], + [], + id="solid-same", + ), + pytest.param( + [ + { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 0, + "Alpha": 1.0, + }, + { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 0, + "Alpha": 0.5, + }, + ], + [ + '<Paint Format="3"><!-- PaintVarSolid -->', + ' <PaletteIndex value="0"/>', + ' <Alpha value="1.0"/>', + ' <VarIndexBase value="0"/>', + "</Paint>", + ], + [0], + id="solid-alpha", + ), + pytest.param( + [ + { + "Format": int(ot.PaintFormat.PaintLinearGradient), + "ColorLine": { + "Extend": int(ot.ExtendMode.PAD), + "ColorStop": [ + {"StopOffset": 0.0, "PaletteIndex": 0, "Alpha": 1.0}, + {"StopOffset": 1.0, "PaletteIndex": 1, "Alpha": 1.0}, + ], + }, + "x0": 0, + "y0": 0, + "x1": 1, + "y1": 1, + "x2": 2, + "y2": 2, + }, + { + "Format": int(ot.PaintFormat.PaintLinearGradient), + "ColorLine": { + "Extend": int(ot.ExtendMode.PAD), + "ColorStop": [ + {"StopOffset": 0.1, "PaletteIndex": 0, "Alpha": 1.0}, + {"StopOffset": 0.9, "PaletteIndex": 1, "Alpha": 1.0}, + ], + }, + "x0": 0, + "y0": 0, + "x1": 1, + "y1": 1, + "x2": 2, + "y2": 2, + }, + ], + [ + '<Paint Format="5"><!-- PaintVarLinearGradient -->', + " <ColorLine>", + ' <Extend value="pad"/>', + " <!-- StopCount=2 -->", + ' <ColorStop index="0">', + ' <StopOffset value="0.0"/>', + ' <PaletteIndex value="0"/>', + ' <Alpha value="1.0"/>', + ' <VarIndexBase value="0"/>', + " </ColorStop>", + ' <ColorStop index="1">', + ' <StopOffset value="1.0"/>', + ' <PaletteIndex value="1"/>', + ' <Alpha value="1.0"/>', + ' <VarIndexBase value="2"/>', + " </ColorStop>", + " </ColorLine>", + ' <x0 value="0"/>', + ' <y0 value="0"/>', + ' <x1 value="1"/>', + ' <y1 value="1"/>', + ' <x2 value="2"/>', + ' <y2 value="2"/>', + " <VarIndexBase/>", + "</Paint>", + ], + [0, NO_VARIATION_INDEX, 1, NO_VARIATION_INDEX], + id="linear_grad-stop-offsets", + ), + pytest.param( + [ + { + "Format": int(ot.PaintFormat.PaintLinearGradient), + "ColorLine": { + "Extend": int(ot.ExtendMode.PAD), + "ColorStop": [ + {"StopOffset": 0.0, "PaletteIndex": 0, "Alpha": 1.0}, + {"StopOffset": 1.0, "PaletteIndex": 1, "Alpha": 1.0}, + ], + }, + "x0": 0, + "y0": 0, + "x1": 1, + "y1": 1, + "x2": 2, + "y2": 2, + }, + { + "Format": int(ot.PaintFormat.PaintLinearGradient), + "ColorLine": { + "Extend": int(ot.ExtendMode.PAD), + "ColorStop": [ + {"StopOffset": 0.0, "PaletteIndex": 0, "Alpha": 0.5}, + {"StopOffset": 1.0, "PaletteIndex": 1, "Alpha": 1.0}, + ], + }, + "x0": 0, + "y0": 0, + "x1": 1, + "y1": 1, + "x2": 2, + "y2": 2, + }, + ], + [ + '<Paint Format="5"><!-- PaintVarLinearGradient -->', + " <ColorLine>", + ' <Extend value="pad"/>', + " <!-- StopCount=2 -->", + ' <ColorStop index="0">', + ' <StopOffset value="0.0"/>', + ' <PaletteIndex value="0"/>', + ' <Alpha value="1.0"/>', + ' <VarIndexBase value="0"/>', + " </ColorStop>", + ' <ColorStop index="1">', + ' <StopOffset value="1.0"/>', + ' <PaletteIndex value="1"/>', + ' <Alpha value="1.0"/>', + " <VarIndexBase/>", + " </ColorStop>", + " </ColorLine>", + ' <x0 value="0"/>', + ' <y0 value="0"/>', + ' <x1 value="1"/>', + ' <y1 value="1"/>', + ' <x2 value="2"/>', + ' <y2 value="2"/>', + " <VarIndexBase/>", + "</Paint>", + ], + [NO_VARIATION_INDEX, 0], + id="linear_grad-stop[0].alpha", + ), + pytest.param( + [ + { + "Format": int(ot.PaintFormat.PaintLinearGradient), + "ColorLine": { + "Extend": int(ot.ExtendMode.PAD), + "ColorStop": [ + {"StopOffset": 0.0, "PaletteIndex": 0, "Alpha": 1.0}, + {"StopOffset": 1.0, "PaletteIndex": 1, "Alpha": 1.0}, + ], + }, + "x0": 0, + "y0": 0, + "x1": 1, + "y1": 1, + "x2": 2, + "y2": 2, + }, + { + "Format": int(ot.PaintFormat.PaintLinearGradient), + "ColorLine": { + "Extend": int(ot.ExtendMode.PAD), + "ColorStop": [ + {"StopOffset": -0.5, "PaletteIndex": 0, "Alpha": 1.0}, + {"StopOffset": 1.0, "PaletteIndex": 1, "Alpha": 1.0}, + ], + }, + "x0": 0, + "y0": 0, + "x1": 1, + "y1": 1, + "x2": 2, + "y2": -200, + }, + ], + [ + '<Paint Format="5"><!-- PaintVarLinearGradient -->', + " <ColorLine>", + ' <Extend value="pad"/>', + " <!-- StopCount=2 -->", + ' <ColorStop index="0">', + ' <StopOffset value="0.0"/>', + ' <PaletteIndex value="0"/>', + ' <Alpha value="1.0"/>', + ' <VarIndexBase value="0"/>', + " </ColorStop>", + ' <ColorStop index="1">', + ' <StopOffset value="1.0"/>', + ' <PaletteIndex value="1"/>', + ' <Alpha value="1.0"/>', + " <VarIndexBase/>", + " </ColorStop>", + " </ColorLine>", + ' <x0 value="0"/>', + ' <y0 value="0"/>', + ' <x1 value="1"/>', + ' <y1 value="1"/>', + ' <x2 value="2"/>', + ' <y2 value="2"/>', + ' <VarIndexBase value="1"/>', + "</Paint>", + ], + [ + 0, + NO_VARIATION_INDEX, + NO_VARIATION_INDEX, + NO_VARIATION_INDEX, + NO_VARIATION_INDEX, + NO_VARIATION_INDEX, + 1, + ], + id="linear_grad-stop[0].offset-y2", + ), + pytest.param( + [ + { + "Format": int(ot.PaintFormat.PaintRadialGradient), + "ColorLine": { + "Extend": int(ot.ExtendMode.PAD), + "ColorStop": [ + {"StopOffset": 0.0, "PaletteIndex": 0, "Alpha": 1.0}, + {"StopOffset": 1.0, "PaletteIndex": 1, "Alpha": 1.0}, + ], + }, + "x0": 0, + "y0": 0, + "r0": 0, + "x1": 1, + "y1": 1, + "r1": 1, + }, + { + "Format": int(ot.PaintFormat.PaintRadialGradient), + "ColorLine": { + "Extend": int(ot.ExtendMode.PAD), + "ColorStop": [ + {"StopOffset": 0.1, "PaletteIndex": 0, "Alpha": 0.6}, + {"StopOffset": 0.9, "PaletteIndex": 1, "Alpha": 0.7}, + ], + }, + "x0": -1, + "y0": -2, + "r0": 3, + "x1": -4, + "y1": -5, + "r1": 6, + }, + ], + [ + '<Paint Format="7"><!-- PaintVarRadialGradient -->', + " <ColorLine>", + ' <Extend value="pad"/>', + " <!-- StopCount=2 -->", + ' <ColorStop index="0">', + ' <StopOffset value="0.0"/>', + ' <PaletteIndex value="0"/>', + ' <Alpha value="1.0"/>', + ' <VarIndexBase value="0"/>', + " </ColorStop>", + ' <ColorStop index="1">', + ' <StopOffset value="1.0"/>', + ' <PaletteIndex value="1"/>', + ' <Alpha value="1.0"/>', + ' <VarIndexBase value="2"/>', + " </ColorStop>", + " </ColorLine>", + ' <x0 value="0"/>', + ' <y0 value="0"/>', + ' <r0 value="0"/>', + ' <x1 value="1"/>', + ' <y1 value="1"/>', + ' <r1 value="1"/>', + ' <VarIndexBase value="4"/>', + "</Paint>", + ], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + id="radial_grad-all-different", + ), + pytest.param( + [ + { + "Format": int(ot.PaintFormat.PaintSweepGradient), + "ColorLine": { + "Extend": int(ot.ExtendMode.REPEAT), + "ColorStop": [ + {"StopOffset": 0.4, "PaletteIndex": 0, "Alpha": 1.0}, + {"StopOffset": 0.6, "PaletteIndex": 1, "Alpha": 1.0}, + ], + }, + "centerX": 0, + "centerY": 0, + "startAngle": 0, + "endAngle": 180.0, + }, + { + "Format": int(ot.PaintFormat.PaintSweepGradient), + "ColorLine": { + "Extend": int(ot.ExtendMode.REPEAT), + "ColorStop": [ + {"StopOffset": 0.4, "PaletteIndex": 0, "Alpha": 1.0}, + {"StopOffset": 0.6, "PaletteIndex": 1, "Alpha": 1.0}, + ], + }, + "centerX": 0, + "centerY": 0, + "startAngle": 90.0, + "endAngle": 180.0, + }, + ], + [ + '<Paint Format="9"><!-- PaintVarSweepGradient -->', + " <ColorLine>", + ' <Extend value="repeat"/>', + " <!-- StopCount=2 -->", + ' <ColorStop index="0">', + ' <StopOffset value="0.4"/>', + ' <PaletteIndex value="0"/>', + ' <Alpha value="1.0"/>', + " <VarIndexBase/>", + " </ColorStop>", + ' <ColorStop index="1">', + ' <StopOffset value="0.6"/>', + ' <PaletteIndex value="1"/>', + ' <Alpha value="1.0"/>', + " <VarIndexBase/>", + " </ColorStop>", + " </ColorLine>", + ' <centerX value="0"/>', + ' <centerY value="0"/>', + ' <startAngle value="0.0"/>', + ' <endAngle value="180.0"/>', + ' <VarIndexBase value="0"/>', + "</Paint>", + ], + [NO_VARIATION_INDEX, NO_VARIATION_INDEX, 0, NO_VARIATION_INDEX], + id="sweep_grad-startAngle", + ), + pytest.param( + [ + { + "Format": int(ot.PaintFormat.PaintSweepGradient), + "ColorLine": { + "Extend": int(ot.ExtendMode.PAD), + "ColorStop": [ + {"StopOffset": 0.0, "PaletteIndex": 0, "Alpha": 1.0}, + {"StopOffset": 1.0, "PaletteIndex": 1, "Alpha": 1.0}, + ], + }, + "centerX": 0, + "centerY": 0, + "startAngle": 0.0, + "endAngle": 180.0, + }, + { + "Format": int(ot.PaintFormat.PaintSweepGradient), + "ColorLine": { + "Extend": int(ot.ExtendMode.PAD), + "ColorStop": [ + {"StopOffset": 0.0, "PaletteIndex": 0, "Alpha": 0.5}, + {"StopOffset": 1.0, "PaletteIndex": 1, "Alpha": 0.5}, + ], + }, + "centerX": 0, + "centerY": 0, + "startAngle": 0.0, + "endAngle": 180.0, + }, + ], + [ + '<Paint Format="9"><!-- PaintVarSweepGradient -->', + " <ColorLine>", + ' <Extend value="pad"/>', + " <!-- StopCount=2 -->", + ' <ColorStop index="0">', + ' <StopOffset value="0.0"/>', + ' <PaletteIndex value="0"/>', + ' <Alpha value="1.0"/>', + ' <VarIndexBase value="0"/>', + " </ColorStop>", + ' <ColorStop index="1">', + ' <StopOffset value="1.0"/>', + ' <PaletteIndex value="1"/>', + ' <Alpha value="1.0"/>', + ' <VarIndexBase value="0"/>', + " </ColorStop>", + " </ColorLine>", + ' <centerX value="0"/>', + ' <centerY value="0"/>', + ' <startAngle value="0.0"/>', + ' <endAngle value="180.0"/>', + " <VarIndexBase/>", + "</Paint>", + ], + [NO_VARIATION_INDEX, 0], + id="sweep_grad-stops-alpha-reuse-varidxbase", + ), + pytest.param( + [ + { + "Format": int(ot.PaintFormat.PaintTransform), + "Paint": { + "Format": int(ot.PaintFormat.PaintRadialGradient), + "ColorLine": { + "Extend": int(ot.ExtendMode.PAD), + "ColorStop": [ + { + "StopOffset": 0.0, + "PaletteIndex": 0, + "Alpha": 1.0, + }, + { + "StopOffset": 1.0, + "PaletteIndex": 1, + "Alpha": 1.0, + }, + ], + }, + "x0": 0, + "y0": 0, + "r0": 0, + "x1": 1, + "y1": 1, + "r1": 1, + }, + "Transform": { + "xx": 1.0, + "xy": 0.0, + "yx": 0.0, + "yy": 1.0, + "dx": 0.0, + "dy": 0.0, + }, + }, + { + "Format": int(ot.PaintFormat.PaintTransform), + "Paint": { + "Format": int(ot.PaintFormat.PaintRadialGradient), + "ColorLine": { + "Extend": int(ot.ExtendMode.PAD), + "ColorStop": [ + { + "StopOffset": 0.0, + "PaletteIndex": 0, + "Alpha": 1.0, + }, + { + "StopOffset": 1.0, + "PaletteIndex": 1, + "Alpha": 1.0, + }, + ], + }, + "x0": 0, + "y0": 0, + "r0": 0, + "x1": 1, + "y1": 1, + "r1": 1, + }, + "Transform": { + "xx": 1.0, + "xy": 0.0, + "yx": 0.0, + "yy": 0.5, + "dx": 0.0, + "dy": -100.0, + }, + }, + ], + [ + '<Paint Format="13"><!-- PaintVarTransform -->', + ' <Paint Format="6"><!-- PaintRadialGradient -->', + " <ColorLine>", + ' <Extend value="pad"/>', + " <!-- StopCount=2 -->", + ' <ColorStop index="0">', + ' <StopOffset value="0.0"/>', + ' <PaletteIndex value="0"/>', + ' <Alpha value="1.0"/>', + " </ColorStop>", + ' <ColorStop index="1">', + ' <StopOffset value="1.0"/>', + ' <PaletteIndex value="1"/>', + ' <Alpha value="1.0"/>', + " </ColorStop>", + " </ColorLine>", + ' <x0 value="0"/>', + ' <y0 value="0"/>', + ' <r0 value="0"/>', + ' <x1 value="1"/>', + ' <y1 value="1"/>', + ' <r1 value="1"/>', + " </Paint>", + " <Transform>", + ' <xx value="1.0"/>', + ' <yx value="0.0"/>', + ' <xy value="0.0"/>', + ' <yy value="1.0"/>', + ' <dx value="0.0"/>', + ' <dy value="0.0"/>', + ' <VarIndexBase value="0"/>', + " </Transform>", + "</Paint>", + ], + [ + NO_VARIATION_INDEX, + NO_VARIATION_INDEX, + NO_VARIATION_INDEX, + 0, + NO_VARIATION_INDEX, + 1, + ], + id="transform-yy-dy", + ), + pytest.param( + [ + { + "Format": ot.PaintFormat.PaintTransform, + "Paint": { + "Format": ot.PaintFormat.PaintSweepGradient, + "ColorLine": { + "Extend": ot.ExtendMode.PAD, + "ColorStop": [ + {"StopOffset": 0.0, "PaletteIndex": 0}, + { + "StopOffset": 1.0, + "PaletteIndex": 1, + "Alpha": 1.0, + }, + ], + }, + "centerX": 0, + "centerY": 0, + "startAngle": 0, + "endAngle": 360, + }, + "Transform": (1.0, 0, 0, 1.0, 0, 0), + }, + { + "Format": ot.PaintFormat.PaintTransform, + "Paint": { + "Format": ot.PaintFormat.PaintSweepGradient, + "ColorLine": { + "Extend": ot.ExtendMode.PAD, + "ColorStop": [ + {"StopOffset": 0.0, "PaletteIndex": 0}, + { + "StopOffset": 1.0, + "PaletteIndex": 1, + "Alpha": 1.0, + }, + ], + }, + "centerX": 256, + "centerY": 0, + "startAngle": 0, + "endAngle": 360, + }, + # Transform.xx below produces the same VarStore delta as the + # above PaintSweepGradient's centerX because, when Fixed16.16 + # is converted to integer, it becomes: + # floatToFixed(1.00390625, 16) == 256 + # Because there is overlap between the varIdxes of the + # PaintVarTransform's Affine2x3 and the PaintSweepGradient's + # the VarIndexBase is reused (0 for both) + "Transform": (1.00390625, 0, 0, 1.0, 10, 0), + }, + ], + [ + '<Paint Format="13"><!-- PaintVarTransform -->', + ' <Paint Format="9"><!-- PaintVarSweepGradient -->', + " <ColorLine>", + ' <Extend value="pad"/>', + " <!-- StopCount=2 -->", + ' <ColorStop index="0">', + ' <StopOffset value="0.0"/>', + ' <PaletteIndex value="0"/>', + ' <Alpha value="1.0"/>', + " <VarIndexBase/>", + " </ColorStop>", + ' <ColorStop index="1">', + ' <StopOffset value="1.0"/>', + ' <PaletteIndex value="1"/>', + ' <Alpha value="1.0"/>', + " <VarIndexBase/>", + " </ColorStop>", + " </ColorLine>", + ' <centerX value="0"/>', + ' <centerY value="0"/>', + ' <startAngle value="0.0"/>', + ' <endAngle value="360.0"/>', + ' <VarIndexBase value="0"/>', + " </Paint>", + " <Transform>", + ' <xx value="1.0"/>', + ' <yx value="0.0"/>', + ' <xy value="0.0"/>', + ' <yy value="1.0"/>', + ' <dx value="0.0"/>', + ' <dy value="0.0"/>', + ' <VarIndexBase value="0"/>', + " </Transform>", + "</Paint>", + ], + [ + 0, + NO_VARIATION_INDEX, + NO_VARIATION_INDEX, + NO_VARIATION_INDEX, + 1, + NO_VARIATION_INDEX, + ], + id="transform-xx-sweep_grad-centerx-same-varidxbase", + ), + ], + ) + def test_merge_Paint(self, paints, ttFont, expected_xml, expected_varIdxes): + paints = [build_paint(p) for p in paints] + out = deepcopy(paints[0]) + + model = VariationModel([{}, {"ZZZZ": 1.0}]) + merger = COLRVariationMerger(model, ["ZZZZ"], ttFont) + + merger.mergeThings(out, paints) + + assert compile_decompile(out, ttFont) == out + assert dump_xml(out, ttFont) == expected_xml + assert merger.varIdxes == expected_varIdxes + + def test_merge_ClipList(self, ttFont): + clipLists = [ + buildClipList(clips) + for clips in [ + { + "A": (0, 0, 1000, 1000), + "B": (0, 0, 1000, 1000), + "C": (0, 0, 1000, 1000), + "D": (0, 0, 1000, 1000), + }, + { + # non-default masters' clip boxes can be 'sparse' + # (i.e. can omit explicit clip box for some glyphs) + # "A": (0, 0, 1000, 1000), + "B": (10, 0, 1000, 1000), + "C": (20, 20, 1020, 1020), + "D": (20, 20, 1020, 1020), + }, + ] + ] + out = deepcopy(clipLists[0]) + + model = VariationModel([{}, {"ZZZZ": 1.0}]) + merger = COLRVariationMerger(model, ["ZZZZ"], ttFont) + + merger.mergeThings(out, clipLists) + + assert compile_decompile(out, ttFont) == out + assert dump_xml(out, ttFont) == [ + '<ClipList Format="1">', + " <Clip>", + ' <Glyph value="A"/>', + ' <ClipBox Format="1">', + ' <xMin value="0"/>', + ' <yMin value="0"/>', + ' <xMax value="1000"/>', + ' <yMax value="1000"/>', + " </ClipBox>", + " </Clip>", + " <Clip>", + ' <Glyph value="B"/>', + ' <ClipBox Format="2">', + ' <xMin value="0"/>', + ' <yMin value="0"/>', + ' <xMax value="1000"/>', + ' <yMax value="1000"/>', + ' <VarIndexBase value="0"/>', + " </ClipBox>", + " </Clip>", + " <Clip>", + ' <Glyph value="C"/>', + ' <Glyph value="D"/>', + ' <ClipBox Format="2">', + ' <xMin value="0"/>', + ' <yMin value="0"/>', + ' <xMax value="1000"/>', + ' <yMax value="1000"/>', + ' <VarIndexBase value="4"/>', + " </ClipBox>", + " </Clip>", + "</ClipList>", + ] + assert merger.varIdxes == [ + 0, + NO_VARIATION_INDEX, + NO_VARIATION_INDEX, + NO_VARIATION_INDEX, + 1, + 1, + 1, + 1, + ] + + @pytest.mark.parametrize( + "master_layer_reuse", + [ + pytest.param(False, id="no-reuse"), + pytest.param(True, id="with-reuse"), + ], + ) + @pytest.mark.parametrize( + "color_glyphs, output_layer_reuse, expected_xml, expected_varIdxes", + [ + pytest.param( + [ + { + "A": { + "Format": int(ot.PaintFormat.PaintColrLayers), + "Layers": [ + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 0, + "Alpha": 1.0, + }, + "Glyph": "B", + }, + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 1, + "Alpha": 1.0, + }, + "Glyph": "B", + }, + ], + }, + }, + { + "A": { + "Format": ot.PaintFormat.PaintColrLayers, + "Layers": [ + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 0, + "Alpha": 1.0, + }, + "Glyph": "B", + }, + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 1, + "Alpha": 1.0, + }, + "Glyph": "B", + }, + ], + }, + }, + ], + False, + [ + "<COLR>", + ' <Version value="1"/>', + " <!-- BaseGlyphRecordCount=0 -->", + " <!-- LayerRecordCount=0 -->", + " <BaseGlyphList>", + " <!-- BaseGlyphCount=1 -->", + ' <BaseGlyphPaintRecord index="0">', + ' <BaseGlyph value="A"/>', + ' <Paint Format="1"><!-- PaintColrLayers -->', + ' <NumLayers value="2"/>', + ' <FirstLayerIndex value="0"/>', + " </Paint>", + " </BaseGlyphPaintRecord>", + " </BaseGlyphList>", + " <LayerList>", + " <!-- LayerCount=2 -->", + ' <Paint index="0" Format="10"><!-- PaintGlyph -->', + ' <Paint Format="2"><!-- PaintSolid -->', + ' <PaletteIndex value="0"/>', + ' <Alpha value="1.0"/>', + " </Paint>", + ' <Glyph value="B"/>', + " </Paint>", + ' <Paint index="1" Format="10"><!-- PaintGlyph -->', + ' <Paint Format="2"><!-- PaintSolid -->', + ' <PaletteIndex value="1"/>', + ' <Alpha value="1.0"/>', + " </Paint>", + ' <Glyph value="B"/>', + " </Paint>", + " </LayerList>", + "</COLR>", + ], + [], + id="no-variation", + ), + pytest.param( + [ + { + "A": { + "Format": int(ot.PaintFormat.PaintColrLayers), + "Layers": [ + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 0, + "Alpha": 1.0, + }, + "Glyph": "B", + }, + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 1, + "Alpha": 1.0, + }, + "Glyph": "B", + }, + ], + }, + "C": { + "Format": int(ot.PaintFormat.PaintColrLayers), + "Layers": [ + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 2, + "Alpha": 1.0, + }, + "Glyph": "B", + }, + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 3, + "Alpha": 1.0, + }, + "Glyph": "B", + }, + ], + }, + }, + { + # NOTE: 'A' is missing from non-default master + "C": { + "Format": int(ot.PaintFormat.PaintColrLayers), + "Layers": [ + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 2, + "Alpha": 0.5, + }, + "Glyph": "B", + }, + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 3, + "Alpha": 0.5, + }, + "Glyph": "B", + }, + ], + }, + }, + ], + False, + [ + "<COLR>", + ' <Version value="1"/>', + " <!-- BaseGlyphRecordCount=0 -->", + " <!-- LayerRecordCount=0 -->", + " <BaseGlyphList>", + " <!-- BaseGlyphCount=2 -->", + ' <BaseGlyphPaintRecord index="0">', + ' <BaseGlyph value="A"/>', + ' <Paint Format="1"><!-- PaintColrLayers -->', + ' <NumLayers value="2"/>', + ' <FirstLayerIndex value="0"/>', + " </Paint>", + " </BaseGlyphPaintRecord>", + ' <BaseGlyphPaintRecord index="1">', + ' <BaseGlyph value="C"/>', + ' <Paint Format="1"><!-- PaintColrLayers -->', + ' <NumLayers value="2"/>', + ' <FirstLayerIndex value="2"/>', + " </Paint>", + " </BaseGlyphPaintRecord>", + " </BaseGlyphList>", + " <LayerList>", + " <!-- LayerCount=4 -->", + ' <Paint index="0" Format="10"><!-- PaintGlyph -->', + ' <Paint Format="2"><!-- PaintSolid -->', + ' <PaletteIndex value="0"/>', + ' <Alpha value="1.0"/>', + " </Paint>", + ' <Glyph value="B"/>', + " </Paint>", + ' <Paint index="1" Format="10"><!-- PaintGlyph -->', + ' <Paint Format="2"><!-- PaintSolid -->', + ' <PaletteIndex value="1"/>', + ' <Alpha value="1.0"/>', + " </Paint>", + ' <Glyph value="B"/>', + " </Paint>", + ' <Paint index="2" Format="10"><!-- PaintGlyph -->', + ' <Paint Format="3"><!-- PaintVarSolid -->', + ' <PaletteIndex value="2"/>', + ' <Alpha value="1.0"/>', + ' <VarIndexBase value="0"/>', + " </Paint>", + ' <Glyph value="B"/>', + " </Paint>", + ' <Paint index="3" Format="10"><!-- PaintGlyph -->', + ' <Paint Format="3"><!-- PaintVarSolid -->', + ' <PaletteIndex value="3"/>', + ' <Alpha value="1.0"/>', + ' <VarIndexBase value="0"/>', + " </Paint>", + ' <Glyph value="B"/>', + " </Paint>", + " </LayerList>", + "</COLR>", + ], + [0], + id="sparse-masters", + ), + pytest.param( + [ + { + "A": { + "Format": int(ot.PaintFormat.PaintColrLayers), + "Layers": [ + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 0, + "Alpha": 1.0, + }, + "Glyph": "B", + }, + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 1, + "Alpha": 1.0, + }, + "Glyph": "B", + }, + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 2, + "Alpha": 1.0, + }, + "Glyph": "B", + }, + ], + }, + "C": { + "Format": int(ot.PaintFormat.PaintColrLayers), + "Layers": [ + # 'C' reuses layers 1-3 from 'A' + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 1, + "Alpha": 1.0, + }, + "Glyph": "B", + }, + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 2, + "Alpha": 1.0, + }, + "Glyph": "B", + }, + ], + }, + "D": { # identical to 'C' + "Format": int(ot.PaintFormat.PaintColrLayers), + "Layers": [ + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 1, + "Alpha": 1.0, + }, + "Glyph": "B", + }, + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 2, + "Alpha": 1.0, + }, + "Glyph": "B", + }, + ], + }, + "E": { # superset of 'C' or 'D' + "Format": int(ot.PaintFormat.PaintColrLayers), + "Layers": [ + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 1, + "Alpha": 1.0, + }, + "Glyph": "B", + }, + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 2, + "Alpha": 1.0, + }, + "Glyph": "B", + }, + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 3, + "Alpha": 1.0, + }, + "Glyph": "B", + }, + ], + }, + }, + { + # NOTE: 'A' is missing from non-default master + "C": { + "Format": int(ot.PaintFormat.PaintColrLayers), + "Layers": [ + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 1, + "Alpha": 0.5, + }, + "Glyph": "B", + }, + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 2, + "Alpha": 0.5, + }, + "Glyph": "B", + }, + ], + }, + "D": { # same as 'C' + "Format": int(ot.PaintFormat.PaintColrLayers), + "Layers": [ + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 1, + "Alpha": 0.5, + }, + "Glyph": "B", + }, + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 2, + "Alpha": 0.5, + }, + "Glyph": "B", + }, + ], + }, + "E": { # first two layers vary the same way as 'C' or 'D' + "Format": int(ot.PaintFormat.PaintColrLayers), + "Layers": [ + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 1, + "Alpha": 0.5, + }, + "Glyph": "B", + }, + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 2, + "Alpha": 0.5, + }, + "Glyph": "B", + }, + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 3, + "Alpha": 1.0, + }, + "Glyph": "B", + }, + ], + }, + }, + ], + True, # reuse + [ + "<COLR>", + ' <Version value="1"/>', + " <!-- BaseGlyphRecordCount=0 -->", + " <!-- LayerRecordCount=0 -->", + " <BaseGlyphList>", + " <!-- BaseGlyphCount=4 -->", + ' <BaseGlyphPaintRecord index="0">', + ' <BaseGlyph value="A"/>', + ' <Paint Format="1"><!-- PaintColrLayers -->', + ' <NumLayers value="3"/>', + ' <FirstLayerIndex value="0"/>', + " </Paint>", + " </BaseGlyphPaintRecord>", + ' <BaseGlyphPaintRecord index="1">', + ' <BaseGlyph value="C"/>', + ' <Paint Format="1"><!-- PaintColrLayers -->', + ' <NumLayers value="2"/>', + ' <FirstLayerIndex value="3"/>', + " </Paint>", + " </BaseGlyphPaintRecord>", + ' <BaseGlyphPaintRecord index="2">', + ' <BaseGlyph value="D"/>', + ' <Paint Format="1"><!-- PaintColrLayers -->', + ' <NumLayers value="2"/>', + ' <FirstLayerIndex value="3"/>', + " </Paint>", + " </BaseGlyphPaintRecord>", + ' <BaseGlyphPaintRecord index="3">', + ' <BaseGlyph value="E"/>', + ' <Paint Format="1"><!-- PaintColrLayers -->', + ' <NumLayers value="2"/>', + ' <FirstLayerIndex value="5"/>', + " </Paint>", + " </BaseGlyphPaintRecord>", + " </BaseGlyphList>", + " <LayerList>", + " <!-- LayerCount=7 -->", + ' <Paint index="0" Format="10"><!-- PaintGlyph -->', + ' <Paint Format="2"><!-- PaintSolid -->', + ' <PaletteIndex value="0"/>', + ' <Alpha value="1.0"/>', + " </Paint>", + ' <Glyph value="B"/>', + " </Paint>", + ' <Paint index="1" Format="10"><!-- PaintGlyph -->', + ' <Paint Format="2"><!-- PaintSolid -->', + ' <PaletteIndex value="1"/>', + ' <Alpha value="1.0"/>', + " </Paint>", + ' <Glyph value="B"/>', + " </Paint>", + ' <Paint index="2" Format="10"><!-- PaintGlyph -->', + ' <Paint Format="2"><!-- PaintSolid -->', + ' <PaletteIndex value="2"/>', + ' <Alpha value="1.0"/>', + " </Paint>", + ' <Glyph value="B"/>', + " </Paint>", + ' <Paint index="3" Format="10"><!-- PaintGlyph -->', + ' <Paint Format="3"><!-- PaintVarSolid -->', + ' <PaletteIndex value="1"/>', + ' <Alpha value="1.0"/>', + ' <VarIndexBase value="0"/>', + " </Paint>", + ' <Glyph value="B"/>', + " </Paint>", + ' <Paint index="4" Format="10"><!-- PaintGlyph -->', + ' <Paint Format="3"><!-- PaintVarSolid -->', + ' <PaletteIndex value="2"/>', + ' <Alpha value="1.0"/>', + ' <VarIndexBase value="0"/>', + " </Paint>", + ' <Glyph value="B"/>', + " </Paint>", + ' <Paint index="5" Format="1"><!-- PaintColrLayers -->', + ' <NumLayers value="2"/>', + ' <FirstLayerIndex value="3"/>', + " </Paint>", + ' <Paint index="6" Format="10"><!-- PaintGlyph -->', + ' <Paint Format="2"><!-- PaintSolid -->', + ' <PaletteIndex value="3"/>', + ' <Alpha value="1.0"/>', + " </Paint>", + ' <Glyph value="B"/>', + " </Paint>", + " </LayerList>", + "</COLR>", + ], + [0], + id="sparse-masters-with-reuse", + ), + pytest.param( + [ + { + "A": { + "Format": int(ot.PaintFormat.PaintColrLayers), + "Layers": [ + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 0, + "Alpha": 1.0, + }, + "Glyph": "B", + }, + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 1, + "Alpha": 1.0, + }, + "Glyph": "B", + }, + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 2, + "Alpha": 1.0, + }, + "Glyph": "B", + }, + ], + }, + "C": { # 'C' shares layer 1 and 2 with 'A' + "Format": int(ot.PaintFormat.PaintColrLayers), + "Layers": [ + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 1, + "Alpha": 1.0, + }, + "Glyph": "B", + }, + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 2, + "Alpha": 1.0, + }, + "Glyph": "B", + }, + ], + }, + }, + { + "A": { + "Format": int(ot.PaintFormat.PaintColrLayers), + "Layers": [ + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 0, + "Alpha": 1.0, + }, + "Glyph": "B", + }, + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 1, + "Alpha": 0.9, + }, + "Glyph": "B", + }, + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 2, + "Alpha": 1.0, + }, + "Glyph": "B", + }, + ], + }, + "C": { + "Format": int(ot.PaintFormat.PaintColrLayers), + "Layers": [ + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 1, + "Alpha": 0.5, + }, + "Glyph": "B", + }, + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 2, + "Alpha": 1.0, + }, + "Glyph": "B", + }, + ], + }, + }, + ], + True, + [ + # a different Alpha variation is applied to a shared layer between + # 'A' and 'C' and thus they are no longer shared. + "<COLR>", + ' <Version value="1"/>', + " <!-- BaseGlyphRecordCount=0 -->", + " <!-- LayerRecordCount=0 -->", + " <BaseGlyphList>", + " <!-- BaseGlyphCount=2 -->", + ' <BaseGlyphPaintRecord index="0">', + ' <BaseGlyph value="A"/>', + ' <Paint Format="1"><!-- PaintColrLayers -->', + ' <NumLayers value="3"/>', + ' <FirstLayerIndex value="0"/>', + " </Paint>", + " </BaseGlyphPaintRecord>", + ' <BaseGlyphPaintRecord index="1">', + ' <BaseGlyph value="C"/>', + ' <Paint Format="1"><!-- PaintColrLayers -->', + ' <NumLayers value="2"/>', + ' <FirstLayerIndex value="3"/>', + " </Paint>", + " </BaseGlyphPaintRecord>", + " </BaseGlyphList>", + " <LayerList>", + " <!-- LayerCount=5 -->", + ' <Paint index="0" Format="10"><!-- PaintGlyph -->', + ' <Paint Format="2"><!-- PaintSolid -->', + ' <PaletteIndex value="0"/>', + ' <Alpha value="1.0"/>', + " </Paint>", + ' <Glyph value="B"/>', + " </Paint>", + ' <Paint index="1" Format="10"><!-- PaintGlyph -->', + ' <Paint Format="3"><!-- PaintVarSolid -->', + ' <PaletteIndex value="1"/>', + ' <Alpha value="1.0"/>', + ' <VarIndexBase value="0"/>', + " </Paint>", + ' <Glyph value="B"/>', + " </Paint>", + ' <Paint index="2" Format="10"><!-- PaintGlyph -->', + ' <Paint Format="2"><!-- PaintSolid -->', + ' <PaletteIndex value="2"/>', + ' <Alpha value="1.0"/>', + " </Paint>", + ' <Glyph value="B"/>', + " </Paint>", + ' <Paint index="3" Format="10"><!-- PaintGlyph -->', + ' <Paint Format="3"><!-- PaintVarSolid -->', + ' <PaletteIndex value="1"/>', + ' <Alpha value="1.0"/>', + ' <VarIndexBase value="1"/>', + " </Paint>", + ' <Glyph value="B"/>', + " </Paint>", + ' <Paint index="4" Format="10"><!-- PaintGlyph -->', + ' <Paint Format="2"><!-- PaintSolid -->', + ' <PaletteIndex value="2"/>', + ' <Alpha value="1.0"/>', + " </Paint>", + ' <Glyph value="B"/>', + " </Paint>", + " </LayerList>", + "</COLR>", + ], + [0, 1], + id="shared-master-layers-different-variations", + ), + ], + ) + def test_merge_full_table( + self, + color_glyphs, + ttFont, + expected_xml, + expected_varIdxes, + master_layer_reuse, + output_layer_reuse, + ): + master_ttfs = [deepcopy(ttFont) for _ in range(len(color_glyphs))] + for ttf, glyphs in zip(master_ttfs, color_glyphs): + # merge algorithm is expected to work the same even if the master COLRs + # may differ as to the layer reuse, hence we try both ways + ttf["COLR"] = buildCOLR(glyphs, allowLayerReuse=master_layer_reuse) + vf = deepcopy(master_ttfs[0]) + + model = VariationModel([{}, {"ZZZZ": 1.0}]) + merger = COLRVariationMerger( + model, ["ZZZZ"], vf, allowLayerReuse=output_layer_reuse + ) + + merger.mergeTables(vf, master_ttfs) + + out = vf["COLR"].table + + assert compile_decompile(out, vf) == out + assert dump_xml(out, vf) == expected_xml + assert merger.varIdxes == expected_varIdxes + + @pytest.mark.parametrize( + "color_glyphs, before_xml, expected_xml", + [ + pytest.param( + { + "A": { + "Format": int(ot.PaintFormat.PaintColrLayers), + "Layers": [ + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 0, + "Alpha": 1.0, + }, + "Glyph": "B", + }, + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 1, + "Alpha": 1.0, + }, + "Glyph": "C", + }, + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 2, + "Alpha": 1.0, + }, + "Glyph": "D", + }, + ], + }, + "E": { + "Format": int(ot.PaintFormat.PaintColrLayers), + "Layers": [ + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 1, + "Alpha": 1.0, + }, + "Glyph": "C", + }, + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 2, + "Alpha": 1.0, + }, + "Glyph": "D", + }, + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 3, + "Alpha": 1.0, + }, + "Glyph": "F", + }, + ], + }, + "G": { + "Format": int(ot.PaintFormat.PaintColrGlyph), + "Glyph": "E", + }, + }, + [ + "<COLR>", + ' <Version value="1"/>', + " <!-- BaseGlyphRecordCount=0 -->", + " <!-- LayerRecordCount=0 -->", + " <BaseGlyphList>", + " <!-- BaseGlyphCount=3 -->", + ' <BaseGlyphPaintRecord index="0">', + ' <BaseGlyph value="A"/>', + ' <Paint Format="1"><!-- PaintColrLayers -->', + ' <NumLayers value="3"/>', + ' <FirstLayerIndex value="0"/>', + " </Paint>", + " </BaseGlyphPaintRecord>", + ' <BaseGlyphPaintRecord index="1">', + ' <BaseGlyph value="E"/>', + ' <Paint Format="1"><!-- PaintColrLayers -->', + ' <NumLayers value="2"/>', + ' <FirstLayerIndex value="3"/>', + " </Paint>", + " </BaseGlyphPaintRecord>", + ' <BaseGlyphPaintRecord index="2">', + ' <BaseGlyph value="G"/>', + ' <Paint Format="11"><!-- PaintColrGlyph -->', + ' <Glyph value="E"/>', + " </Paint>", + " </BaseGlyphPaintRecord>", + " </BaseGlyphList>", + " <LayerList>", + " <!-- LayerCount=5 -->", + ' <Paint index="0" Format="10"><!-- PaintGlyph -->', + ' <Paint Format="2"><!-- PaintSolid -->', + ' <PaletteIndex value="0"/>', + ' <Alpha value="1.0"/>', + " </Paint>", + ' <Glyph value="B"/>', + " </Paint>", + ' <Paint index="1" Format="10"><!-- PaintGlyph -->', + ' <Paint Format="2"><!-- PaintSolid -->', + ' <PaletteIndex value="1"/>', + ' <Alpha value="1.0"/>', + " </Paint>", + ' <Glyph value="C"/>', + " </Paint>", + ' <Paint index="2" Format="10"><!-- PaintGlyph -->', + ' <Paint Format="2"><!-- PaintSolid -->', + ' <PaletteIndex value="2"/>', + ' <Alpha value="1.0"/>', + " </Paint>", + ' <Glyph value="D"/>', + " </Paint>", + ' <Paint index="3" Format="1"><!-- PaintColrLayers -->', + ' <NumLayers value="2"/>', + ' <FirstLayerIndex value="1"/>', + " </Paint>", + ' <Paint index="4" Format="10"><!-- PaintGlyph -->', + ' <Paint Format="2"><!-- PaintSolid -->', + ' <PaletteIndex value="3"/>', + ' <Alpha value="1.0"/>', + " </Paint>", + ' <Glyph value="F"/>', + " </Paint>", + " </LayerList>", + "</COLR>", + ], + [ + "<COLR>", + ' <Version value="1"/>', + " <!-- BaseGlyphRecordCount=0 -->", + " <!-- LayerRecordCount=0 -->", + " <BaseGlyphList>", + " <!-- BaseGlyphCount=3 -->", + ' <BaseGlyphPaintRecord index="0">', + ' <BaseGlyph value="A"/>', + ' <Paint Format="1"><!-- PaintColrLayers -->', + ' <NumLayers value="3"/>', + ' <FirstLayerIndex value="0"/>', + " </Paint>", + " </BaseGlyphPaintRecord>", + ' <BaseGlyphPaintRecord index="1">', + ' <BaseGlyph value="E"/>', + ' <Paint Format="1"><!-- PaintColrLayers -->', + ' <NumLayers value="3"/>', + ' <FirstLayerIndex value="3"/>', + " </Paint>", + " </BaseGlyphPaintRecord>", + ' <BaseGlyphPaintRecord index="2">', + ' <BaseGlyph value="G"/>', + ' <Paint Format="11"><!-- PaintColrGlyph -->', + ' <Glyph value="E"/>', + " </Paint>", + " </BaseGlyphPaintRecord>", + " </BaseGlyphList>", + " <LayerList>", + " <!-- LayerCount=6 -->", + ' <Paint index="0" Format="10"><!-- PaintGlyph -->', + ' <Paint Format="2"><!-- PaintSolid -->', + ' <PaletteIndex value="0"/>', + ' <Alpha value="1.0"/>', + " </Paint>", + ' <Glyph value="B"/>', + " </Paint>", + ' <Paint index="1" Format="10"><!-- PaintGlyph -->', + ' <Paint Format="2"><!-- PaintSolid -->', + ' <PaletteIndex value="1"/>', + ' <Alpha value="1.0"/>', + " </Paint>", + ' <Glyph value="C"/>', + " </Paint>", + ' <Paint index="2" Format="10"><!-- PaintGlyph -->', + ' <Paint Format="2"><!-- PaintSolid -->', + ' <PaletteIndex value="2"/>', + ' <Alpha value="1.0"/>', + " </Paint>", + ' <Glyph value="D"/>', + " </Paint>", + ' <Paint index="3" Format="10"><!-- PaintGlyph -->', + ' <Paint Format="2"><!-- PaintSolid -->', + ' <PaletteIndex value="1"/>', + ' <Alpha value="1.0"/>', + " </Paint>", + ' <Glyph value="C"/>', + " </Paint>", + ' <Paint index="4" Format="10"><!-- PaintGlyph -->', + ' <Paint Format="2"><!-- PaintSolid -->', + ' <PaletteIndex value="2"/>', + ' <Alpha value="1.0"/>', + " </Paint>", + ' <Glyph value="D"/>', + " </Paint>", + ' <Paint index="5" Format="10"><!-- PaintGlyph -->', + ' <Paint Format="2"><!-- PaintSolid -->', + ' <PaletteIndex value="3"/>', + ' <Alpha value="1.0"/>', + " </Paint>", + ' <Glyph value="F"/>', + " </Paint>", + " </LayerList>", + "</COLR>", + ], + id="simple-reuse", + ), + pytest.param( + { + "A": { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "PaletteIndex": 0, + "Alpha": 1.0, + }, + "Glyph": "B", + }, + }, + [ + "<COLR>", + ' <Version value="1"/>', + " <!-- BaseGlyphRecordCount=0 -->", + " <!-- LayerRecordCount=0 -->", + " <BaseGlyphList>", + " <!-- BaseGlyphCount=1 -->", + ' <BaseGlyphPaintRecord index="0">', + ' <BaseGlyph value="A"/>', + ' <Paint Format="10"><!-- PaintGlyph -->', + ' <Paint Format="2"><!-- PaintSolid -->', + ' <PaletteIndex value="0"/>', + ' <Alpha value="1.0"/>', + " </Paint>", + ' <Glyph value="B"/>', + " </Paint>", + " </BaseGlyphPaintRecord>", + " </BaseGlyphList>", + "</COLR>", + ], + [ + "<COLR>", + ' <Version value="1"/>', + " <!-- BaseGlyphRecordCount=0 -->", + " <!-- LayerRecordCount=0 -->", + " <BaseGlyphList>", + " <!-- BaseGlyphCount=1 -->", + ' <BaseGlyphPaintRecord index="0">', + ' <BaseGlyph value="A"/>', + ' <Paint Format="10"><!-- PaintGlyph -->', + ' <Paint Format="2"><!-- PaintSolid -->', + ' <PaletteIndex value="0"/>', + ' <Alpha value="1.0"/>', + " </Paint>", + ' <Glyph value="B"/>', + " </Paint>", + " </BaseGlyphPaintRecord>", + " </BaseGlyphList>", + "</COLR>", + ], + id="no-layer-list", + ), + ], + ) + def test_expandPaintColrLayers( + self, color_glyphs, ttFont, before_xml, expected_xml + ): + colr = buildCOLR(color_glyphs, allowLayerReuse=True) + + assert dump_xml(colr.table, ttFont) == before_xml + + before_layer_count = 0 + reuses_colr_layers = False + if colr.table.LayerList: + before_layer_count = len(colr.table.LayerList.Paint) + reuses_colr_layers = any( + p.Format == ot.PaintFormat.PaintColrLayers + for p in colr.table.LayerList.Paint + ) + + COLRVariationMerger.expandPaintColrLayers(colr.table) + + assert dump_xml(colr.table, ttFont) == expected_xml + + after_layer_count = ( + 0 if not colr.table.LayerList else len(colr.table.LayerList.Paint) + ) + + if reuses_colr_layers: + assert not any( + p.Format == ot.PaintFormat.PaintColrLayers + for p in colr.table.LayerList.Paint + ) + assert after_layer_count > before_layer_count + else: + assert after_layer_count == before_layer_count + + if colr.table.LayerList: + assert len({id(p) for p in colr.table.LayerList.Paint}) == after_layer_count |