Imported from sickn33/agentic-awesome-skills (
skills/photopea-embedded-editor/SKILL.md). Install upstream withnpx skills add sickn33/agentic-awesome-skills --skill photopea-embedded-editor. Copyright stays with the author (MIT).
Photopea Embedded Editor Skill
Detailed Guide
Read the detailed guide before executing this skill. It retains the complete procedure and reference material. Treat its safety, prerequisites, and validation requirements as mandatory. For focused work, load the relevant sections; for end-to-end work, read the guide completely.
When to Use This Skill
Use this skill for every task that involves:
- Embedding Photopea as an image editor inside a webpage or web app
- Controlling an embedded Photopea instance from your JavaScript code
- Automating image editing workflows from a host page (open files, run scripts, export results)
- Building an image editing feature into your product using Photopea as the engine
- Writing scripts to manipulate documents, layers, text, selections, filters, colors, and paths
Use a reviewed messaging wrapper or carefully scoped Live Messaging implementation. A wrapper alone does not establish message authenticity: verify received origin and source against the exact iframe/parent, use an exact target origin, serialize operations and bound timeouts. Do not accept arbitrary page messages as export data.
Complete Practical Script Examples
1. Rename all text layers based on their contents
app.preferences.rulerUnits = Units.PIXELS;
var doc = app.activeDocument;
function processLayers(parent) {
for (var i = 0; i < parent.layers.length; i++) {
var l = parent.layers[i];
if (l.typename === "LayerSet") processLayers(l);
else if (l.kind === LayerKind.TEXT) {
l.name = l.textItem.contents.substring(0, 30);
}
}
}
processLayers(doc);
app.echoToOE("done");
2. Export each layer as a separate PNG
app.preferences.rulerUnits = Units.PIXELS;
var doc = app.activeDocument;
var originalVisibility = [];
for (var i = 0; i < doc.layers.length; i++) originalVisibility.push(doc.layers[i].visible);
try {
for (var i = 0; i < doc.layers.length; i++) {
// Hide all layers
for (var j = 0; j < doc.layers.length; j++) doc.layers[j].visible = false;
// Show only this layer
doc.layers[i].visible = true;
// Export
var opts = new ExportOptionsSaveForWeb();
opts.format = SaveDocumentType.PNG;
opts.PNG8 = false;
opts.quality = 100;
doc.exportDocument(
new File("/layer-" + i + ".png"),
ExportType.SAVEFORWEB, opts
);
}
} finally {
for (var i = 0; i < doc.layers.length; i++) doc.layers[i].visible = originalVisibility[i];
}
3. Find and replace text across all text layers
var searchText = "2024";
var replaceText = "2025";
function findReplaceText(parent) {
for (var i = 0; i < parent.layers.length; i++) {
var l = parent.layers[i];
if (l.typename === "LayerSet") findReplaceText(l);
else if (l.kind === LayerKind.TEXT) {
var t = l.textItem;
if (t.contents.indexOf(searchText) !== -1) {
t.contents = t.contents.split(searchText).join(replaceText);
}
}
}
}
findReplaceText(app.activeDocument);
app.echoToOE("Find & Replace complete");
4. Grid of duplicate layers
app.preferences.rulerUnits = Units.PIXELS;
var doc = app.activeDocument;
var layer = doc.activeLayer;
var cols = 4, rows = 3;
var padX = 20, padY = 20;
var w = layer.bounds[2] - layer.bounds[0];
var h = layer.bounds[3] - layer.bounds[1];
for (var r = 0; r < rows; r++) {
for (var c = 0; c < cols; c++) {
if (r === 0 && c === 0) continue; // skip original
var copy = layer.duplicate();
var targetX = layer.bounds[0] + c * (w + padX);
var targetY = layer.bounds[1] + r * (h + padY);
copy.translate(targetX - copy.bounds[0], targetY - copy.bounds[1]);
copy.opacity = 100 - (r * cols + c) * 5;
}
}
5. Apply watermark from URL
app.preferences.rulerUnits = Units.PIXELS;
var doc = app.activeDocument;
// Open watermark as smart object layer
app.open("https://example.com/watermark.png", null, true);
var wm = doc.activeLayer;
// Resize to 20% of document width
var wmW = wm.bounds[2] - wm.bounds[0];
var targetW = doc.width * 0.2;
if (wmW <= 0) throw new Error("Watermark has no measurable width");
var scalePct = (targetW / wmW) * 100;
wm.resize(scalePct, scalePct, AnchorPosition.TOPLEFT);
// Move to bottom-right with 20px margin
var wmNewW = wm.bounds[2] - wm.bounds[0];
var wmNewH = wm.bounds[3] - wm.bounds[1];
wm.translate(
doc.width - wmNewW - 20 - wm.bounds[0],
doc.height - wmNewH - 20 - wm.bounds[1]
);
wm.opacity = 60;
app.echoToOE("watermark applied");
6. Get all layer info as JSON
function getLayerInfo(parent, depth) {
depth = depth || 0;
var result = [];
for (var i = 0; i < parent.layers.length; i++) {
var l = parent.layers[i];
var info = {
name: l.name,
type: l.typename,
visible: l.visible,
opacity: l.opacity,
depth: depth
};
if (l.typename === "ArtLayer") {
info.kind = l.kind.toString();
info.bounds = [l.bounds[0], l.bounds[1], l.bounds[2], l.bounds[3]];
if (l.kind === LayerKind.TEXT) {
info.text = l.textItem.contents;
info.font = l.textItem.font;
info.size = l.textItem.size;
}
} else if (l.typename === "LayerSet") {
info.children = getLayerInfo(l, depth + 1);
}
result.push(info);
}
return result;
}
app.echoToOE(JSON.stringify(getLayerInfo(app.activeDocument)));
Integration boundary
Embedding loads a third-party application. Review its terms and privacy/network behavior before loading confidential documents, fonts or assets. Work on an approved duplicate for destructive scripts and preserve a recoverable original. The third-party wrapper source is not bundled; pin and inspect its actual version before enabling document control. Treat URLs and returned strings as data; HTML script embedding also requires safe handling of </script> beyond JavaScript string serialization. Do not expose credentials in remote asset URLs.
Limitations
- This skill covers host-page integration patterns; it does not replace Photopea's own terms, API documentation, or licensing guidance.
- Remote URL loading depends on browser CORS behavior, network availability, and the user's Photopea account/session state.
runScriptexecutes scripts inside the embedded Photopea document context. Only run scripts you understand and only with user-approved files.- Serialize dynamic values with
JSON.stringifybefore embedding them in arunScriptstring. Never concatenate user-provided URLs, layer names, or text directly into Photopea script source. - Export behavior can vary by document size, browser memory limits, and the formats supported by the active Photopea runtime.
