/** * 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);