mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-16 11:52:25 +02:00
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>
131 lines
5.6 KiB
Python
131 lines
5.6 KiB
Python
"""Perspective Crop (3-4) — pure-engine numeric tests + static contract.
|
|
|
|
The pixel engine (perspective-crop.js + perspective-warp.js `warpRgba`) is
|
|
node-runnable, so the size/normalization maths is exercised for real. The rest
|
|
is a static contract over the wiring (galleryEditor, topbar, wire, CSS, sw).
|
|
"""
|
|
import json
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
CROP = ROOT / "static/js/editor/perspective-crop.js"
|
|
TOOL = ROOT / "static/js/editor/tools/perspective-crop.js"
|
|
GE = ROOT / "static/js/galleryEditor.js"
|
|
TOPBAR = ROOT / "static/js/editor/build/topbar.js"
|
|
WIRE = ROOT / "static/js/editor/wire-topbar-menus.js"
|
|
CSS = ROOT / "static/css/13-image-editor.css"
|
|
SW = ROOT / "static/sw.js"
|
|
|
|
|
|
def _node(expr: str):
|
|
return subprocess.run(
|
|
["node", "--input-type=module", "-e", expr],
|
|
cwd=ROOT, capture_output=True, text=True,
|
|
)
|
|
|
|
|
|
def test_perspective_crop_size_and_normalize_numeric():
|
|
expr = (
|
|
"const m = await import('./static/js/editor/perspective-crop.js');"
|
|
"const size = m.perspectiveCropSize(["
|
|
"{x:10,y:20},{x:110,y:20},{x:110,y:70},{x:10,y:70}],200,100);"
|
|
"const norm = m.normalizeCropCorners(["
|
|
"{x:10,y:20},{x:110,y:20},{x:110,y:70},{x:10,y:70}],200,100);"
|
|
"console.log(JSON.stringify({size, norm}));"
|
|
)
|
|
result = _node(expr)
|
|
assert result.returncode == 0, result.stderr
|
|
data = json.loads(result.stdout.strip().splitlines()[-1])
|
|
assert data["size"] == {"width": 100, "height": 50}
|
|
assert abs(data["norm"][0]["x"] - 0.05) < 1e-6
|
|
assert abs(data["norm"][2]["y"] - 0.7) < 1e-6
|
|
|
|
|
|
def test_warp_identity_and_quad_scale_numeric():
|
|
expr = (
|
|
"const w = await import('./static/js/editor/perspective-warp.js');"
|
|
"const src = new Uint8ClampedArray(4*4*4);"
|
|
"for (let i=0;i<4*4;i++){ src[i*4]=255; src[i*4+3]=255; }"
|
|
"const full = w.warpRgba(src,4,4,[{x:0,y:0},{x:1,y:0},{x:1,y:1},{x:0,y:1}],4,4);"
|
|
"const half = w.warpRgba(src,4,4,[{x:0,y:0},{x:0.5,y:0},{x:0.5,y:0.5},{x:0,y:0.5}],2,2);"
|
|
"console.log(JSON.stringify({full0:Array.from(full.slice(0,4)),halfLen:half.length,half0:Array.from(half.slice(0,4))}));"
|
|
)
|
|
result = _node(expr)
|
|
assert result.returncode == 0, result.stderr
|
|
data = json.loads(result.stdout.strip().splitlines()[-1])
|
|
assert data["full0"][0] == 255 and data["full0"][3] == 255
|
|
assert data["halfLen"] == 2 * 2 * 4
|
|
assert data["half0"][3] == 255
|
|
|
|
|
|
def test_engine_exports_and_forward_homography():
|
|
src = CROP.read_text()
|
|
assert "export function perspectiveCropSize(" in src
|
|
assert "export function normalizeCropCorners(" in src
|
|
assert "export function perspectiveCropCanvas(" in src
|
|
assert "export function defaultPerspectiveCorners(" in src
|
|
# Straighten uses the FORWARD homography (quad -> axis-aligned output),
|
|
# not the inverse warp that left the result skewed.
|
|
assert "solveSquareToQuad" in src
|
|
assert "import { solveSquareToQuad } from './perspective-warp.js';" in src
|
|
assert "warpRgba(" not in src
|
|
assert "export function solveSquareToQuad(" in (ROOT / "static/js/editor/perspective-warp.js").read_text()
|
|
|
|
|
|
def test_forward_map_sends_output_corners_to_the_quad_numeric():
|
|
# The quad's four corners must land EXACTLY on the output unit-square
|
|
# corners (so the quad fills the whole output rectangle => straighten).
|
|
quad = [
|
|
{"x": 0.2, "y": 0.1}, {"x": 0.8, "y": 0.2},
|
|
{"x": 0.7, "y": 0.9}, {"x": 0.15, "y": 0.8},
|
|
]
|
|
expr = (
|
|
"const w = await import('./static/js/editor/perspective-warp.js');"
|
|
f"const quad = {json.dumps(quad)};"
|
|
"const [a,b,c,d,e,f,g,h] = w.solveSquareToQuad(quad);"
|
|
"const map = (u,v) => { const ww = g*u+h*v+1;"
|
|
" return {x:(a*u+b*v+c)/ww, y:(d*u+e*v+f)/ww}; };"
|
|
"console.log(JSON.stringify([map(0,0),map(1,0),map(1,1),map(0,1)]));"
|
|
)
|
|
result = _node(expr)
|
|
assert result.returncode == 0, result.stderr
|
|
mapped = json.loads(result.stdout.strip().splitlines()[-1])
|
|
for got, want in zip(mapped, quad):
|
|
assert abs(got["x"] - want["x"]) < 1e-9, (got, want)
|
|
assert abs(got["y"] - want["y"]) < 1e-9, (got, want)
|
|
|
|
|
|
def test_tool_overlay_module():
|
|
src = TOOL.read_text()
|
|
assert "export function createPerspectiveCropTool(" in src
|
|
assert "ge-pcrop-overlay" in src
|
|
assert 'data-pcrop="apply"' in src
|
|
assert 'data-pcrop="cancel"' in src
|
|
assert "data-corner" in src
|
|
assert "onApply?." in src
|
|
|
|
|
|
def test_gallery_editor_wiring():
|
|
src = GE.read_text()
|
|
assert "import { createPerspectiveCropTool } from './editor/tools/perspective-crop.js';" in src
|
|
assert "import { perspectiveCropSize as _perspectiveCropSize, perspectiveCropCanvas as _perspectiveCropCanvas }" in src
|
|
assert "const _perspectiveCropTool = createPerspectiveCropTool(" in src
|
|
assert "async function _applyPerspectiveCrop(corners)" in src
|
|
assert "_perspectiveCropSize(corners, state.imgWidth, state.imgHeight)" in src
|
|
assert "'document.perspectiveCrop':" in src
|
|
assert "openPerspectiveCrop: () => _openPerspectiveCrop()," in src
|
|
|
|
|
|
def test_menu_and_handler_and_css_and_sw():
|
|
assert 'data-image-action="perspective-crop"' in TOPBAR.read_text()
|
|
wire = WIRE.read_text()
|
|
assert "openPerspectiveCrop" in wire
|
|
assert "else if (action === 'perspective-crop') openPerspectiveCrop?.();" in wire
|
|
assert ".ge-pcrop-overlay" in CSS.read_text()
|
|
sw = SW.read_text()
|
|
import re
|
|
assert re.search(r"odysseus-v\d+-pr40-editor-precache", sw)
|
|
assert "/static/js/editor/perspective-crop.js" in sw
|
|
assert "/static/js/editor/tools/perspective-crop.js" in sw
|