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