mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-18 21:02: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,130 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user