Embeds Photopea in a host web app via photopea.js (createEmbed, runScript, saveToOE) for PSD-like layers, text, filters, file I/O, and Photoshop-compatible scripts. Use when integrating an in-page image editor or automating edits from the host. Not for desktop Photoshop plugins, raw postMessage wiring, or server-side image libraries (Sharp/Pillow); never call createEmbed on a zero-size container.
يبقى الأمر في سطر واحد. مرّر أفقيًا لمراجعته كاملًا قبل النسخ.
تفضّل نسخة محلية؟ نزّل الملفات المتاحة حاليًا لدى SkillsMP.
عرض SKILL.md
SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
photopea-embedded-editor
description
Embeds Photopea in a host web app via photopea.js (createEmbed, runScript, saveToOE) for PSD-like layers, text, filters, file I/O, and Photoshop-compatible scripts. Use when integrating an in-page image editor or automating edits from the host. Not for desktop Photoshop plugins, raw postMessage wiring, or server-side image libraries (Sharp/Pillow); never call createEmbed on a zero-size container.
React Strict Mode guard: In development, useEffect fires twice. Always guard with if (peaRef.current) return; to prevent double-embedding.
Step 3 — Open Files
// Remote URL → new document
await pea.openFromURL("https://example.com/design.psd", false);
// Remote URL → smart object layer inside current document
await pea.openFromURL("https://example.com/overlay.png", true);
// Local file (user input → ArrayBuffer → loadAsset)
document.getElementById("fileInput").addEventListener("change", async (e) => {
const buf = await e.target.files[0].arrayBuffer();
await pea.loadAsset(buf);
});
// Base64 data URI via runScript
await pea.runScript(`app.open("data:image/png;base64,iVBORw0...");`);
Step 4 — Run Scripts
runScript sends a JS string, returns an array of app.echoToOE(...) values + "done" last.
const result = await pea.runScript(`app.echoToOE("hello");`);
// result → ["hello", "done"]
// Return structured data
const out = await pea.runScript(`
app.echoToOE(JSON.stringify({
width: app.activeDocument.width,
height: app.activeDocument.height,
layers: app.activeDocument.layers.length
}));
`);
const info = JSON.parse(out[0]);
HARD RULE: Always 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.
HARD RULE: Always set app.preferences.rulerUnits = Units.PIXELS at the start of any script that uses pixel measurements:
var savedUnits = app.preferences.rulerUnits;
app.preferences.rulerUnits = Units.PIXELS;
// ... your code ...
app.preferences.rulerUnits = savedUnits;
// Font
const buf = await (await fetch("https://example.com/MyFont.otf")).arrayBuffer();
await pea.loadAsset(buf);
// Now usable in textItem.font
// Brush
await pea.loadAsset(await (await fetch("Nature.ABR")).arrayBuffer());
// Gradient
await pea.loadAsset(await (await fetch("Gradients.GRD")).arrayBuffer());
Step 7 — Plugin Mode
When your page is inside Photopea's sidebar iframe:
const pea = new Photopea(window.parent);
const out = await pea.runScript(`app.echoToOE(app.activeDocument.width);`);
console.log("Width:", out[0]);
// Load an asset from your plugin
const buf = await (await fetch("https://my-assets.com/sticker.png")).arrayBuffer();
await pea.loadAsset(buf);
All code in this section runs inside pea.runScript("...") strings.
Photopea implements the Adobe Photoshop CC 2015 JavaScript scripting interface.
Any Photoshop script targeting that version should work in Photopea.
Photopea extension — clear undo history to free RAM
exportDocument
(file, exportType, options)
Export to filesystem (triggers ZIP). ExportType: SAVEFORWEB
paste
(intoSelection)
Paste clipboard into document
suspendHistory
(historyName, callback)
Wrap multiple ops in one history state
Document examples
var doc = app.activeDocument;
// Resize image to 1920×1080 at 72dpi bicubic
doc.resizeImage(1920, 1080, 72, ResampleMethod.BICUBIC);
// Expand canvas to 2000px wide, keeping content centered
doc.resizeCanvas(2000, doc.height, AnchorPosition.MIDDLECENTER);
// Crop to a region
doc.crop([100, 100, 900, 600]);
// Trim transparent edges
doc.trim(TrimType.TRANSPARENT, true, true, true, true);
// Flip horizontal
doc.flipCanvas(Direction.HORIZONTAL);
// Change to grayscale
doc.changeMode(ChangeMode.GRAYSCALE);
// Export PNG to filesystem (triggers ZIP download)
var opts = new ExportOptionsSaveForWeb();
opts.format = SaveDocumentType.PNG;
opts.PNG8 = false;
opts.quality = 100;
doc.exportDocument(new File("/output.png"), ExportType.SAVEFORWEB, opts);
// Close without saving
doc.close(SaveOptions.DONOTSAVECHANGES);
Layers / ArtLayers / LayerSets Collections
var doc = app.activeDocument;
// Access
doc.layers // all top-level (art + groups)
doc.artLayers // top-level art layers only
doc.layerSets // top-level group layers only
// By index (0 = topmost)
doc.layers[0]
doc.layers[doc.layers.length - 1] // bottommost
// By name (throws if not found)
doc.layers.getByName("Background")
doc.artLayers.getByName("Logo")
doc.layerSets.getByName("Header Group")
// Add
var newLayer = doc.artLayers.add(); // new blank art layer
var newGroup = doc.layerSets.add(); // new group
var innerLayer = newGroup.artLayers.add(); // layer inside a group
// Remove
doc.artLayers.getByName("Temp").remove();
// Iterate all layers recursively
function walkLayers(parent) {
for (var i = 0; i < parent.layers.length; i++) {
var l = parent.layers[i];
if (l.typename === "LayerSet") walkLayers(l);
else /* ArtLayer */ processLayer(l);
}
}
walkLayers(doc);
ArtLayer — Individual Layer
Properties
Property
Type
R/W
Description
name
string
R/W
Layer name
visible
boolean
R/W
Layer visibility
opacity
number
R/W
Layer opacity 0–100
fillOpacity
number
R
Fill opacity 0–100
blendMode
BlendMode
R/W
Blend mode
kind
LayerKind
R/W
Layer type (can set to LayerKind.TEXT on empty layer)
textItem
TextItem
R
Text object (only when kind === LayerKind.TEXT)
bounds
array
R
[left, top, right, bottom] in current ruler units
parent
Document/LayerSet
R
Containing object
typename
string
R
Always "ArtLayer"
selected
boolean
R
Photopea extension — is layer highlighted in panel
isBackgroundLayer
boolean
R
Is this the locked background layer
grouped
boolean
R
Is clipping mask applied
pixelsLocked
boolean
R
Pixels locked
positionLocked
boolean
R
Position locked
transparentPixelsLocked
boolean
R
Transparent pixels locked
layerMaskDensity
number
R
Layer mask density 0–100
layerMaskFeather
number
R
Layer mask feather 0–250
vectorMaskDensity
number
R
Vector mask density 0–100
vectorMaskFeather
number
R
Vector mask feather 0–250
Transform Methods
Method
Signature
Description
translate
(deltaX, deltaY)
Move layer by offset
rotate
(angle, anchor)
Rotate by degrees. AnchorPosition optional (default center)
For operations not covered by the DOM API, use Action Manager:
// Select a layer by name using AM
function selectLayerByName(name) {
var desc = new ActionDescriptor();
var ref = new ActionReference();
ref.putName(charIDToTypeID("Lyr "), name);
desc.putReference(charIDToTypeID("null"), ref);
desc.putBoolean(charIDToTypeID("MkVs"), false);
executeAction(charIDToTypeID("slct"), desc, DialogModes.NO);
}
// Open Smart Object for editing
var l = doc.layers.getByName("SmartObj");
doc.activeLayer = l;
executeAction(stringIDToTypeID("placedLayerEditContents"));
// Smart Object is now the active document
doc.activeLayer.rotate(90);
doc.save();
doc.close();
// Apply Hue/Saturation as destructive adjustment
var desc = new ActionDescriptor();
var list = new ActionList();
var channel = new ActionDescriptor();
channel.putEnumerated(stringIDToTypeID("presetKind"), stringIDToTypeID("presetKindType"), stringIDToTypeID("presetKindDefault"));
channel.putInteger(stringIDToTypeID("hue"), 20);
channel.putInteger(stringIDToTypeID("saturation"), 30);
channel.putInteger(stringIDToTypeID("lightness"), 0);
list.putObject(stringIDToTypeID("hueSaturationAdjustmentV2Layer"), channel);
desc.putList(stringIDToTypeID("adjustment"), list);
executeAction(stringIDToTypeID("hueSaturation"), desc, DialogModes.NO);
HARD RULE: When editing Smart Objects via AM, always call doc.save(); doc.close(); when done. Failing to do so leaves the SO open and hangs subsequent operations.
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;
for (var i = 0; i < doc.layers.length; i++) {
for (var j = 0; j < doc.layers.length; j++) doc.layers[j].visible = false;
doc.layers[i].visible = true;
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
);
}
for (var i = 0; i < doc.layers.length; i++) doc.layers[i].visible = true;
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;
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;
app.open("https://example.com/watermark.png", null, true);
var wm = doc.activeLayer;
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);
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)));
Pitfalls
Problem
Cause
Fix
createEmbed never resolves
Container has no size
Add width + height CSS to the container <div>
runScript returns ["done"] with no data
No echoToOE in script
Add app.echoToOE(value) for anything you want back
result[0] is "done", not the expected value
echoToOE not reached
Check script logic for early exit or errors
Images won't load (network error)
CORS
Server must respond with Access-Control-Allow-Origin: *
openFromURL(url, true) layer not ready
Async loading lag
Use addImageAndWait utility
exportImage only PNG/JPG
exportImage limitation
Use runScript("saveToOE('webp:0.85')") for other formats
Pixel coordinates behave unexpectedly
Wrong ruler units
Always set app.preferences.rulerUnits = Units.PIXELS first
Text size set but looks different
Wrong type units
Set app.preferences.typeUnits = TypeUnits.PIXELS
Layer not found by name
Wrong layer level
Layers are scoped; use recursive search for nested layers
layer.bounds[0] returns a UnitValue, not number
Ruler units issue
Force Units.PIXELS before reading bounds
Smart Object edit hangs
Missing doc.save(); doc.close()
Always save + close when done editing SO
React double-mount in dev
Strict Mode
Use if (peaRef.current) return guard in useEffect
Script injection via user input
Direct string concatenation
Always serialize dynamic values with JSON.stringify before embedding in runScript strings
Verification
Embed loads: Open your page in a browser. The Photopea iframe should render inside the container <div> with the Photopea UI visible.
Script round-trip works:
const result = await pea.runScript(`app.echoToOE("hello");`);
console.log(result); // → ["hello", "done"]
const result = await pea.runScript(`app.activeDocument.saveToOE("webp:0.85");`);
const webpBlob = new Blob([result[0]], { type: "image/webp" });
console.log(webpBlob.size);
Expected: non-zero size.
React guard check: In React 18 dev mode with Strict Mode, verify only one iframe is created by inspecting the DOM — there should be exactly one iframe inside the container.
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.