This commit is contained in:
2026-07-09 02:59:53 +03:00
commit 7ace048d94
817 changed files with 234289 additions and 0 deletions
+41
View File
@@ -0,0 +1,41 @@
# Photopea / Photoshop ExtendScript Dataset Generator
This folder contains JSX scripts to generate systematic layer-effect ground-truth images in Photopea or Photoshop.
## Files
- `generate_inner_shadow_dataset.jsx` — Short starter script for Inner Shadow.
## Usage
1. Open Photopea (https://www.photopea.com) in a browser.
2. Press `Alt+Ctrl+I` (or use `File > Automate > Script`) to open the script runner.
3. Paste the contents of the desired `.jsx` file and run it.
4. Select an output folder when prompted.
Photopea will create a new document for each parameter combination, draw a black rectangle, apply the Inner Shadow effect, hide the background, and export a transparent PNG.
## Current parameter grid (short version)
| Parameter | Values |
|---|---|
| angle | 0, 90, 180, 270 |
| distance | 5, 15, 30 |
| size | 10, 30 |
| choke | 0, 20 |
| fx opacity | 75, 100 |
| fx blend mode | Normal, Multiply |
Total images: `4 × 3 × 2 × 2 × 2 × 2 = 192`.
## Notes
- Blend mode keys use Photoshop's 4-character codes (`Nrml`, `Mltp`).
- The output PNGs have transparent backgrounds, so MAE comparisons can ignore white reference pixels if needed.
- ExtendScript error handling is basic; if a parameter combination fails, the script logs it and continues.
## Next steps
- Add more layer blend modes, fill opacity, and layer opacity variations.
- Extend to Drop Shadow, Outer Glow, Inner Glow, Stroke, Bevel & Emboss.
- Add a companion Rust generator that creates matching PSD files so the same dataset can be imported back into HCIE for MAE tuning.
@@ -0,0 +1,292 @@
/**
* generate_inner_shadow_dataset.jsx
*
* Purpose:
* Generate a systematic Inner Shadow effect dataset in Photopea.
* Uses stringID-based ActionDescriptor for maximum Photopea compatibility.
*
* Logic & Workflow:
* 1. Creates a new RGB document with transparent background.
* 2. Adds a normal raster layer and fills the center with black.
* 3. Applies one Inner Shadow effect via ActionDescriptor (stringID).
* 4. Hides the background layer.
* 5. Saves the result as a transparent PNG.
*
* Usage:
* 1. Open https://www.photopea.com in a browser.
* 2. Alt+Ctrl+I (or File > Automate > Script).
* 3. Paste this script and run.
* 4. Select output folder when prompted.
*/
var outFolder = Folder.selectDialog("Select output folder for Inner Shadow dataset");
if (!outFolder) {
alert("No output folder selected. Exiting.");
throw new Error("No output folder");
}
var angles = [0, 90, 180, 270];
var distances = [5, 15, 30];
var sizes = [10, 30];
var chokes = [0, 20];
var fxOpacities = [75, 100];
var fxBlendModes = [
{ id: "normal", name: "normal" },
{ id: "multiply", name: "multiply" }
];
function createDocument(name, width, height) {
var doc = app.documents.add(width, height, 72, name, NewDocumentMode.RGB, DocumentFill.TRANSPARENT);
try {
var bg = doc.artLayers[doc.artLayers.length - 1];
if (bg.isBackgroundLayer) {
bg.visible = false;
}
} catch (e) {}
return doc;
}
function addBlackRectangle(doc) {
var w = doc.width;
var h = doc.height;
if (typeof w === "object" && typeof w.value === "function") { w = w.value; }
if (typeof h === "object" && typeof h.value === "function") { h = h.value; }
w = Number(w);
h = Number(h);
var margin = Math.round(Math.min(w, h) * 0.25);
var layer = doc.artLayers.add();
layer.name = "shape";
var region = [
[margin, margin],
[w - margin, margin],
[w - margin, h - margin],
[margin, h - margin]
];
doc.selection.select(region);
var black = new SolidColor();
black.rgb.red = 0;
black.rgb.green = 0;
black.rgb.blue = 0;
doc.selection.fill(black);
doc.selection.deselect();
return layer;
}
/**
* Try applying Inner Shadow via ActionDescriptor with stringID keys.
* Returns true on success, false on failure.
*/
function applyInnerShadow(params) {
try {
var desc = new ActionDescriptor();
var ref = new ActionReference();
ref.putProperty(stringIDToTypeID("property"), stringIDToTypeID("layerEffects"));
ref.putEnumerated(stringIDToTypeID("layer"), stringIDToTypeID("ordinal"), stringIDToTypeID("targetEnum"));
desc.putReference(stringIDToTypeID("null"), ref);
var fxDesc = new ActionDescriptor();
var isDesc = new ActionDescriptor();
isDesc.putBoolean(stringIDToTypeID("enabled"), true);
// Blend mode
isDesc.putEnumerated(stringIDToTypeID("mode"), stringIDToTypeID("blendMode"), stringIDToTypeID(params.blendMode));
// Color
var colorDesc = new ActionDescriptor();
colorDesc.putDouble(stringIDToTypeID("red"), 0);
colorDesc.putDouble(stringIDToTypeID("grain"), 0);
colorDesc.putDouble(stringIDToTypeID("blue"), 0);
isDesc.putObject(stringIDToTypeID("color"), stringIDToTypeID("RGBColor"), colorDesc);
// Opacity (0-100)
isDesc.putInteger(stringIDToTypeID("opacity"), params.opacity);
// Angle
isDesc.putInteger(stringIDToTypeID("localLightingAngle"), params.angle);
// Global light
isDesc.putBoolean(stringIDToTypeID("useGlobalAngle"), false);
// Distance
isDesc.putInteger(stringIDToTypeID("distance"), params.distance);
// Choke
isDesc.putInteger(stringIDToTypeID("chokeMatte"), params.choke);
// Blur (size)
isDesc.putInteger(stringIDToTypeID("blur"), params.size);
// Noise
isDesc.putInteger(stringIDToTypeID("noise"), 0);
// Anti-alias
isDesc.putBoolean(stringIDToTypeID("antiAlias"), true);
// Transfer function (contour)
isDesc.putBoolean(stringIDToTypeID("transferSpecIsScaled"), false);
fxDesc.putObject(stringIDToTypeID("innerShadow"), stringIDToTypeID("innerShadow"), isDesc);
desc.putObject(stringIDToTypeID("to"), stringIDToTypeID("layerEffects"), fxDesc);
executeAction(stringIDToTypeID("set"), desc, DialogModes.NO);
return true;
} catch (e) {
$.writeln("stringID failed: " + e);
return false;
}
}
/**
* Fallback: apply via charID-based descriptor.
*/
function applyInnerShadowCharID(params) {
try {
var desc = new ActionDescriptor();
var ref = new ActionReference();
ref.putProperty(charIDToTypeID("Prpr"), charIDToTypeID("Lefx"));
ref.putEnumerated(charIDToTypeID("Lyr "), charIDToTypeID("Ordn"), charIDToTypeID("Trgt"));
desc.putReference(charIDToTypeID("null"), ref);
var fxDesc = new ActionDescriptor();
var isDesc = new ActionDescriptor();
isDesc.putBoolean(charIDToTypeID("enab"), true);
isDesc.putBoolean(charIDToTypeID("present"), true);
isDesc.putBoolean(charIDToTypeID("showInDialog"), true);
// Blend mode
isDesc.putEnumerated(charIDToTypeID("Blnd"), charIDToTypeID("BlnM"), charIDToTypeID(params.blendModeCharID));
// Color
var clr = new ActionDescriptor();
clr.putDouble(charIDToTypeID("Rd "), 0);
clr.putDouble(charIDToTypeID("Grn "), 0);
clr.putDouble(charIDToTypeID("Bl "), 0);
isDesc.putObject(charIDToTypeID("Clr "), charIDToTypeID("RGBC"), clr);
// Opacity: 0-100 integer
isDesc.putInteger(charIDToTypeID("Opct"), params.opacity);
// Angle: use lagl (local angle) instead of Angl
isDesc.putInteger(charIDToTypeID("lagl"), params.angle);
isDesc.putBoolean(charIDToTypeID("uglg"), false);
// Distance
isDesc.putInteger(charIDToTypeID("Dstn"), params.distance);
// Choke
isDesc.putInteger(charIDToTypeID("Ckmt"), params.choke);
// Blur
isDesc.putInteger(charIDToTypeID("blur"), params.size);
// Noise
isDesc.putInteger(charIDToTypeID("Nose"), 0);
fxDesc.putObject(charIDToTypeID("IrSh"), charIDToTypeID("IrSh"), isDesc);
desc.putObject(charIDToTypeID("T "), charIDToTypeID("Lefx"), fxDesc);
executeAction(charIDToTypeID("setd"), desc, DialogModes.NO);
return true;
} catch (e) {
$.writeln("charID failed: " + e);
return false;
}
}
function saveTransparentPng(doc, file) {
var pngOptions = new PNGSaveOptions();
pngOptions.compression = 6;
pngOptions.interlaced = false;
doc.saveAs(file, pngOptions, true, Extension.LOWERCASE);
}
var count = 0;
var errors = 0;
var total = angles.length * distances.length * sizes.length * chokes.length * fxOpacities.length * fxBlendModes.length;
var blendModeCharMap = {
"normal": "Nrml",
"multiply": "Mltp"
};
for (var a = 0; a < angles.length; a++) {
for (var d = 0; d < distances.length; d++) {
for (var s = 0; s < sizes.length; s++) {
for (var c = 0; c < chokes.length; c++) {
for (var o = 0; o < fxOpacities.length; o++) {
for (var b = 0; b < fxBlendModes.length; b++) {
var bmName = fxBlendModes[b].name;
var bmID = fxBlendModes[b].id;
var fileName = "is_" +
"a" + angles[a] + "_" +
"d" + distances[d] + "_" +
"s" + sizes[s] + "_" +
"c" + chokes[c] + "_" +
"op" + fxOpacities[o] + "_" +
"bm" + bmName +
".png";
var file = new File(outFolder + "/" + fileName);
if (file.exists) {
count++;
continue;
}
var params = {
angle: angles[a],
distance: distances[d],
size: sizes[s],
choke: chokes[c],
opacity: fxOpacities[o],
blendMode: bmID,
blendModeCharID: blendModeCharMap[bmID] || "Nrml"
};
var doc = createDocument("inner_shadow_test", 256, 256);
var layer = addBlackRectangle(doc);
var applied = applyInnerShadow(params);
if (!applied) {
applied = applyInnerShadowCharID(params);
}
if (applied) {
try {
saveTransparentPng(doc, file);
} catch (e2) {
$.writeln("Save error: " + fileName + " — " + e2);
errors++;
}
} else {
$.writeln("FAILED to apply effect: " + fileName);
errors++;
}
doc.close(SaveOptions.DONOTSAVECHANGES);
count++;
if (count % 10 === 0) {
$.writeln("Progress: " + count + " / " + total + " (errors: " + errors + ")");
}
}
}
}
}
}
}
alert("Done. Generated " + count + " images, " + errors + " errors.\n" + outFolder.fsName);
+206
View File
@@ -0,0 +1,206 @@
#!/usr/bin/env python3
"""Test Photopea inner shadow via CDP."""
import json, time, subprocess, sys, os
CHROMIUM = "/snap/bin/chromium"
PORT = 9555
OUT_DIR = "/mnt/extra/00_PROJECTS/hcie-rust-v4/_tmp/photopea_test"
os.makedirs(OUT_DIR, exist_ok=True)
def http_get(path):
import urllib.request
with urllib.request.urlopen(f"http://127.0.0.1:{PORT}{path}", timeout=10) as r:
return json.loads(r.read())
class CDP:
def __init__(self, ws_url):
import websocket
self.ws = websocket.create_connection(ws_url, timeout=60)
self._id = 0
def send(self, method, params=None):
self._id += 1
msg = {"id": self._id, "method": method}
if params:
msg["params"] = params
self.ws.send(json.dumps(msg))
while True:
resp = json.loads(self.ws.recv())
if resp.get("id") == self._id:
return resp
def eval_js(self, expr, await_promise=True, timeout_ms=45000):
r = self.send("Runtime.evaluate", {
"expression": expr,
"awaitPromise": await_promise,
"returnByValue": True,
"timeout": timeout_ms,
})
val = r.get("result", {}).get("result", {})
if "value" in val:
return val["value"]
return val
def close(self):
try: self.ws.close()
except: pass
def main():
proc = subprocess.Popen([
CHROMIUM,
"--headless=new",
"--no-sandbox",
"--disable-gpu",
"--disable-dev-shm-usage",
f"--remote-debugging-port={PORT}",
"--remote-allow-origins=*",
"--window-size=1024,768",
"about:blank",
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
time.sleep(4)
try:
targets = http_get("/json")
ws_url = None
for t in targets:
if t.get("type") == "page":
ws_url = t["webSocketDebuggerUrl"]
break
if not ws_url:
print("FATAL: no page target")
return 1
cdp = CDP(ws_url)
print(f"Connected ({ws_url[:60]}...)")
print("[1] Navigating to photopea.com...")
cdp.send("Page.navigate", {"url": "https://www.photopea.com"})
print("[2] Waiting for page load...")
time.sleep(90)
r = cdp.eval_js('document.readyState', await_promise=False)
print(f" readyState={r}")
# Check what globals exist
print("[3] Probing global scope...")
probe = cdp.eval_js("""
JSON.stringify({
hasApp: typeof app !== "undefined",
hasApp2: typeof window.app !== "undefined",
hasPhotoshop: typeof Photoshop !== "undefined",
hasPhotoshopApp: typeof photoshop !== "undefined",
bodyLen: document.body ? document.body.innerHTML.length : 0,
scripts: document.scripts.length,
title: document.title,
iframes: document.querySelectorAll("iframe").length,
})
""", await_promise=False)
print(f" globals: {probe}")
# Photopea uses a special mechanism - try postMessage approach
# Photopea might listen for messages with specific format
print("[4] Trying Photopea postMessage API...")
# Photopea's API: when loaded in an iframe, parent sends scripts via postMessage.
# But we're ON the photopea.com page. Let's try app.executeScript or direct eval.
r2 = cdp.eval_js("""
(function() {
// Try to find app in various scopes
var found = [];
try { if (window.app) found.push("window.app"); } catch(e) {}
try { if (self.app) found.push("self.app"); } catch(e) {}
try { if (globalThis.app) found.push("globalThis.app"); } catch(e) {}
// Check if there are any Photopea-specific globals
var keys = Object.keys(window).filter(k =>
k.toLowerCase().includes("photo") ||
k.toLowerCase().includes("ppea") ||
k.toLowerCase().includes("pp") ||
k.toLowerCase().includes("app")
);
return JSON.stringify({found: found, photoKeys: keys.slice(0, 20)});
})()
""", await_promise=False)
print(f" probe result: {r2}")
# Try running a script via Photopea's mechanism
# Photopea can receive scripts via URL hash: photopea.com/#script_here
# But more importantly, it exposes app when loaded as intended
print("[5] Trying hash-based script execution...")
script = 'var doc = app.documents.add(256, 256, 72, "test", NewDocumentMode.RGB, DocumentFill.TRANSPARENT); app.echoToOE("OK");'
import base64
b64 = base64.b64encode(script.encode()).decode()
cdp.send("Page.navigate", {"url": f"https://www.photopea.com#{script}"})
time.sleep(15)
r3 = cdp.eval_js('typeof app !== "undefined" ? "app_FOUND" : "no_app"', await_promise=False)
print(f" after hash nav: {r3}")
# Maybe we need to add --enable-webgl or similar
# Let's try a completely different approach: use Photopea's RPC mechanism
print("[6] Checking if Photopea uses MessagePort or ServiceWorker...")
r4 = cdp.eval_js("""
(function() {
var info = {};
try { info.workers = navigator.serviceWorker ? "available" : "not available"; } catch(e) { info.workers = "err:" + e; }
try { info.sockets = typeof WebSocket !== "undefined" ? "available" : "not"; } catch(e) {}
// Check for MessageChannel
try {
var ch = new MessageChannel();
info.messageChannel = "available";
} catch(e) { info.messageChannel = "err"; }
// Check canvas elements (Photopea renders to canvas)
var canvases = document.querySelectorAll("canvas");
info.canvasCount = canvases.length;
info.canvasDims = [];
canvases.forEach(function(c) { info.canvasDims.push(c.width + "x" + c.height); });
return JSON.stringify(info);
})()
""", await_promise=False)
print(f" {r4}")
# Last resort: check the actual Photopea API docs
# Photopea exposes `app` on window when the UI is loaded
# It might need WebGL canvas to be created first
# Let's check if there's a #pea-app or similar container
r5 = cdp.eval_js("""
(function() {
var divs = document.querySelectorAll("div[id]");
var ids = [];
divs.forEach(function(d) { ids.push(d.id); });
return JSON.stringify(ids.slice(0, 30));
})()
""", await_promise=False)
print(f" div ids: {r5}")
print("\n=== CONCLUSION ===")
print("Photopea loaded but `app` is NOT in the global JS scope.")
print("This means Photopea's scripting API is only available via postMessage")
print("when Photopea is embedded as an iframe in another page.")
print("")
print("The correct approach for batch generation is:")
print(" - The user runs the JSX script manually in Photopea's Script runner")
print(" - OR use a different tool (GIMP Script-Fu, Python PIL)")
return 0
except Exception as e:
print(f"EXCEPTION: {e}")
import traceback
traceback.print_exc()
return 1
finally:
try: cdp.close()
except: pass
try: proc.kill()
except: pass
try: proc.wait(timeout=5)
except: pass
if __name__ == "__main__":
sys.exit(main() or 0)