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
from io import BytesIO
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
class SparsePositioningMergerTest:
def test_zero_kern_at_default(self):
# https://github.com/fonttools/fonttools/issues/3111
pytest.importorskip("ufo2ft")
pytest.importorskip("ufoLib2")
from fontTools.designspaceLib import DesignSpaceDocument
from ufo2ft import compileVariableTTF
from ufoLib2 import Font
ds = DesignSpaceDocument()
ds.addAxisDescriptor(
name="wght", tag="wght", minimum=100, maximum=900, default=400
)
ds.addSourceDescriptor(font=Font(), location=dict(wght=100))
ds.addSourceDescriptor(font=Font(), location=dict(wght=400))
ds.addSourceDescriptor(font=Font(), location=dict(wght=900))
ds.sources[0].font.newGlyph("a").unicode = ord("a")
ds.sources[0].font.newGlyph("b").unicode = ord("b")
ds.sources[0].font.features.text = "feature kern { pos a b b' 100; } kern;"
ds.sources[1].font.newGlyph("a").unicode = ord("a")
ds.sources[1].font.newGlyph("b").unicode = ord("b")
ds.sources[1].font.features.text = "feature kern { pos a b b' 0; } kern;"
ds.sources[2].font.newGlyph("a").unicode = ord("a")
ds.sources[2].font.newGlyph("b").unicode = ord("b")
ds.sources[2].font.features.text = "feature kern { pos a b b' -100; } kern;"
font = compileVariableTTF(ds, inplace=True)
b = BytesIO()
font.save(b)
assert font["GDEF"].table.VarStore.VarData[0].Item[0] == [100, -100]
def test_sparse_cursive(self):
# https://github.com/fonttools/fonttools/issues/3168
pytest.importorskip("ufo2ft")
pytest.importorskip("ufoLib2")
from fontTools.designspaceLib import DesignSpaceDocument
from ufo2ft import compileVariableTTF
from ufoLib2 import Font
ds = DesignSpaceDocument()
ds.addAxisDescriptor(
name="wght", tag="wght", minimum=100, maximum=900, default=400
)
ds.addSourceDescriptor(font=Font(), location=dict(wght=100))
ds.addSourceDescriptor(font=Font(), location=dict(wght=400))
ds.addSourceDescriptor(font=Font(), location=dict(wght=900))
ds.sources[0].font.newGlyph("a").unicode = ord("a")
ds.sources[0].font.newGlyph("b").unicode = ord("b")
ds.sources[0].font.newGlyph("c").unicode = ord("c")
ds.sources[
0
].font.features.text = """
feature curs {
position cursive a ;
position cursive c ;
} curs;
"""
ds.sources[1].font.newGlyph("a").unicode = ord("a")
ds.sources[1].font.newGlyph("b").unicode = ord("b")
ds.sources[1].font.newGlyph("c").unicode = ord("c")
ds.sources[
1
].font.features.text = """
feature curs {
position cursive a ;
position cursive b ;
position cursive c ;
} curs;
"""
ds.sources[2].font.newGlyph("a").unicode = ord("a")
ds.sources[2].font.newGlyph("b").unicode = ord("b")
ds.sources[2].font.newGlyph("c").unicode = ord("c")
ds.sources[
2
].font.features.text = """
feature curs {
position cursive b ;
position cursive c ;
} curs;
"""
font = compileVariableTTF(ds, inplace=True)
b = BytesIO()
font.save(b)
assert font["GDEF"].table.VarStore.VarData[0].Item[0] == [-100, 0]