Files
odysseus/tests/test_editor_adjustment_layers_js.py
T

104 lines
4.4 KiB
Python

import json
import subprocess
import textwrap
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
PIXEL_MODULE = (ROOT / "static/js/editor/fx/pixel-pass.js").as_uri()
ADJUSTMENT_MODULE = (ROOT / "static/js/editor/adjustment-layer.js").as_uri()
ADJUSTMENT_WORKER = ROOT / "static/js/editor/adjustments-worker.js"
def run_node(source: str):
result = subprocess.run(
["node", "--input-type=module", "-e", source],
cwd=ROOT,
text=True,
capture_output=True,
check=True,
)
return json.loads(result.stdout)
def test_curve_lut_is_identity_and_interpolates_control_points():
source = textwrap.dedent(
f"""
import {{curveLut}} from {json.dumps(PIXEL_MODULE)};
const identity=curveLut([[0,0],[255,255]]);
const shaped=curveLut([[0,0],[128,220],[255,255]]);
console.log(JSON.stringify({{
identity:[identity[0],identity[64],identity[128],identity[255]],
shaped:[shaped[0],shaped[64],shaped[128],shaped[192],shaped[255]],
}}));
"""
)
result = run_node(source)
assert result["identity"] == [0, 64, 128, 255]
assert result["shaped"] == [0, 110, 220, 238, 255]
def test_adjustment_normalization_deep_merges_channel_and_selective_defaults():
source = textwrap.dedent(
f"""
import {{normalizeAdjustmentData}} from {json.dumps(ADJUSTMENT_MODULE)};
const levels=normalizeAdjustmentData({{type:'levels',params:{{channels:{{red:{{outWhite:90}}}}}}}});
const selective=normalizeAdjustmentData({{type:'selective-color',params:{{range:'blues',ranges:{{blues:{{cyan:30}}}}}}}});
console.log(JSON.stringify({{levels,selective}}));
"""
)
result = run_node(source)
assert result["levels"]["params"]["channels"]["red"] == {
"inBlack": 0, "inWhite": 255, "gamma": 1,
"outBlack": 0, "outWhite": 90,
}
assert result["selective"]["params"]["ranges"]["blues"] == {
"cyan": 30, "magenta": 0, "yellow": 0, "black": 0,
}
assert "reds" in result["selective"]["params"]["ranges"]
def test_color_adjustments_produce_deterministic_pixels_and_preserve_alpha():
source = textwrap.dedent(
f"""
import {{applyAdjustment}} from {json.dumps(PIXEL_MODULE)};
class FakeContext {{
constructor(canvas) {{ this.canvas=canvas; this.filter='none'; }}
drawImage(source) {{ this.canvas.pixels=new Uint8ClampedArray(source.pixels); }}
getImageData() {{ return {{data:new Uint8ClampedArray(this.canvas.pixels)}}; }}
putImageData(image) {{ this.canvas.pixels=new Uint8ClampedArray(image.data); }}
}}
class FakeCanvas {{
constructor(pixels=[0,0,0,0]) {{ this.width=1; this.height=1; this.pixels=new Uint8ClampedArray(pixels); this.ctx=new FakeContext(this); }}
getContext() {{ return this.ctx; }}
}}
globalThis.document={{createElement:()=>new FakeCanvas()}};
const source=new FakeCanvas([160,80,40,173]);
const specs=[
{{type:'exposure',params:{{exposure:1,offset:0,gamma:1}}}},
{{type:'white-balance',params:{{temperature:80,tint:-20}}}},
{{type:'hue-saturation',params:{{hue:90,saturation:1.4,lightness:10}}}},
{{type:'vibrance',params:{{vibrance:80}}}},
{{type:'shadows-highlights',params:{{shadows:65,highlights:-40}}}},
{{type:'selective-color',params:{{ranges:{{reds:{{cyan:40,magenta:0,yellow:0,black:0}}}}}}}},
{{type:'gradient-map',params:{{shadows:'#102040',highlights:'#f0d090',midpoint:50,reverse:false}}}},
];
console.log(JSON.stringify(specs.map(spec=>Array.from(applyAdjustment(source,spec).pixels))));
"""
)
result = run_node(source)
assert len(result) == 7
assert all(pixel[3] == 173 for pixel in result)
assert all(pixel[:3] != [160, 80, 40] for pixel in result)
assert len({tuple(pixel) for pixel in result}) == 7
def test_adjustment_worker_reuses_shared_pixel_pass_and_returns_transferable_bitmap():
worker = ADJUSTMENT_WORKER.read_text()
adjustment = (ROOT / "static/js/editor/adjustment-layer.js").read_text()
assert "await import('./fx/pixel-pass.js')" in worker
assert "self.document = { createElement:" in worker
assert "transferToImageBitmap()" in worker
assert "drawAdjustmentLayerAsync" in adjustment
assert "new Worker(new URL('./adjustments-worker.js', import.meta.url)" in adjustment