mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-20 05:42:20 +02:00
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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c74f475679
commit
5fac693c1c
@@ -0,0 +1,74 @@
|
||||
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
|
||||
Reference in New Issue
Block a user