Embed Photopea in web apps using photopea.js. Covers embedding, file I/O, scripting, exporting, layers, text, filters, and the full Photoshop-compatible API.
Scanned 9/12/2026
Install to Claude Code
npx -y skills add FrancoStino/opencode-skills-collection --skill photopea-embedded-editor --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Photopea Embedded Editor?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/francostino-photopea-embedded-editor)More formats (shields.io, HTML) on the badges page.
---
name: photopea-embedded-editor
description: Embed Photopea in web apps using photopea.js. Covers embedding, file I/O, scripting, exporting, layers, text, filters, and the full Photoshop-compatible API.
risk: safe
source: community
source_repo: yikuansun/PhotopeaAPI
source_type: community
license: MIT
license_source: "https://github.com/yikuansun/PhotopeaAPI/blob/master/LICENSE"
date_added: 2026-05-20
---
# Photopea Embedded Editor Skill
## Detailed Guide
Read [the detailed guide](references/detailed-guide.md) 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
**Do NOT** use raw `postMessage` wiring — always use `photopea.js` as the wrapper.
---
## Complete Practical Script Examples
### 1. Rename all text layers based on their contents
```js
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
```js
app.preferences.rulerUnits = Units.PIXELS;
var doc = app.activeDocument;
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("/" + doc.layers[i].name + ".png"),
ExportType.SAVEFORWEB, opts
);
}
// Restore visibility
for (var i = 0; i < doc.layers.length; i++) doc.layers[i].visible = true;
```
### 3. Find and replace text across all text layers
```js
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
```js
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
```js
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;
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
```js
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)));
```
---
## 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.
- `runScript` executes scripts inside the embedded Photopea document context. Only run scripts you understand and only with user-approved files.
- Serialize dynamic values with `JSON.stringify` before embedding them in a `runScript` string. 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.
---
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!