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, }, ], [ '', ' ', ' ', "", ], [], 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, }, ], [ '', ' ', ' ', ' ', "", ], [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, }, ], [ '', " ", ' ', " ", ' ', ' ', ' ', ' ', ' ', " ", ' ', ' ', ' ', ' ', ' ', " ", " ", ' ', ' ', ' ', ' ', ' ', ' ', " ", "", ], [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, }, ], [ '', " ", ' ', " ", ' ', ' ', ' ', ' ', ' ', " ", ' ', ' ', ' ', ' ', " ", " ", " ", ' ', ' ', ' ', ' ', ' ', ' ', " ", "", ], [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, }, ], [ '', " ", ' ', " ", ' ', ' ', ' ', ' ', ' ', " ", ' ', ' ', ' ', ' ', " ", " ", " ", ' ', ' ', ' ', ' ', ' ', ' ', ' ', "", ], [ 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, }, ], [ '', " ", ' ', " ", ' ', ' ', ' ', ' ', ' ', " ", ' ', ' ', ' ', ' ', ' ', " ", " ", ' ', ' ', ' ', ' ', ' ', ' ', ' ', "", ], [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, }, ], [ '', " ", ' ', " ", ' ', ' ', ' ', ' ', " ", " ", ' ', ' ', ' ', ' ', " ", " ", " ", ' ', ' ', ' ', ' ', ' ', "", ], [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, }, ], [ '', " ", ' ', " ", ' ', ' ', ' ', ' ', ' ', " ", ' ', ' ', ' ', ' ', ' ', " ", " ", ' ', ' ', ' ', ' ', " ", "", ], [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, }, }, ], [ '', ' ', " ", ' ', " ", ' ', ' ', ' ', ' ', " ", ' ', ' ', ' ', ' ', " ", " ", ' ', ' ', ' ', ' ', ' ', ' ', " ", " ", ' ', ' ', ' ', ' ', ' ', ' ', ' ', " ", "", ], [ 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), }, ], [ '', ' ', " ", ' ', " ", ' ', ' ', ' ', ' ', " ", " ", ' ', ' ', ' ', ' ', " ", " ", " ", ' ', ' ', ' ', ' ', ' ', " ", " ", ' ', ' ', ' ', ' ', ' ', ' ', ' ', " ", "", ], [ 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) == [ '', " ", ' ', ' ', ' ', ' ', ' ', ' ', " ", " ", " ", ' ', ' ', ' ', ' ', ' ', ' ', ' ', " ", " ", " ", ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', " ", " ", "", ] 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, [ "", ' ', " ", " ", " ", " ", ' ', ' ', ' ', ' ', ' ', " ", " ", " ", " ", " ", ' ', ' ', ' ', ' ', " ", ' ', " ", ' ', ' ', ' ', ' ', " ", ' ', " ", " ", "", ], [], 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, [ "", ' ', " ", " ", " ", " ", ' ', ' ', ' ', ' ', ' ', " ", " ", ' ', ' ', ' ', ' ', ' ', " ", " ", " ", " ", " ", ' ', ' ', ' ', ' ', " ", ' ', " ", ' ', ' ', ' ', ' ', " ", ' ', " ", ' ', ' ', ' ', ' ', ' ', " ", ' ', " ", ' ', ' ', ' ', ' ', ' ', " ", ' ', " ", " ", "", ], [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 [ "", ' ', " ", " ", " ", " ", ' ', ' ', ' ', ' ', ' ', " ", " ", ' ', ' ', ' ', ' ', ' ', " ", " ", ' ', ' ', ' ', ' ', ' ', " ", " ", ' ', ' ', ' ', ' ', ' ', " ", " ", " ", " ", " ", ' ', ' ', ' ', ' ', " ", ' ', " ", ' ', ' ', ' ', ' ', " ", ' ', " ", ' ', ' ', ' ', ' ', " ", ' ', " ", ' ', ' ', ' ', ' ', ' ', " ", ' ', " ", ' ', ' ', ' ', ' ', ' ', " ", ' ', " ", ' ', ' ', ' ', " ", ' ', ' ', ' ', ' ', " ", ' ', " ", " ", "", ], [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. "", ' ', " ", " ", " ", " ", ' ', ' ', ' ', ' ', ' ', " ", " ", ' ', ' ', ' ', ' ', ' ', " ", " ", " ", " ", " ", ' ', ' ', ' ', ' ', " ", ' ', " ", ' ', ' ', ' ', ' ', ' ', " ", ' ', " ", ' ', ' ', ' ', ' ', " ", ' ', " ", ' ', ' ', ' ', ' ', ' ', " ", ' ', " ", ' ', ' ', ' ', ' ', " ", ' ', " ", " ", "", ], [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", }, }, [ "", ' ', " ", " ", " ", " ", ' ', ' ', ' ', ' ', ' ', " ", " ", ' ', ' ', ' ', ' ', ' ', " ", " ", ' ', ' ', ' ', ' ', " ", " ", " ", " ", " ", ' ', ' ', ' ', ' ', " ", ' ', " ", ' ', ' ', ' ', ' ', " ", ' ', " ", ' ', ' ', ' ', ' ', " ", ' ', " ", ' ', ' ', ' ', " ", ' ', ' ', ' ', ' ', " ", ' ', " ", " ", "", ], [ "", ' ', " ", " ", " ", " ", ' ', ' ', ' ', ' ', ' ', " ", " ", ' ', ' ', ' ', ' ', ' ', " ", " ", ' ', ' ', ' ', ' ', " ", " ", " ", " ", " ", ' ', ' ', ' ', ' ', " ", ' ', " ", ' ', ' ', ' ', ' ', " ", ' ', " ", ' ', ' ', ' ', ' ', " ", ' ', " ", ' ', ' ', ' ', ' ', " ", ' ', " ", ' ', ' ', ' ', ' ', " ", ' ', " ", ' ', ' ', ' ', ' ', " ", ' ', " ", " ", "", ], id="simple-reuse", ), pytest.param( { "A": { "Format": int(ot.PaintFormat.PaintGlyph), "Paint": { "Format": int(ot.PaintFormat.PaintSolid), "PaletteIndex": 0, "Alpha": 1.0, }, "Glyph": "B", }, }, [ "", ' ', " ", " ", " ", " ", ' ', ' ', ' ', ' ', ' ', ' ', " ", ' ', " ", " ", " ", "", ], [ "", ' ', " ", " ", " ", " ", ' ', ' ', ' ', ' ', ' ', ' ', " ", ' ', " ", " ", " ", "", ], 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