from copy import deepcopy import string from fontTools.colorLib.builder import LayerListBuilder, 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") 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": -360, "endAngle": 0, }, "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": -360, "endAngle": 0, }, # 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, ]