Files
odysseus/tests/test_editor_mask_ops_js.py
T
pewdiepie-archdaemonandClaude Opus 5.5 5fac693c1c feat(photo-editor): Ajax Image Editor client (Photoshop-parity tools)
Photoshop-parity editor client: layers, selection/masking, brushes,
smart filters, LUT import, Quick Edit, mobile gestures, workspaces,
batch operations, and the service-worker versioning (v978->v1006).

Source: vendor/photocraft (ad1cff44) photo-editor client files, merged
onto upstream/dev (a8c147b2). Conflict resolution documented in
pr_work/A_conflicts.md; editor files taken from vendor/photocraft
(upstream's editor is an earlier snapshot of the same feature).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 11:57:35 +00:00

75 lines
3.0 KiB
Python

import json
import subprocess
import textwrap
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
MODULE = (ROOT / 'static/js/editor/mask-ops.js').as_uri()
def run_node(body):
script = textwrap.dedent(f"""
import {{ alphaBounds, invertMaskData, featherMaskCanvas, bakeMaskIntoLayer }} from {json.dumps(MODULE)};
{body}
""")
result = subprocess.run(
['node', '--input-type=module', '-e', script],
cwd=ROOT, text=True, capture_output=True, check=True,
)
return json.loads(result.stdout)
def test_alpha_bounds_of_non_transparent_pixels():
out = run_node("""
const w = 5, h = 4;
const d = new Uint8ClampedArray(w * h * 4);
const set = (x, y, a) => { d[(y * w + x) * 4 + 3] = a; };
set(1, 1, 255); set(3, 2, 255); set(0, 0, 4); // 4 is below the 8 threshold
const b = alphaBounds(d, w, h, 8);
const empty = alphaBounds(new Uint8ClampedArray(w * h * 4), w, h, 8);
console.log(JSON.stringify({ b, empty }));
""")
assert out['b'] == {'x': 1, 'y': 1, 'w': 3, 'h': 2}
assert out['empty'] is None
def test_invert_mask_flips_alpha_to_white():
out = run_node("""
const d = new Uint8ClampedArray(4 * 4); // two pixels
d[3] = 255; // pixel 0 selected
// pixel 1 alpha 0
const inv = invertMaskData(d);
console.log(JSON.stringify({ p0: [inv[0], inv[1], inv[2], inv[3]], p1: [inv[4], inv[5], inv[6], inv[7]], src: [d[3], d[7]] }));
""")
assert out['p0'] == [255, 255, 255, 0]
assert out['p1'] == [255, 255, 255, 255]
assert out['src'] == [255, 0]
def test_feather_mask_uses_blur_filter_and_bake_multiplies_alpha():
out = run_node("""
const created = [];
function mk(w, h) {
const cv = { width: w, height: h, calls: [] };
const ctx = { filter: 'none', drawImage(...a) { cv.calls.push(['drawImage', this.filter, a[0] && a[0].tag]); } };
cv.getContext = () => ctx;
created.push(cv);
return cv;
}
const src = { width: 2, height: 2, tag: 'src' };
const out = featherMaskCanvas(mk, src, 3);
const noFeather = featherMaskCanvas(mk, src, 0);
const lc = { globalCompositeOperation: 'source-over', calls: [], save() { this.calls.push('save'); }, restore() { this.calls.push('restore'); }, drawImage(...a) { this.calls.push(['drawImage', this.globalCompositeOperation, a[0] && a[0].tag]); } };
bakeMaskIntoLayer(lc, { tag: 'mask' }, 0, 0);
console.log(JSON.stringify({ out: out.calls, noFeather: noFeather.calls, lc: lc.calls }));
""")
assert out['out'][0] == ['drawImage', 'blur(3px)', 'src']
assert out['noFeather'][0] == ['drawImage', 'none', 'src']
assert out['lc'] == ['save', ['drawImage', 'destination-in', 'mask'], 'restore']
def test_mask_ops_module_is_precached_for_offline_use():
sw = (ROOT / 'static/sw.js').read_text(encoding='utf-8')
assert "'/static/js/editor/mask-ops.js'" in sw