photopea-embedded-editor — quality + safety report
In the Skillier index (antigravity__photopea-embedded-editor) · scanned 2026-06-03 · engine: builtin+triage
1 heuristic flag to review
Heuristic flags from the builtin scanner, which is known to over-flag (it trips on legitimate env-reading integrations, security skills, and library .eval calls). This is NOT an authoritative malicious verdict — re-scan with SkillSpector for the authoritative result. Run the authoritative scan →
📇 This skill is in the Skillier index (curated · deduped · quality-filtered). Install Skillier to route & load it into your AI client.
Quality notes
About this skill
Embed Photopea in web apps using photopea.js. Covers embedding, file I/O, scripting, exporting, layers, text, filters, and the full Photoshop-compatible API.
📄 Read the SKILL.md
---
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
## Using photopea.js (yikuansun/PhotopeaAPI) in Websites & Apps
---
## 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.
---
## Library: photopea.js
`photopea.js` is a Promises-based JavaScript wrapper around the Photopea Live Messaging API.
Repository: https://github.com/yikuansun/PhotopeaAPI
npm package: https://www.npmjs.com/package/photopea
### Installation
**CDN (no build step)**
```html
<script src="https://cdn.jsdelivr.net/npm/photopea@1.1.1/dist/photopea.min.js"></script>
```
**Self-hosted**
```html
<script src="./photopea.min.js"></script>
```
**npm (Webpack / Vite / Rollup)**
```bash
npm install photopea
```
```js
import Photopea from "photopea";
```
---
## Core API: The `Photopea` Class
| Method | Description |
|--------|-------------|
| `Photopea.createEmbed(container)` | Creates + injects the iframe, resolves when ready |
| `new Photopea(window.parent)` | Plugin mode: wrap the parent window |
| `pea.runScript(script)` | Run JS string inside Photopea; returns output array |
| `pea.loadAsset(arrayBuffer)` | Load binary file (image, font, brush, etc.) |
| `pea.openFromURL(url, asSmart)` | Open remote URL as new doc or smart object layer |
| `pea.exportImage(type)` | Export current doc; returns `Blob` (`"png"` or `"jpg"`) |
All methods return Promises — always `await` or `.then()`.
---
## Step 1 — Embed
The container `<div>` **must** have a fixed width and height before calling `createEmbed`.
```html
<div id="editor" style="width:1000px; height:650px;"></div>
<script src="https://cdn.jsdelivr.net/npm/photopea@1.1.1/dist/photopea.min.js"></script>
<script>
Photopea.createEmbed(document.getElementById("editor")).then(async (pea) => {
// pea is ready
});
</script>
```
**React:**
```jsx
import { useEffect, useRef } from "react";
import Photopea from "photopea";
export default function Editor() {
const containerRef = useRef(null);
const peaRef = useRef(null);
useEffect(() => {
if (!containerRef.current || peaRef.current) return;
Photopea.createEmbed(containerRef.current).then((pea) => {
peaRef.current = pea;
});
}, []);
return <div ref={containerRef} style={{ width: "100%", height: "650px" }} />;
}
```
---
## Step 2 — Opening Files
```js
// 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 3 — Running Scripts
`runScript` sends a JS string, returns an array of `app.echoToOE(...)` values + `"done"` last.
```js
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]);
```
---
## Step 4 — Exporting
```js
// PNG Blob (via exportImage)
const blob = await pea.exportImage("png");
document.getElementById("preview").src = URL.createObjectURL(blob);
// JPEG Blob
const blob = await pea.exportImage("jpg");
// WebP / PSD / quality-controlled JPEG via saveToOE
const result = await pea.runScript(`app.activeDocument.saveToOE("webp:0.85");`);
const webpBlob = new Blob([result[0]], { type: "image/webp" });
const result = await pea.runScript(`app.activeDocument.saveToOE("psd:true");`);
const psdBlob = new Blob([result[0]], { type: "application/octet-stream" });
// Trigger download
async function download(pea, filename = "export.png") {
const blob = await pea.exportImage("png");
const a = Object.assign(document.createElement("a"), {
href: URL.createObjectURL(blob),
download: filename
});
a.click();
}
```
**Export format strings for `saveToOE`:**
| String | Format |
|--------|--------|
| `"png"` | PNG lossless |
| `"jpg"` | JPEG default |
| `"jpg:0.8"` | JPEG quality 0.0–1.0 |
| `"webp:0.7"` | WebP quality 0.0–1.0 |
| `"psd"` | Full PSD |
| `"psd:true"` | Minified PSD |
| `"svg:true"` | SVG |
---
## Step 5 — Loading Assets
```js
// 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 6 — Plugin Mode
```js
// 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);
```
Plugin config:
```json
{
"environment": {
"plugins": [{
"name": "My Plugin",
"url": "https://my-plugin.example.com",
"icon": "===https://my-plugin.example.com/icon.png"
}]
}
}
```
---
## Utility Patterns
### addImageAndWait — robust async layer insertion
```js
async function addImageAndWait(pea, imgURI) {
let count = "done";
while (count === "done")
count = (await pea.runScript(`app.echoToOE(app.activeDocument.layers.length)`))[0];
count = parseInt(count);
const imageUrlLiteral = JSON.stringify(imgURI);
await pea.runScript(`app.open(${imageUrlLiteral}, null, true);`);
return new Promise((resolve) => {
const check = async () => {
const n = parseInt((await pea.runScript(
`app.echoToOE(app.activeDocument.layers.length)`
))[0]);
n === count + 1 ? resolve() : setTimeout(check, 50);
};
check();
});
}
```
### getDocumentAsImage — returns `<img>` element
```js
async function getDocumentAsImage(pea) {
const result = await pea.runScript(`app.activeDocument.saveToOE('png')`);
return new Promise((resolve) => {
const fr = new FileReader();
fr.addEventListener("load", (e) => {
const img = new Image(); img.src = e.target.result; resolve(img);
});
fr.readAsDataURL(new Blob([result[0]], { type: "image/png" }));
});
}
```
---
## Real-World Patterns
### Pattern A — Open + Export UI
```html
<input type="file" id="fileInput" accept="image/*,.psd">
<button id="exportBtn">Export PNG</button>
<div id="editor" style="width:100%;height:600px;"></div>
<script src="https://cdn.jsdelivr.net/npm/photopea@1.1.1/dist/photopea.min.js"></script>
<script>
let pea;
Photopea.createEmbed(document.getElementById("editor")).then(p => pea = p);
document.getElementById("fileInput").addEventListener("change", async e => {
await pea.loadAsset(await e.target.files[0].arrayBuffer());
});
document.getElementById("exportBtn").addEventListener("click", async () => {
const blob = await pea.exportImage("png");
const a = Object.assign(document.createElement("a"), {
href: URL.createObjectURL(blob), download: "export.png"
});
a.click();
});
</script>
```
### Pattern B — Template + Text Edit + Export
```js
async function generateCard(pea, name, tagline) {
await pea.openFromURL("https://example.com/card.psd", false);
const nameLiteral = JSON.stringify(name);
const taglineLiteral = JSON.stringify(tagline);
await pea.runScript(`
app.activeDocument.layers.getByName("Name").textItem.contents = ${nameLiteral};
app.activeDocument.layers.getByName("Tagline").textItem.contents = ${taglineLiteral};
`);
return await pea.exportImage("png");
}
```
### Pattern C — Batch Watermark
```js
async function batchWatermark(pea, imageURLs, watermarkURL) {
const results = [];
for (const url of imageURLs) {
await pea.openFromURL(url, false);
await pea.openFromURL(watermarkURL, true);
await pea.runScript(`
var doc = app.activeDocument, wm = doc.activeLayer;
wm.translate(doc.width - wm.bounds[2] - 20, doc.height - wm.bounds[3] - 20);
wm.opacity = 70;
`);
results.push(await pea.exportImage("png"));
await pea.runScript(`app.activeDocument.close(SaveOptions.DONOTSAVECHANGES);`);
}
return results;
}
```
---
# FULL SCRIPTING API REFERENCE
> 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.
---
## `app` — Application Object
### Properties
| Property | Type | R/W | Description |
|----------|------|-----|-------------|
| `app.activeDocument` | Document | R/W | The currently active document |
| `app.documents` | Documents | R | Collection of all open documents |
| `app.documents.length` | number | R | Count of open documents |
| `app.documents[i]` | Document | R | Access by zero-based index |
| `app.foregroundColor` | SolidColor | R/W | Current foreground color |
| `app.backgroundColor` | SolidColor | R/W | Current background color |
| `app.preferences.rulerUnits` | Units | R/W | `Units.PIXELS`, `Units.CM`, `Units.INCHES`, `Units.MM`, `Units.PICAS`, `Units.POINTS`, `Units.PERCENT` |
| `app.preferences.typeUnits` | TypeUnits | R/W | `TypeUnits.PIXELS`, `TypeUnits.MM`, `TypeUnits.POINTS` |
| `app.displayDialogs` | DialogModes | R/W | `DialogModes.NO`, `DialogModes.ALL`, `DialogModes.ERROR` |
### Methods
| Method | Description |
|--------|-------------|
| `app.open(url)` | Open URL as new document |
| `app.open(url, null, true)` | Open URL as smart object layer in active document |
| `app.echoToOE(string)` | **Photopea extension** — send string to host page (captured by `runScript`) |
| `app.showWindow("magiccut")` | **Photopea extension** — open Magic Cut panel |
| `app.showWindow("vbitmap")` | **Photopea extension** — open Vectorize Bitmap panel |
| `app.UI.zoomIn()` | Zoom in |
| `app.UI.zoomOut()` | Zoom out |
| `app.UI.fitTheArea()` | Fit canvas to viewport |
| `app.UI.pixelToPixel()` | 100% zoom |
| `app.UI.switchFullscreen()` | Toggle fullscreen |
| `app.UI.scroll(dx, dy)` | Scroll by delta |
| `app.UI.scrollTo(x, y)` | Scroll to absolute position |
**Important:** Always set ruler units to pixels at the start of any script that uses pixel measurements:
```js
var savedUnits = app.preferences.rulerUnits;
app.preferences.rulerUnits = Units.PIXELS;
// ... your code ...
app.preferences.rulerUnits = savedUnits;
```
---
## `Document` — Document Object
Access via `app.activeDocument` or `app.documents[i]`.
### Properties
| Property | Type | R/W | Description |
|----------|------|-----|-------------|
| `width` | number | R | Document width in current ruler units |
| `height` | number | R | Document height in current ruler units |
| `resolutio
… (truncated)Want a live grade + an embeddable README badge? Run your skill through the free scanner.
Graded independently by Skillproof — nothing to sell the author. Quality is mechanical + corpus-grounded; safety flags are heuristic (builtin+triage), not a malicious verdict.