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)
<script src="https://cdn.jsdelivr.net/npm/photopea@1.1.1/dist/photopea.min.js"></script>
Self-hosted
<script src="./photopea.min.js"></script>
npm (Webpack / Vite / Rollup)
npm install photopea
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.
<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:
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
// 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.
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
// 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
// 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
// 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:
{
"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
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
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
<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
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
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:
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 |
resolution |
number |
R |
DPI (pixels per inch) |
name |
string |
R/W |
Photopea extension — display label (no history step) |
source |
string |
R/W |
Photopea extension — file origin URL or "local,X,NAME" |
mode |
DocumentMode |
R |
DocumentMode.RGB, GRAYSCALE, CMYK, LAB, BITMAP, INDEXEDCOLOR, MULTICHANNEL |
bitsPerChannel |
BitsPerChannelType |
R |
BitsPerChannelType.EIGHT, SIXTEEN, THIRTYTWO |
colorProfileName |
string |
R |
Name of embedded color profile |
activeLayer |
Layer/ArtLayer/LayerSet |
R/W |
Set to activate a layer |
currentLayer |
ArtLayer |
R/W |
Alias for activeLayer |
layers |
Layers |
R |
All top-level layers (both art + group) |
artLayers |
ArtLayers |
R |
All top-level art layers only |
layerSets |
LayerSets |
R |
All top-level group layers only |
selection |
Selection |
R |
The current selection |
channels |
Channels |
R |
All channels |
historyStates |
HistoryStates |
R |
Undo history |
activeHistoryState |
HistoryState |
R/W |
Current history position |
layerComps |
LayerComps |
R |
Layer comps collection |
guides |
Guides |
R |
Guides collection |
pathItems |
PathItems |
R |
Vector paths |
id |
number |
R |
Unique document ID |
saved |
boolean |
R |
Whether document has unsaved changes |
quickMaskMode |
boolean |
R |
Whether in Quick Mask mode |
backgroundLayer |
ArtLayer |
R |
The background layer |
pixelAspectRatio |
number |
R |
Custom pixel aspect ratio (0.1–10.0) |
histogram |
array |
R |
256-element histogram array |
Methods
| Method |
Signature |
Description |
resizeImage |
(w, h, res, resampleMethod) |
Resize image pixels. ResampleMethod: BICUBIC, BILINEAR, NEARESTNEIGHBOR, NONE, BICUBICSHARPER, BICUBICSMOOTHER |
resizeCanvas |
(w, h, anchor) |
Resize canvas without scaling. AnchorPosition: TOPLEFT, TOPCENTER, TOPRIGHT, MIDDLELEFT, MIDDLECENTER, MIDDLERIGHT, BOTTOMLEFT, BOTTOMCENTER, BOTTOMRIGHT |
rotateCanvas |
(degrees) |
Rotate entire canvas. Positive = clockwise |
flipCanvas |
(direction) |
Direction.HORIZONTAL or Direction.VERTICAL |
crop |
([x1,y1,x2,y2], angle, w, h) |
Crop canvas. Angle and dimensions are optional |
trim |
(trimType, top, left, bottom, right) |
Trim transparent/background-color borders. TrimType: TRANSPARENT, TOPLEFT, BOTTOMRIGHT |
revealAll |
() |
Expand canvas to show clipped content |
flatten |
() |
Merge all layers into one |
mergeVisibleLayers |
() |
Merge all visible layers |
rasterizeAllLayers |
() |
Rasterize all vector/text layers |
changeMode |
(mode, options) |
Convert color mode (e.g., ChangeMode.GRAYSCALE) |
convertProfile |
(profileName, renderingIntent, blackPointCompensation, dither) |
Convert color profile |
duplicate |
(name, mergedLayers) |
Duplicate the document |
close |
(saveOptions) |
Close document. SaveOptions: DONOTSAVECHANGES, SAVECHANGES, PROMPTTOSAVECHANGES |
save |
() |
Save (requires server config in embed) |
saveToOE |
(format) |
Photopea extension — send binary to host. Formats: "png", "jpg:0.8", "webp:0.7", "psd:true", "svg:true" |
clearHistory |
() |
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 |
Practical 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);
// One undo step for many operations
doc.suspendHistory("Batch Edit", "action");
// (Inside Photopea, all ops become one history state)
// 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
These collections exist on Document, LayerSet (groups within groups), and can be iterated.
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 (see enum below) |
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) |
resize |
(widthPct, heightPct, anchor) |
Scale as percentage of current size |
rasterize |
(target) |
Rasterize. RasterizeType: ENTIRE, FILLCONTENT, LAYERCLIPPINGMASK, LINKEDLAYERS, SHAPE, TEXTCONTENTS, VECTORMASK |
Layer Management Methods
| Method |
Signature |
Description |
duplicate |
() |
Duplicate to same document, returns new layer |
duplicate |
(doc, placement) |
Duplicate to another document |
remove |
() |
Delete the layer |
merge |
() |
Merge down; returns the merged ArtLayer |
move |
(relativeLayer, placement) |
Reorder. ElementPlacement: PLACEBEFORE, PLACEAFTER, PLACEATBEGINNING, PLACEATEND, INSIDE |
copy |
(merged) |
Copy to clipboard |
cut |
() |
Cut to clipboard |
clear |
() |
Cut without clipboard |
Adjustment Methods on ArtLayer
| Method |
Signature |
Description |
adjustBrightnessContrast |
(brightness, contrast) |
Brightness -100–100, Contrast -100–100 |
adjustColorBalance |
(shadows, midtones, highlights, preserveLuminosity) |
Each is [cyan-red, magenta-green, yellow-blue] array |
adjustCurves |
(curveShape) |
Array of [input,output] pairs per channel |
adjustLevels |
(inputRangeStart, inputRangeEnd, gamma, outputRangeStart, outputRangeEnd) |
Levels adjustment |
autoLevels |
() |
Auto levels |
autoContrast |
() |
Auto contrast |
desaturate |
() |
Convert to grayscale values in current mode |
equalize |
() |
Equalize brightness distribution |
invert |
() |
Invert pixel colors |
posterize |
(levels) |
Posterize (2–255 levels) |
threshold |
(level) |
B&W threshold (1–255) |
shadowHighlight |
(shadowAmount, shadowWidth, shadowRadius, highlightAmount, highlightWidth, highlightRadius, colorCorrection, midtoneContrast, blackClip, whiteClip) |
Shadows/Highlights |
photoFilter |
(fillColor, density, luminosity) |
Photo filter |
mixChannels |
(outputChannels, monochrome) |
Channel mixer |
selectiveColor |
(colors, cyan, magenta, yellow, black, method) |
Selective color |
Filter Methods on ArtLayer
| Method |
Signature |
Description |
applyGaussianBlur |
(radius) |
Gaussian blur (0.1–250 px radius) |
applyMotionBlur |
(angle, distance) |
Motion blur |
applyRadialBlur |
(amount, blurMethod, blurQuality) |
Radial blur |
applySmartBlur |
(radius, threshold, blurQuality, blurMode) |
Smart blur |
applyBlur |
() |
Simple blur |
applyBlurMore |
() |
Blur more |
applyUnSharpMask |
(amount, radius, threshold) |
Unsharp mask |
applySharpen |
() |
Sharpen |
applySharpenEdges |
() |
Sharpen edges |
applySharpenMore |
() |
Sharpen more |
applyAddNoise |
(amount, distribution, monochromatic) |
Add noise. NoiseDistribution: GAUSSIAN, UNIFORM |
applyDespeckle |
() |
Despeckle |
applyDustAndScratches |
(radius, threshold) |
Dust and scratches |
applyMedianNoise |
(radius) |
Median noise reduction |
applyMaximum |
(radius) |
Maximum filter (dilate) |
applyMinimum |
(radius) |
Minimum filter (erode) |
applyHighPass |
(radius) |
High pass |
applyOffset |
(horizontal, vertical, undefinedAreas) |
Offset. UndefinedAreas: SETTOBACKGROUND, WRAPAROUND, REPEATEDGEPIXELS |
applyRipple |
(amount, size) |
Ripple. RippleSize: SMALL, MEDIUM, LARGE |
applyWave |
(generators, minWavelength, maxWavelength, minAmplitude, maxAmplitude, horizScale, vertScale, waveType, undefinedAreas, randomSeed) |
Wave filter |
applyZigZag |
(amount, ridges, style) |
Zig-Zag |
applyTwirl |
(angle) |
Twirl |
applyPolarCoordinates |
(conversion) |
Polar coordinates |
applySpherize |
(amount, mode) |
Spherize |
applyPinch |
(amount) |
Pinch (-100–100) |
applyShear |
(curve, undefinedAreas) |
Shear |
applyDisplace |
(horizontalScale, verticalScale, displacementType, undefinedAreas, displacementMapFile) |
Displace |
applyClouds |
() |
Render Clouds |
applyDifferenceClouds |
() |
Difference Clouds |
applyLensFlare |
(brightness, flareCenter, lensType) |
Lens Flare. LensType: ZOOMWIDE, ZOOMNORMAL, MOVIE |
applyDiffuseGlow |
(graininess, glowAmount, clearAmount) |
Diffuse Glow |
applyGlassEffect |
(distortion, smoothness, scaling, invert, texture, textureFile) |
Glass |
applyOceanRipple |
(size, magnitude) |
Ocean Ripple |
applyLensBlur |
(source, focalDistance, invertDepthMap, shape, radius, bladeCurvature, rotation, brightness, threshold, amount, distribution, monochromatic) |
Lens Blur |
applyAverage |
() |
Average blur |
applyDeInterlace |
(eliminateFields, createFields) |
De-interlace |
applyNTSC |
() |
NTSC colors |
applyCustomFilter |
(characteristics, scale, offset) |
Custom filter (5×5 matrix) |
applyTextureFill |
(textureFile) |
Texture fill |
applyStyle |
(styleName) |
Apply a layer style preset by name |
photoFilter |
(fillColor, density, luminosity) |
Photo Filter |
Practical examples:
var layer = app.activeDocument.activeLayer;
// Move to absolute position (layer.bounds[0] = current left edge)
layer.translate(200 - layer.bounds[0], 100 - layer.bounds[1]);
// Rotate 45° around center
layer.rotate(45);
// Scale to 50% keeping center
layer.resize(50, 50, AnchorPosition.MIDDLECENTER);
// Gaussian blur radius 10
layer.applyGaussianBlur(10);
// Unsharp mask
layer.applyUnSharpMask(50, 2, 0);
// Levels: input 0–200, gamma 1.2, output 0–255
layer.adjustLevels(0, 200, 1.2, 0, 255);
// Brightness +20, Contrast +10
layer.adjustBrightnessContrast(20, 10);
// Invert
layer.invert();
// Rasterize text
layer.rasterize(RasterizeType.TEXTCONTENTS);
// Duplicate layer
var copy = layer.duplicate();
copy.name = "Layer Copy";
// Move layer below another
var target = doc.layers.getByName("Background");
layer.move(target, ElementPlacement.PLACEAFTER);
LayerSet — Group Layer
A LayerSet is a folder/group in the Layers panel. It has the same layer management methods as Document.
Properties
| Property |
Type |
R/W |
Description |
name |
string |
R/W |
Group name |
visible |
boolean |
R/W |
Group visibility |
opacity |
number |
R/W |
Group opacity 0–100 |
blendMode |
BlendMode |
R/W |
Group blend mode |
bounds |
array |
R |
Bounding box [left,top,right,bottom] |
layers |
Layers |
R |
All layers inside this group |
artLayers |
ArtLayers |
R |
Art layers inside this group |
layerSets |
LayerSets |
R |
Sub-groups inside this group |
parent |
Document/LayerSet |
R |
Parent container |
typename |
string |
R |
Always "LayerSet" |
Methods
Same as Document for layer management: layers.add(), artLayers.add(), layerSets.add(), .getByName(), plus duplicate(), remove(), move().
// Create group with layers inside
var group = doc.layerSets.add();
group.name = "Product Card";
var bgLayer = group.artLayers.add(); bgLayer.name = "Background";
var textLayer = group.artLayers.add(); textLayer.kind = LayerKind.TEXT;
textLayer.textItem.contents = "Buy Now";
textLayer.textItem.size = 36;
// Collapse/expand group (Photopea specific, not in standard DOM)
// Use visibility as workaround
// Get specific layer inside a group
var innerLayer = doc.layerSets.getByName("Header Group").artLayers.getByName("Title");
TextItem — Text Layer Content
Access via layer.textItem on any layer with layer.kind === LayerKind.TEXT.
Core Properties (most commonly used)
| Property |
Type |
R/W |
Description |
contents |
string |
R/W |
The actual text content |
font |
string |
R/W |
Font PostScript name (e.g. "ArialMT", "Verdana-Bold") |
size |
number |
R/W |
Font size in points |
color |
SolidColor |
R/W |
Text color |
position |
array |
R/W |
[x, y] origin of text (point text) or bounding box top-left |
justification |
Justification |
R/W |
Justification.LEFT, CENTER, RIGHT, FULLJUSTIFY |
kind |
TextType |
R/W |
TextType.POINTTEXT or TextType.PARAGRAPHTEXT |
width |
number |
R/W |
Width of bounding box (paragraph text only) |
height |
number |
R/W |
Height of bounding box (paragraph text only) |
direction |
Direction |
R/W |
Direction.HORIZONTAL or Direction.VERTICAL |
Typography Properties
| Property |
Type |
R/W |
Description |
leading |
number |
R/W |
Line spacing in points |
tracking |
number |
R/W |
Letter spacing -1000–10000 (1000 = 1 em) |
horizontalScale |
number |
R/W |
Horizontal scaling 0–1000% |
verticalScale |
number |
R/W |
Vertical scaling 0–1000% |
baselineShift |
number |
R/W |
Baseline offset in points |
capitalization |
Case |
R/W |
Case.NORMAL, ALLCAPS, SMALLCAPS |
fauxBold |
boolean |
R/W |
Simulated bold |
fauxItalic |
boolean |
R/W |
Simulated italic |
underline |
UnderlineType |
R/W |
UnderlineType.NONE, UNDERLINELEFT, UNDERLINERIGHT |
strikeThru |
StrikeThruType |
R/W |
StrikeThruType.NONE, STRIKEBOX, STRIKEHEIGHT |
antiAliasMethod |
AntiAlias |
R/W |
AntiAlias.NONE, SHARP, CRISP, STRONG, SMOOTH |
autoKerning |
AutoKernType |
R/W |
AutoKernType.MANUAL, METRICS, OPTICAL |
language |
Language |
R/W |
Language.ENGLISH, etc. |
ligatures |
boolean |
R/W |
Enable ligatures |
alternateLigatures |
boolean |
R/W |
Enable alternate ligatures |
oldStyle |
boolean |
R/W |
Old-style numerals |
noBreak |
boolean |
R/W |
Prevent line breaks in this text |
useAutoLeading |
boolean |
R/W |
Use font's built-in leading |
autoLeadingAmount |
number |
R/W |
Auto leading percentage 0.01–5000 |
hyphenation |
boolean |
R/W |
Enable hyphenation |
Paragraph Properties
| Property |
Type |
R/W |
Description |
leftIndent |
number |
R/W |
Left indent -1296–1296 |
rightIndent |
number |
R/W |
Right indent -1296–1296 |
firstLineIndent |
number |
R/W |
First line indent -1296–1296 |
spaceBefore |
number |
R/W |
Space before paragraph -1296–1296 |
spaceAfter |
number |
R/W |
Space after paragraph -1296–1296 |
hangingPuntuation |
boolean |
R/W |
Roman hanging punctuation |
textComposer |
TextComposer |
R/W |
TextComposer.ADOBEEVERYLINE, ADOBESINGLELINE |
Warp Properties
| Property |
Type |
R/W |
Description |
warpStyle |
WarpStyle |
R/W |
WarpStyle.NONE, ARC, ARCH, BULGE, SHELLLOWER, SHELLUPPER, FLAG, WAVE, FISH, RISE, FISHEYE, INFLATE, SQUEEZE, TWIST |
warpDirection |
Direction |
R/W |
Direction.HORIZONTAL or Direction.VERTICAL |
warpBend |
number |
R/W |
Warp bend -100–100 |
warpHorizontalDistortion |
number |
R/W |
Horizontal distortion -100–100 |
warpVerticalDistortion |
number |
R/W |
Vertical distortion -100–100 |
Photopea Extensions
| Property |
Type |
Description |
totalTextStyle |
string |
JSON string with ALL style parameters of the text |
transform |
string |
JSON array — the affine transform matrix of the text |
Methods
| Method |
Description |
convertToShape() |
Convert text to a filled shape layer with text as clipping path |
createPath() |
Create work path from text outlines |
Practical examples:
var layer = doc.layers.getByName("Headline");
var text = layer.textItem;
// Set content
text.contents = "Hello World";
// Style
text.font = "Verdana-Bold";
text.size = 72;
text.color.rgb.hexValue = "FF0000"; // red
// Position point text at (50, 100)
text.position = [50, 100];
// Center align
text.justification = Justification.CENTER;
// Paragraph text with bounding box
text.kind = TextType.PARAGRAPHTEXT;
text.width = new UnitValue("400 pixels");
text.height = new UnitValue("200 pixels");
// Letter spacing
text.tracking = 100; // 10% spacing
// Scale text horizontally to 80%
text.horizontalScale = 80;
// Warp arc
text.warpStyle = WarpStyle.ARC;
text.warpBend = 30;
// Read all text styles as JSON (Photopea extension)
var styles = JSON.parse(text.totalTextStyle);
app.echoToOE(JSON.stringify(styles));
Creating a Text Layer from Scratch
app.preferences.rulerUnits = Units.PIXELS;
var layer = doc.artLayers.add();
layer.kind = LayerKind.TEXT; // Convert blank layer to text
layer.name = "My Title";
var text = layer.textItem;
text.contents = "Welcome";
text.font = "ArialMT";
text.size = 48;
text.justification = Justification.CENTER;
text.position = [doc.width / 2, 100];
var color = new SolidColor();
color.rgb.red = 255;
color.rgb.green = 255;
color.rgb.blue = 255;
text.color = color;
SolidColor — Color Object
// RGB (most common in Photopea)
var c = new SolidColor();
c.rgb.red = 255; // 0–255
c.rgb.green = 128;
c.rgb.blue = 0;
c.rgb.hexValue = "FF8000"; // Set via hex string (no #)
// CMYK
var c2 = new SolidColor();
c2.cmyk.cyan = 0; // 0–100
c2.cmyk.magenta = 50;
c2.cmyk.yellow = 100;
c2.cmyk.black = 0;
// Grayscale
var c3 = new SolidColor();
c3.gray.gray = 50; // 0–100
// HSB
var c4 = new SolidColor();
c4.hsb.hue = 30; // 0–360
c4.hsb.saturation = 100; // 0–100
c4.hsb.brightness = 100; // 0–100
// Lab
var c5 = new SolidColor();
c5.lab.l = 50; // 0–100
c5.lab.a = 20; // -128–127
c5.lab.b = 40; // -128–127
// Set as foreground color
app.foregroundColor = c;
// Use with selection fill
doc.selection.selectAll();
doc.selection.fill(c);
doc.selection.deselect();
Selection — Selection Object
Access via doc.selection.
Properties
| Property |
Type |
Description |
bounds |
array |
[left, top, right, bottom] bounding rectangle |
solid |
boolean |
Whether selection is a solid rectangle |
Methods
| Method |
Signature |
Description |
selectAll |
() |
Select entire document |
deselect |
() |
Remove selection |
invert |
() |
Invert the selection |
select |
(region, type, feather, antiAlias) |
Select polygon region. Region is array of [x,y] points. SelectionType: REPLACE, ADD, SUBTRACT, INTERSECT |
feather |
(radius) |
Feather the selection edges |
contract |
(radius) |
Contract (shrink) selection |
expand |
(radius) |
Expand selection |
grow |
(tolerance, antiAlias) |
Grow selection to similar adjacent pixels |
similar |
(tolerance, antiAlias) |
Select similar pixels throughout document |
smooth |
(radius) |
Smooth selection edges |
selectBorder |
(width) |
Select only the border of the current selection |
resize |
(widthPct, heightPct, anchor) |
Resize selection boundary |
rotate |
(angle, anchor) |
Rotate selection boundary |
translate |
(deltaX, deltaY) |
Move selection boundary |
fill |
(fillWith, mode, opacity, preserveTransparency) |
Fill selection with color or content. fillWith is SolidColor or string |
stroke |
(strokeColor, width, location, mode, opacity, preserveTransparency) |
Stroke selection border. StrokeLocation: INSIDE, OUTSIDE, CENTER |
copy |
(merged) |
Copy selection to clipboard |
cut |
() |
Cut selection to clipboard |
clear |
() |
Delete selection content |
load |
(from, type, invert) |
Load selection from channel |
store |
(into, type) |
Save selection as channel |
makeWorkPath |
(tolerance) |
Convert to work path |
Practical examples:
var sel = doc.selection;
// Rectangle select (top-left to bottom-right)
sel.select([[0,0],[500,0],[500,300],[0,300]]);
// Select all
sel.selectAll();
// Add to existing selection
sel.select([[600,0],[900,0],[900,300],[600,300]], SelectionType.ADD);
// Feather 10px
sel.feather(10);
// Contract by 5px
sel.contract(5);
// Fill with red
var red = new SolidColor();
red.rgb.red = 255; red.rgb.green = 0; red.rgb.blue = 0;
sel.fill(red);
// Stroke selection with black, 3px, inside
var black = new SolidColor();
black.rgb.hexValue = "000000";
sel.stroke(black, 3, StrokeLocation.INSIDE);
// Copy, paste as new layer
sel.copy();
doc.paste();
// Invert and delete (remove background)
sel.invert();
sel.clear();
sel.deselect();
BlendMode Enum — All Values
Used in layer.blendMode (string form in Photopea) and BlendMode constant (standard):
BlendMode Constant |
Photopea String |
Name |
BlendMode.NORMAL |
"norm" |
Normal |
BlendMode.DISSOLVE |
"diss" |
Dissolve |
BlendMode.DARKEN |
"dark" |
Darken |
BlendMode.MULTIPLY |
"mul " |
Multiply |
BlendMode.COLORBURN |
"idiv" |
Color Burn |
BlendMode.LINEARBURN |
"lbrn" |
Linear Burn |
BlendMode.DARKERCOLOR |
"dkCl" |
Darker Color |
BlendMode.LIGHTEN |
"lite" |
Lighten |
BlendMode.SCREEN |
"scrn" |
Screen |
BlendMode.COLORDODGE |
"div " |
Color Dodge |
BlendMode.LINEARDODGE |
"lddg" |
Linear Dodge (Add) |
BlendMode.LIGHTERCOLOR |
"lgCl" |
Lighter Color |
BlendMode.OVERLAY |
"over" |
Overlay |
BlendMode.SOFTLIGHT |
"sLit" |
Soft Light |
BlendMode.HARDLIGHT |
"hLit" |
Hard Light |
BlendMode.VIVIDLIGHT |
"vLit" |
Vivid Light |
BlendMode.LINEARLIGHT |
"lLit" |
Linear Light |
BlendMode.PINLIGHT |
"pLit" |
Pin Light |
BlendMode.HARDMIX |
"hMix" |
Hard Mix |
BlendMode.DIFFERENCE |
"diff" |
Difference |
BlendMode.EXCLUSION |
"smud" |
Exclusion |
BlendMode.SUBTRACT |
"fsub" |
Subtract |
BlendMode.DIVIDE |
"fdiv" |
Divide |
BlendMode.HUE |
"hue " |
Hue |
BlendMode.SATURATION |
"sat " |
Saturation |
BlendMode.COLOR |
"colr" |
Color |
BlendMode.LUMINOSITY |
"lum " |
Luminosity |
BlendMode.PASSTHROUGH |
"pass" |
Pass Through (groups only) |
// Use either form:
layer.blendMode = BlendMode.SCREEN; // constant
layer.blendMode = "scrn"; // string (Photopea internal form)
LayerKind Enum
| Constant |
Description |
LayerKind.NORMAL |
Regular pixel layer |
LayerKind.TEXT |
Text layer |
LayerKind.SMARTOBJECT |
Smart Object / linked layer |
LayerKind.SOLIDFILL |
Solid color fill layer |
LayerKind.GRADIENTFILL |
Gradient fill layer |
LayerKind.PATTERNFILL |
Pattern fill layer |
LayerKind.BRIGHTNESSCONTRAST |
Brightness/Contrast adjustment layer |
LayerKind.CURVES |
Curves adjustment layer |
LayerKind.LEVELS |
Levels adjustment layer |
LayerKind.HUESATURATION |
Hue/Saturation adjustment layer |
LayerKind.COLORBALANCE |
Color Balance adjustment layer |
LayerKind.CHANNELMIXER |
Channel Mixer adjustment layer |
LayerKind.GRADIENTMAP |
Gradient Map adjustment layer |
LayerKind.INVERSION |
Invert adjustment layer |
LayerKind.POSTERIZE |
Posterize adjustment layer |
LayerKind.THRESHOLD |
Threshold adjustment layer |
LayerKind.SELECTIVECOLOR |
Selective Color adjustment layer |
LayerKind.PHOTOFILTER |
Photo Filter adjustment layer |
LayerKind.EXPOSURE |
Exposure adjustment layer |
LayerKind.VIBRANCE |
Vibrance adjustment layer |
LayerKind.COLORLOOKUP |
Color Lookup adjustment layer |
LayerKind.LAYER3D |
3D layer (not generally useful in Photopea) |
LayerKind.VIDEO |
Video layer |
// Identify layer type
var layer = doc.activeLayer;
if (layer.kind === LayerKind.TEXT) /* text layer */;
if (layer.kind === LayerKind.SMARTOBJECT) /* smart object */;
if (layer.typename === "LayerSet") /* group */;
// Filter: collect all text layers recursively
var textLayers = [];
function collectText(parent) {
for (var i = 0; i < parent.layers.length; i++) {
var l = parent.layers[i];
if (l.typename === "LayerSet") collectText(l);
else if (l.kind === LayerKind.TEXT) textLayers.push(l);
}
}
collectText(doc);
AnchorPosition Enum
| Constant |
Position |
AnchorPosition.TOPLEFT |
Top left |
AnchorPosition.TOPCENTER |
Top center |
AnchorPosition.TOPRIGHT |
Top right |
AnchorPosition.MIDDLELEFT |
Middle left |
AnchorPosition.MIDDLECENTER |
Center |
AnchorPosition.MIDDLERIGHT |
Middle right |
AnchorPosition.BOTTOMLEFT |
Bottom left |
AnchorPosition.BOTTOMCENTER |
Bottom center |
AnchorPosition.BOTTOMRIGHT |
Bottom right |
ElementPlacement Enum
Used with layer.move(relativeObject, placement):
| Constant |
Effect |
ElementPlacement.PLACEBEFORE |
Above the target layer in the p |
…(truncated)
1---2name: photopea-embedded-editor3description: Embed Photopea in web apps using photopea.js. Covers embedding, file I/O, scripting, exporting, layers, text, filters, and the full Photoshop-compatible API.4license: MIT5---67# Photopea Embedded Editor Skill8## Using photopea.js (yikuansun/PhotopeaAPI) in Websites & Apps910---1112## When to Use This Skill1314Use this skill for **every task** that involves:15- Embedding Photopea as an image editor inside a webpage or web app16- Controlling an embedded Photopea instance from your JavaScript code17- Automating image editing workflows from a host page (open files, run scripts, export results)18- Building an image editing feature into your product using Photopea as the engine19- Writing scripts to manipulate documents, layers, text, selections, filters, colors, and paths2021**Do NOT** use raw `postMessage` wiring — always use `photopea.js` as the wrapper.2223---2425## Library: photopea.js2627`photopea.js` is a Promises-based JavaScript wrapper around the Photopea Live Messaging API.28Repository: https://github.com/yikuansun/PhotopeaAPI29npm package: https://www.npmjs.com/package/photopea3031### Installation3233**CDN (no build step)**34```html35<script src="https://cdn.jsdelivr.net/npm/photopea@1.1.1/dist/photopea.min.js"></script>36```3738**Self-hosted**39```html40<script src="./photopea.min.js"></script>41```4243**npm (Webpack / Vite / Rollup)**44```bash45npm install photopea46```47```js48import Photopea from "photopea";49```5051---5253## Core API: The `Photopea` Class5455| Method | Description |56|--------|-------------|57| `Photopea.createEmbed(container)` | Creates + injects the iframe, resolves when ready |58| `new Photopea(window.parent)` | Plugin mode: wrap the parent window |59| `pea.runScript(script)` | Run JS string inside Photopea; returns output array |60| `pea.loadAsset(arrayBuffer)` | Load binary file (image, font, brush, etc.) |61| `pea.openFromURL(url, asSmart)` | Open remote URL as new doc or smart object layer |62| `pea.exportImage(type)` | Export current doc; returns `Blob` (`"png"` or `"jpg"`) |6364All methods return Promises — always `await` or `.then()`.6566---6768## Step 1 — Embed6970The container `<div>` **must** have a fixed width and height before calling `createEmbed`.7172```html73<div id="editor" style="width:1000px; height:650px;"></div>74<script src="https://cdn.jsdelivr.net/npm/photopea@1.1.1/dist/photopea.min.js"></script>75<script>76 Photopea.createEmbed(document.getElementById("editor")).then(async (pea) => {77 // pea is ready78 });79</script>80```8182**React:**83```jsx84import { useEffect, useRef } from "react";85import Photopea from "photopea";8687export default function Editor() {88 const containerRef = useRef(null);89 const peaRef = useRef(null);9091 useEffect(() => {92 if (!containerRef.current || peaRef.current) return;93 Photopea.createEmbed(containerRef.current).then((pea) => {94 peaRef.current = pea;95 });96 }, []);9798 return <div ref={containerRef} style={{ width: "100%", height: "650px" }} />;99}100```101102---103104## Step 2 — Opening Files105106```js107// Remote URL → new document108await pea.openFromURL("https://example.com/design.psd", false);109110// Remote URL → smart object layer inside current document111await pea.openFromURL("https://example.com/overlay.png", true);112113// Local file (user input → ArrayBuffer → loadAsset)114document.getElementById("fileInput").addEventListener("change", async (e) => {115 const buf = await e.target.files[0].arrayBuffer();116 await pea.loadAsset(buf);117});118119// Base64 data URI via runScript120await pea.runScript(`app.open("data:image/png;base64,iVBORw0...");`);121```122123---124125## Step 3 — Running Scripts126127`runScript` sends a JS string, returns an array of `app.echoToOE(...)` values + `"done"` last.128129```js130const result = await pea.runScript(`app.echoToOE("hello");`);131// result → ["hello", "done"]132133// Return structured data134const out = await pea.runScript(`135 app.echoToOE(JSON.stringify({136 width: app.activeDocument.width,137 height: app.activeDocument.height,138 layers: app.activeDocument.layers.length139 }));140`);141const info = JSON.parse(out[0]);142```143144---145146## Step 4 — Exporting147148```js149// PNG Blob (via exportImage)150const blob = await pea.exportImage("png");151document.getElementById("preview").src = URL.createObjectURL(blob);152153// JPEG Blob154const blob = await pea.exportImage("jpg");155156// WebP / PSD / quality-controlled JPEG via saveToOE157const result = await pea.runScript(`app.activeDocument.saveToOE("webp:0.85");`);158const webpBlob = new Blob([result[0]], { type: "image/webp" });159160const result = await pea.runScript(`app.activeDocument.saveToOE("psd:true");`);161const psdBlob = new Blob([result[0]], { type: "application/octet-stream" });162163// Trigger download164async function download(pea, filename = "export.png") {165 const blob = await pea.exportImage("png");166 const a = Object.assign(document.createElement("a"), {167 href: URL.createObjectURL(blob),168 download: filename169 });170 a.click();171}172```173174**Export format strings for `saveToOE`:**175176| String | Format |177|--------|--------|178| `"png"` | PNG lossless |179| `"jpg"` | JPEG default |180| `"jpg:0.8"` | JPEG quality 0.0–1.0 |181| `"webp:0.7"` | WebP quality 0.0–1.0 |182| `"psd"` | Full PSD |183| `"psd:true"` | Minified PSD |184| `"svg:true"` | SVG |185186---187188## Step 5 — Loading Assets189190```js191// Font192const buf = await (await fetch("https://example.com/MyFont.otf")).arrayBuffer();193await pea.loadAsset(buf);194// Now usable in textItem.font195196// Brush197await pea.loadAsset(await (await fetch("Nature.ABR")).arrayBuffer());198199// Gradient200await pea.loadAsset(await (await fetch("Gradients.GRD")).arrayBuffer());201```202203---204205## Step 6 — Plugin Mode206207```js208// Your page is inside Photopea's sidebar iframe209const pea = new Photopea(window.parent);210211const out = await pea.runScript(`app.echoToOE(app.activeDocument.width);`);212console.log("Width:", out[0]);213214// Load an asset from your plugin215const buf = await (await fetch("https://my-assets.com/sticker.png")).arrayBuffer();216await pea.loadAsset(buf);217```218219Plugin config:220```json221{222 "environment": {223 "plugins": [{224 "name": "My Plugin",225 "url": "https://my-plugin.example.com",226 "icon": "===https://my-plugin.example.com/icon.png"227 }]228 }229}230```231232---233234## Utility Patterns235236### addImageAndWait — robust async layer insertion237```js238async function addImageAndWait(pea, imgURI) {239 let count = "done";240 while (count === "done")241 count = (await pea.runScript(`app.echoToOE(app.activeDocument.layers.length)`))[0];242 count = parseInt(count);243244 const imageUrlLiteral = JSON.stringify(imgURI);245 await pea.runScript(`app.open(${imageUrlLiteral}, null, true);`);246247 return new Promise((resolve) => {248 const check = async () => {249 const n = parseInt((await pea.runScript(250 `app.echoToOE(app.activeDocument.layers.length)`251 ))[0]);252 n === count + 1 ? resolve() : setTimeout(check, 50);253 };254 check();255 });256}257```258259### getDocumentAsImage — returns `<img>` element260```js261async function getDocumentAsImage(pea) {262 const result = await pea.runScript(`app.activeDocument.saveToOE('png')`);263 return new Promise((resolve) => {264 const fr = new FileReader();265 fr.addEventListener("load", (e) => {266 const img = new Image(); img.src = e.target.result; resolve(img);267 });268 fr.readAsDataURL(new Blob([result[0]], { type: "image/png" }));269 });270}271```272273---274275## Real-World Patterns276277### Pattern A — Open + Export UI278```html279<input type="file" id="fileInput" accept="image/*,.psd">280<button id="exportBtn">Export PNG</button>281<div id="editor" style="width:100%;height:600px;"></div>282<script src="https://cdn.jsdelivr.net/npm/photopea@1.1.1/dist/photopea.min.js"></script>283<script>284let pea;285Photopea.createEmbed(document.getElementById("editor")).then(p => pea = p);286287document.getElementById("fileInput").addEventListener("change", async e => {288 await pea.loadAsset(await e.target.files[0].arrayBuffer());289});290document.getElementById("exportBtn").addEventListener("click", async () => {291 const blob = await pea.exportImage("png");292 const a = Object.assign(document.createElement("a"), {293 href: URL.createObjectURL(blob), download: "export.png"294 });295 a.click();296});297</script>298```299300### Pattern B — Template + Text Edit + Export301```js302async function generateCard(pea, name, tagline) {303 await pea.openFromURL("https://example.com/card.psd", false);304 const nameLiteral = JSON.stringify(name);305 const taglineLiteral = JSON.stringify(tagline);306 await pea.runScript(`307 app.activeDocument.layers.getByName("Name").textItem.contents = ${nameLiteral};308 app.activeDocument.layers.getByName("Tagline").textItem.contents = ${taglineLiteral};309 `);310 return await pea.exportImage("png");311}312```313314### Pattern C — Batch Watermark315```js316async function batchWatermark(pea, imageURLs, watermarkURL) {317 const results = [];318 for (const url of imageURLs) {319 await pea.openFromURL(url, false);320 await pea.openFromURL(watermarkURL, true);321 await pea.runScript(`322 var doc = app.activeDocument, wm = doc.activeLayer;323 wm.translate(doc.width - wm.bounds[2] - 20, doc.height - wm.bounds[3] - 20);324 wm.opacity = 70;325 `);326 results.push(await pea.exportImage("png"));327 await pea.runScript(`app.activeDocument.close(SaveOptions.DONOTSAVECHANGES);`);328 }329 return results;330}331```332333---334335# FULL SCRIPTING API REFERENCE336337> All code in this section runs **inside `pea.runScript("...")`** strings.338> Photopea implements the Adobe Photoshop CC 2015 JavaScript scripting interface.339> Any Photoshop script targeting that version should work in Photopea.340341---342343## `app` — Application Object344345### Properties346347| Property | Type | R/W | Description |348|----------|------|-----|-------------|349| `app.activeDocument` | Document | R/W | The currently active document |350| `app.documents` | Documents | R | Collection of all open documents |351| `app.documents.length` | number | R | Count of open documents |352| `app.documents[i]` | Document | R | Access by zero-based index |353| `app.foregroundColor` | SolidColor | R/W | Current foreground color |354| `app.backgroundColor` | SolidColor | R/W | Current background color |355| `app.preferences.rulerUnits` | Units | R/W | `Units.PIXELS`, `Units.CM`, `Units.INCHES`, `Units.MM`, `Units.PICAS`, `Units.POINTS`, `Units.PERCENT` |356| `app.preferences.typeUnits` | TypeUnits | R/W | `TypeUnits.PIXELS`, `TypeUnits.MM`, `TypeUnits.POINTS` |357| `app.displayDialogs` | DialogModes | R/W | `DialogModes.NO`, `DialogModes.ALL`, `DialogModes.ERROR` |358359### Methods360361| Method | Description |362|--------|-------------|363| `app.open(url)` | Open URL as new document |364| `app.open(url, null, true)` | Open URL as smart object layer in active document |365| `app.echoToOE(string)` | **Photopea extension** — send string to host page (captured by `runScript`) |366| `app.showWindow("magiccut")` | **Photopea extension** — open Magic Cut panel |367| `app.showWindow("vbitmap")` | **Photopea extension** — open Vectorize Bitmap panel |368| `app.UI.zoomIn()` | Zoom in |369| `app.UI.zoomOut()` | Zoom out |370| `app.UI.fitTheArea()` | Fit canvas to viewport |371| `app.UI.pixelToPixel()` | 100% zoom |372| `app.UI.switchFullscreen()` | Toggle fullscreen |373| `app.UI.scroll(dx, dy)` | Scroll by delta |374| `app.UI.scrollTo(x, y)` | Scroll to absolute position |375376**Important:** Always set ruler units to pixels at the start of any script that uses pixel measurements:377```js378var savedUnits = app.preferences.rulerUnits;379app.preferences.rulerUnits = Units.PIXELS;380// ... your code ...381app.preferences.rulerUnits = savedUnits;382```383384---385386## `Document` — Document Object387388Access via `app.activeDocument` or `app.documents[i]`.389390### Properties391392| Property | Type | R/W | Description |393|----------|------|-----|-------------|394| `width` | number | R | Document width in current ruler units |395| `height` | number | R | Document height in current ruler units |396| `resolution` | number | R | DPI (pixels per inch) |397| `name` | string | **R/W** | **Photopea extension** — display label (no history step) |398| `source` | string | **R/W** | **Photopea extension** — file origin URL or `"local,X,NAME"` |399| `mode` | DocumentMode | R | `DocumentMode.RGB`, `GRAYSCALE`, `CMYK`, `LAB`, `BITMAP`, `INDEXEDCOLOR`, `MULTICHANNEL` |400| `bitsPerChannel` | BitsPerChannelType | R | `BitsPerChannelType.EIGHT`, `SIXTEEN`, `THIRTYTWO` |401| `colorProfileName` | string | R | Name of embedded color profile |402| `activeLayer` | Layer/ArtLayer/LayerSet | R/W | Set to activate a layer |403| `currentLayer` | ArtLayer | R/W | Alias for `activeLayer` |404| `layers` | Layers | R | All top-level layers (both art + group) |405| `artLayers` | ArtLayers | R | All top-level art layers only |406| `layerSets` | LayerSets | R | All top-level group layers only |407| `selection` | Selection | R | The current selection |408| `channels` | Channels | R | All channels |409| `historyStates` | HistoryStates | R | Undo history |410| `activeHistoryState` | HistoryState | R/W | Current history position |411| `layerComps` | LayerComps | R | Layer comps collection |412| `guides` | Guides | R | Guides collection |413| `pathItems` | PathItems | R | Vector paths |414| `id` | number | R | Unique document ID |415| `saved` | boolean | R | Whether document has unsaved changes |416| `quickMaskMode` | boolean | R | Whether in Quick Mask mode |417| `backgroundLayer` | ArtLayer | R | The background layer |418| `pixelAspectRatio` | number | R | Custom pixel aspect ratio (0.1–10.0) |419| `histogram` | array | R | 256-element histogram array |420421### Methods422423| Method | Signature | Description |424|--------|-----------|-------------|425| `resizeImage` | `(w, h, res, resampleMethod)` | Resize image pixels. ResampleMethod: `BICUBIC`, `BILINEAR`, `NEARESTNEIGHBOR`, `NONE`, `BICUBICSHARPER`, `BICUBICSMOOTHER` |426| `resizeCanvas` | `(w, h, anchor)` | Resize canvas without scaling. AnchorPosition: `TOPLEFT`, `TOPCENTER`, `TOPRIGHT`, `MIDDLELEFT`, `MIDDLECENTER`, `MIDDLERIGHT`, `BOTTOMLEFT`, `BOTTOMCENTER`, `BOTTOMRIGHT` |427| `rotateCanvas` | `(degrees)` | Rotate entire canvas. Positive = clockwise |428| `flipCanvas` | `(direction)` | `Direction.HORIZONTAL` or `Direction.VERTICAL` |429| `crop` | `([x1,y1,x2,y2], angle, w, h)` | Crop canvas. Angle and dimensions are optional |430| `trim` | `(trimType, top, left, bottom, right)` | Trim transparent/background-color borders. TrimType: `TRANSPARENT`, `TOPLEFT`, `BOTTOMRIGHT` |431| `revealAll` | `()` | Expand canvas to show clipped content |432| `flatten` | `()` | Merge all layers into one |433| `mergeVisibleLayers` | `()` | Merge all visible layers |434| `rasterizeAllLayers` | `()` | Rasterize all vector/text layers |435| `changeMode` | `(mode, options)` | Convert color mode (e.g., `ChangeMode.GRAYSCALE`) |436| `convertProfile` | `(profileName, renderingIntent, blackPointCompensation, dither)` | Convert color profile |437| `duplicate` | `(name, mergedLayers)` | Duplicate the document |438| `close` | `(saveOptions)` | Close document. SaveOptions: `DONOTSAVECHANGES`, `SAVECHANGES`, `PROMPTTOSAVECHANGES` |439| `save` | `()` | Save (requires server config in embed) |440| `saveToOE` | `(format)` | **Photopea extension** — send binary to host. Formats: `"png"`, `"jpg:0.8"`, `"webp:0.7"`, `"psd:true"`, `"svg:true"` |441| `clearHistory` | `()` | **Photopea extension** — clear undo history to free RAM |442| `exportDocument` | `(file, exportType, options)` | Export to filesystem (triggers ZIP). ExportType: `SAVEFORWEB` |443| `paste` | `(intoSelection)` | Paste clipboard into document |444| `suspendHistory` | `(historyName, callback)` | Wrap multiple ops in one history state |445446**Practical examples:**447```js448var doc = app.activeDocument;449450// Resize image to 1920×1080 at 72dpi bicubic451doc.resizeImage(1920, 1080, 72, ResampleMethod.BICUBIC);452453// Expand canvas to 2000px wide, keeping content centered454doc.resizeCanvas(2000, doc.height, AnchorPosition.MIDDLECENTER);455456// Crop to a region457doc.crop([100, 100, 900, 600]);458459// Trim transparent edges460doc.trim(TrimType.TRANSPARENT, true, true, true, true);461462// Flip horizontal463doc.flipCanvas(Direction.HORIZONTAL);464465// Change to grayscale466doc.changeMode(ChangeMode.GRAYSCALE);467468// One undo step for many operations469doc.suspendHistory("Batch Edit", "action");470// (Inside Photopea, all ops become one history state)471472// Export PNG to filesystem (triggers ZIP download)473var opts = new ExportOptionsSaveForWeb();474opts.format = SaveDocumentType.PNG;475opts.PNG8 = false;476opts.quality = 100;477doc.exportDocument(new File("/output.png"), ExportType.SAVEFORWEB, opts);478479// Close without saving480doc.close(SaveOptions.DONOTSAVECHANGES);481```482483---484485## `Layers` / `ArtLayers` / `LayerSets` Collections486487These collections exist on `Document`, `LayerSet` (groups within groups), and can be iterated.488489```js490var doc = app.activeDocument;491492// Access493doc.layers // all top-level (art + groups)494doc.artLayers // top-level art layers only495doc.layerSets // top-level group layers only496497// By index (0 = topmost)498doc.layers[0]499doc.layers[doc.layers.length - 1] // bottommost500501// By name (throws if not found)502doc.layers.getByName("Background")503doc.artLayers.getByName("Logo")504doc.layerSets.getByName("Header Group")505506// Add507var newLayer = doc.artLayers.add(); // new blank art layer508var newGroup = doc.layerSets.add(); // new group509var innerLayer = newGroup.artLayers.add(); // layer inside a group510511// Remove512doc.artLayers.getByName("Temp").remove();513514// Iterate all layers recursively515function walkLayers(parent) {516 for (var i = 0; i < parent.layers.length; i++) {517 var l = parent.layers[i];518 if (l.typename === "LayerSet") walkLayers(l);519 else /* ArtLayer */ processLayer(l);520 }521}522walkLayers(doc);523```524525---526527## `ArtLayer` — Individual Layer528529### Properties530531| Property | Type | R/W | Description |532|----------|------|-----|-------------|533| `name` | string | R/W | Layer name |534| `visible` | boolean | R/W | Layer visibility |535| `opacity` | number | R/W | Layer opacity 0–100 |536| `fillOpacity` | number | R | Fill opacity 0–100 |537| `blendMode` | BlendMode | R/W | Blend mode (see enum below) |538| `kind` | LayerKind | R/W | Layer type (can set to `LayerKind.TEXT` on empty layer) |539| `textItem` | TextItem | R | Text object (only when `kind === LayerKind.TEXT`) |540| `bounds` | array | R | `[left, top, right, bottom]` in current ruler units |541| `parent` | Document/LayerSet | R | Containing object |542| `typename` | string | R | Always `"ArtLayer"` |543| `selected` | boolean | R | **Photopea extension** — is layer highlighted in panel |544| `isBackgroundLayer` | boolean | R | Is this the locked background layer |545| `grouped` | boolean | R | Is clipping mask applied |546| `pixelsLocked` | boolean | R | Pixels locked |547| `positionLocked` | boolean | R | Position locked |548| `transparentPixelsLocked` | boolean | R | Transparent pixels locked |549| `layerMaskDensity` | number | R | Layer mask density 0–100 |550| `layerMaskFeather` | number | R | Layer mask feather 0–250 |551| `vectorMaskDensity` | number | R | Vector mask density 0–100 |552| `vectorMaskFeather` | number | R | Vector mask feather 0–250 |553554### Transform Methods555556| Method | Signature | Description |557|--------|-----------|-------------|558| `translate` | `(deltaX, deltaY)` | Move layer by offset |559| `rotate` | `(angle, anchor)` | Rotate by degrees. AnchorPosition optional (default center) |560| `resize` | `(widthPct, heightPct, anchor)` | Scale as percentage of current size |561| `rasterize` | `(target)` | Rasterize. RasterizeType: `ENTIRE`, `FILLCONTENT`, `LAYERCLIPPINGMASK`, `LINKEDLAYERS`, `SHAPE`, `TEXTCONTENTS`, `VECTORMASK` |562563### Layer Management Methods564565| Method | Signature | Description |566|--------|-----------|-------------|567| `duplicate` | `()` | Duplicate to same document, returns new layer |568| `duplicate` | `(doc, placement)` | Duplicate to another document |569| `remove` | `()` | Delete the layer |570| `merge` | `()` | Merge down; returns the merged ArtLayer |571| `move` | `(relativeLayer, placement)` | Reorder. ElementPlacement: `PLACEBEFORE`, `PLACEAFTER`, `PLACEATBEGINNING`, `PLACEATEND`, `INSIDE` |572| `copy` | `(merged)` | Copy to clipboard |573| `cut` | `()` | Cut to clipboard |574| `clear` | `()` | Cut without clipboard |575576### Adjustment Methods on ArtLayer577578| Method | Signature | Description |579|--------|-----------|-------------|580| `adjustBrightnessContrast` | `(brightness, contrast)` | Brightness -100–100, Contrast -100–100 |581| `adjustColorBalance` | `(shadows, midtones, highlights, preserveLuminosity)` | Each is `[cyan-red, magenta-green, yellow-blue]` array |582| `adjustCurves` | `(curveShape)` | Array of `[input,output]` pairs per channel |583| `adjustLevels` | `(inputRangeStart, inputRangeEnd, gamma, outputRangeStart, outputRangeEnd)` | Levels adjustment |584| `autoLevels` | `()` | Auto levels |585| `autoContrast` | `()` | Auto contrast |586| `desaturate` | `()` | Convert to grayscale values in current mode |587| `equalize` | `()` | Equalize brightness distribution |588| `invert` | `()` | Invert pixel colors |589| `posterize` | `(levels)` | Posterize (2–255 levels) |590| `threshold` | `(level)` | B&W threshold (1–255) |591| `shadowHighlight` | `(shadowAmount, shadowWidth, shadowRadius, highlightAmount, highlightWidth, highlightRadius, colorCorrection, midtoneContrast, blackClip, whiteClip)` | Shadows/Highlights |592| `photoFilter` | `(fillColor, density, luminosity)` | Photo filter |593| `mixChannels` | `(outputChannels, monochrome)` | Channel mixer |594| `selectiveColor` | `(colors, cyan, magenta, yellow, black, method)` | Selective color |595596### Filter Methods on ArtLayer597598| Method | Signature | Description |599|--------|-----------|-------------|600| `applyGaussianBlur` | `(radius)` | Gaussian blur (0.1–250 px radius) |601| `applyMotionBlur` | `(angle, distance)` | Motion blur |602| `applyRadialBlur` | `(amount, blurMethod, blurQuality)` | Radial blur |603| `applySmartBlur` | `(radius, threshold, blurQuality, blurMode)` | Smart blur |604| `applyBlur` | `()` | Simple blur |605| `applyBlurMore` | `()` | Blur more |606| `applyUnSharpMask` | `(amount, radius, threshold)` | Unsharp mask |607| `applySharpen` | `()` | Sharpen |608| `applySharpenEdges` | `()` | Sharpen edges |609| `applySharpenMore` | `()` | Sharpen more |610| `applyAddNoise` | `(amount, distribution, monochromatic)` | Add noise. NoiseDistribution: `GAUSSIAN`, `UNIFORM` |611| `applyDespeckle` | `()` | Despeckle |612| `applyDustAndScratches` | `(radius, threshold)` | Dust and scratches |613| `applyMedianNoise` | `(radius)` | Median noise reduction |614| `applyMaximum` | `(radius)` | Maximum filter (dilate) |615| `applyMinimum` | `(radius)` | Minimum filter (erode) |616| `applyHighPass` | `(radius)` | High pass |617| `applyOffset` | `(horizontal, vertical, undefinedAreas)` | Offset. UndefinedAreas: `SETTOBACKGROUND`, `WRAPAROUND`, `REPEATEDGEPIXELS` |618| `applyRipple` | `(amount, size)` | Ripple. RippleSize: `SMALL`, `MEDIUM`, `LARGE` |619| `applyWave` | `(generators, minWavelength, maxWavelength, minAmplitude, maxAmplitude, horizScale, vertScale, waveType, undefinedAreas, randomSeed)` | Wave filter |620| `applyZigZag` | `(amount, ridges, style)` | Zig-Zag |621| `applyTwirl` | `(angle)` | Twirl |622| `applyPolarCoordinates` | `(conversion)` | Polar coordinates |623| `applySpherize` | `(amount, mode)` | Spherize |624| `applyPinch` | `(amount)` | Pinch (-100–100) |625| `applyShear` | `(curve, undefinedAreas)` | Shear |626| `applyDisplace` | `(horizontalScale, verticalScale, displacementType, undefinedAreas, displacementMapFile)` | Displace |627| `applyClouds` | `()` | Render Clouds |628| `applyDifferenceClouds` | `()` | Difference Clouds |629| `applyLensFlare` | `(brightness, flareCenter, lensType)` | Lens Flare. LensType: `ZOOMWIDE, ZOOMNORMAL, MOVIE` |630| `applyDiffuseGlow` | `(graininess, glowAmount, clearAmount)` | Diffuse Glow |631| `applyGlassEffect` | `(distortion, smoothness, scaling, invert, texture, textureFile)` | Glass |632| `applyOceanRipple` | `(size, magnitude)` | Ocean Ripple |633| `applyLensBlur` | `(source, focalDistance, invertDepthMap, shape, radius, bladeCurvature, rotation, brightness, threshold, amount, distribution, monochromatic)` | Lens Blur |634| `applyAverage` | `()` | Average blur |635| `applyDeInterlace` | `(eliminateFields, createFields)` | De-interlace |636| `applyNTSC` | `()` | NTSC colors |637| `applyCustomFilter` | `(characteristics, scale, offset)` | Custom filter (5×5 matrix) |638| `applyTextureFill` | `(textureFile)` | Texture fill |639| `applyStyle` | `(styleName)` | Apply a layer style preset by name |640| `photoFilter` | `(fillColor, density, luminosity)` | Photo Filter |641642**Practical examples:**643```js644var layer = app.activeDocument.activeLayer;645646// Move to absolute position (layer.bounds[0] = current left edge)647layer.translate(200 - layer.bounds[0], 100 - layer.bounds[1]);648649// Rotate 45° around center650layer.rotate(45);651652// Scale to 50% keeping center653layer.resize(50, 50, AnchorPosition.MIDDLECENTER);654655// Gaussian blur radius 10656layer.applyGaussianBlur(10);657658// Unsharp mask659layer.applyUnSharpMask(50, 2, 0);660661// Levels: input 0–200, gamma 1.2, output 0–255662layer.adjustLevels(0, 200, 1.2, 0, 255);663664// Brightness +20, Contrast +10665layer.adjustBrightnessContrast(20, 10);666667// Invert668layer.invert();669670// Rasterize text671layer.rasterize(RasterizeType.TEXTCONTENTS);672673// Duplicate layer674var copy = layer.duplicate();675copy.name = "Layer Copy";676677// Move layer below another678var target = doc.layers.getByName("Background");679layer.move(target, ElementPlacement.PLACEAFTER);680```681682---683684## `LayerSet` — Group Layer685686A LayerSet is a folder/group in the Layers panel. It has the same layer management methods as `Document`.687688### Properties689690| Property | Type | R/W | Description |691|----------|------|-----|-------------|692| `name` | string | R/W | Group name |693| `visible` | boolean | R/W | Group visibility |694| `opacity` | number | R/W | Group opacity 0–100 |695| `blendMode` | BlendMode | R/W | Group blend mode |696| `bounds` | array | R | Bounding box `[left,top,right,bottom]` |697| `layers` | Layers | R | All layers inside this group |698| `artLayers` | ArtLayers | R | Art layers inside this group |699| `layerSets` | LayerSets | R | Sub-groups inside this group |700| `parent` | Document/LayerSet | R | Parent container |701| `typename` | string | R | Always `"LayerSet"` |702703### Methods704Same as Document for layer management: `layers.add()`, `artLayers.add()`, `layerSets.add()`, `.getByName()`, plus `duplicate()`, `remove()`, `move()`.705706```js707// Create group with layers inside708var group = doc.layerSets.add();709group.name = "Product Card";710711var bgLayer = group.artLayers.add(); bgLayer.name = "Background";712var textLayer = group.artLayers.add(); textLayer.kind = LayerKind.TEXT;713textLayer.textItem.contents = "Buy Now";714textLayer.textItem.size = 36;715716// Collapse/expand group (Photopea specific, not in standard DOM)717// Use visibility as workaround718719// Get specific layer inside a group720var innerLayer = doc.layerSets.getByName("Header Group").artLayers.getByName("Title");721```722723---724725## `TextItem` — Text Layer Content726727Access via `layer.textItem` on any layer with `layer.kind === LayerKind.TEXT`.728729### Core Properties (most commonly used)730731| Property | Type | R/W | Description |732|----------|------|-----|-------------|733| `contents` | string | R/W | The actual text content |734| `font` | string | R/W | Font PostScript name (e.g. `"ArialMT"`, `"Verdana-Bold"`) |735| `size` | number | R/W | Font size in points |736| `color` | SolidColor | R/W | Text color |737| `position` | array | R/W | `[x, y]` origin of text (point text) or bounding box top-left |738| `justification` | Justification | R/W | `Justification.LEFT`, `CENTER`, `RIGHT`, `FULLJUSTIFY` |739| `kind` | TextType | R/W | `TextType.POINTTEXT` or `TextType.PARAGRAPHTEXT` |740| `width` | number | R/W | Width of bounding box (paragraph text only) |741| `height` | number | R/W | Height of bounding box (paragraph text only) |742| `direction` | Direction | R/W | `Direction.HORIZONTAL` or `Direction.VERTICAL` |743744### Typography Properties745746| Property | Type | R/W | Description |747|----------|------|-----|-------------|748| `leading` | number | R/W | Line spacing in points |749| `tracking` | number | R/W | Letter spacing -1000–10000 (1000 = 1 em) |750| `horizontalScale` | number | R/W | Horizontal scaling 0–1000% |751| `verticalScale` | number | R/W | Vertical scaling 0–1000% |752| `baselineShift` | number | R/W | Baseline offset in points |753| `capitalization` | Case | R/W | `Case.NORMAL`, `ALLCAPS`, `SMALLCAPS` |754| `fauxBold` | boolean | R/W | Simulated bold |755| `fauxItalic` | boolean | R/W | Simulated italic |756| `underline` | UnderlineType | R/W | `UnderlineType.NONE`, `UNDERLINELEFT`, `UNDERLINERIGHT` |757| `strikeThru` | StrikeThruType | R/W | `StrikeThruType.NONE`, `STRIKEBOX`, `STRIKEHEIGHT` |758| `antiAliasMethod` | AntiAlias | R/W | `AntiAlias.NONE`, `SHARP`, `CRISP`, `STRONG`, `SMOOTH` |759| `autoKerning` | AutoKernType | R/W | `AutoKernType.MANUAL`, `METRICS`, `OPTICAL` |760| `language` | Language | R/W | `Language.ENGLISH`, etc. |761| `ligatures` | boolean | R/W | Enable ligatures |762| `alternateLigatures` | boolean | R/W | Enable alternate ligatures |763| `oldStyle` | boolean | R/W | Old-style numerals |764| `noBreak` | boolean | R/W | Prevent line breaks in this text |765| `useAutoLeading` | boolean | R/W | Use font's built-in leading |766| `autoLeadingAmount` | number | R/W | Auto leading percentage 0.01–5000 |767| `hyphenation` | boolean | R/W | Enable hyphenation |768769### Paragraph Properties770771| Property | Type | R/W | Description |772|----------|------|-----|-------------|773| `leftIndent` | number | R/W | Left indent -1296–1296 |774| `rightIndent` | number | R/W | Right indent -1296–1296 |775| `firstLineIndent` | number | R/W | First line indent -1296–1296 |776| `spaceBefore` | number | R/W | Space before paragraph -1296–1296 |777| `spaceAfter` | number | R/W | Space after paragraph -1296–1296 |778| `hangingPuntuation` | boolean | R/W | Roman hanging punctuation |779| `textComposer` | TextComposer | R/W | `TextComposer.ADOBEEVERYLINE`, `ADOBESINGLELINE` |780781### Warp Properties782783| Property | Type | R/W | Description |784|----------|------|-----|-------------|785| `warpStyle` | WarpStyle | R/W | `WarpStyle.NONE`, `ARC`, `ARCH`, `BULGE`, `SHELLLOWER`, `SHELLUPPER`, `FLAG`, `WAVE`, `FISH`, `RISE`, `FISHEYE`, `INFLATE`, `SQUEEZE`, `TWIST` |786| `warpDirection` | Direction | R/W | `Direction.HORIZONTAL` or `Direction.VERTICAL` |787| `warpBend` | number | R/W | Warp bend -100–100 |788| `warpHorizontalDistortion` | number | R/W | Horizontal distortion -100–100 |789| `warpVerticalDistortion` | number | R/W | Vertical distortion -100–100 |790791### Photopea Extensions792793| Property | Type | Description |794|----------|------|-------------|795| `totalTextStyle` | string | JSON string with ALL style parameters of the text |796| `transform` | string | JSON array — the affine transform matrix of the text |797798### Methods799800| Method | Description |801|--------|-------------|802| `convertToShape()` | Convert text to a filled shape layer with text as clipping path |803| `createPath()` | Create work path from text outlines |804805**Practical examples:**806```js807var layer = doc.layers.getByName("Headline");808var text = layer.textItem;809810// Set content811text.contents = "Hello World";812813// Style814text.font = "Verdana-Bold";815text.size = 72;816text.color.rgb.hexValue = "FF0000"; // red817818// Position point text at (50, 100)819text.position = [50, 100];820821// Center align822text.justification = Justification.CENTER;823824// Paragraph text with bounding box825text.kind = TextType.PARAGRAPHTEXT;826text.width = new UnitValue("400 pixels");827text.height = new UnitValue("200 pixels");828829// Letter spacing830text.tracking = 100; // 10% spacing831832// Scale text horizontally to 80%833text.horizontalScale = 80;834835// Warp arc836text.warpStyle = WarpStyle.ARC;837text.warpBend = 30;838839// Read all text styles as JSON (Photopea extension)840var styles = JSON.parse(text.totalTextStyle);841app.echoToOE(JSON.stringify(styles));842```843844---845846## Creating a Text Layer from Scratch847848```js849app.preferences.rulerUnits = Units.PIXELS;850851var layer = doc.artLayers.add();852layer.kind = LayerKind.TEXT; // Convert blank layer to text853layer.name = "My Title";854855var text = layer.textItem;856text.contents = "Welcome";857text.font = "ArialMT";858text.size = 48;859text.justification = Justification.CENTER;860text.position = [doc.width / 2, 100];861862var color = new SolidColor();863color.rgb.red = 255;864color.rgb.green = 255;865color.rgb.blue = 255;866text.color = color;867```868869---870871## `SolidColor` — Color Object872873```js874// RGB (most common in Photopea)875var c = new SolidColor();876c.rgb.red = 255; // 0–255877c.rgb.green = 128;878c.rgb.blue = 0;879c.rgb.hexValue = "FF8000"; // Set via hex string (no #)880881// CMYK882var c2 = new SolidColor();883c2.cmyk.cyan = 0; // 0–100884c2.cmyk.magenta = 50;885c2.cmyk.yellow = 100;886c2.cmyk.black = 0;887888// Grayscale889var c3 = new SolidColor();890c3.gray.gray = 50; // 0–100891892// HSB893var c4 = new SolidColor();894c4.hsb.hue = 30; // 0–360895c4.hsb.saturation = 100; // 0–100896c4.hsb.brightness = 100; // 0–100897898// Lab899var c5 = new SolidColor();900c5.lab.l = 50; // 0–100901c5.lab.a = 20; // -128–127902c5.lab.b = 40; // -128–127903904// Set as foreground color905app.foregroundColor = c;906907// Use with selection fill908doc.selection.selectAll();909doc.selection.fill(c);910doc.selection.deselect();911```912913---914915## `Selection` — Selection Object916917Access via `doc.selection`.918919### Properties920921| Property | Type | Description |922|----------|------|-------------|923| `bounds` | array | `[left, top, right, bottom]` bounding rectangle |924| `solid` | boolean | Whether selection is a solid rectangle |925926### Methods927928| Method | Signature | Description |929|--------|-----------|-------------|930| `selectAll` | `()` | Select entire document |931| `deselect` | `()` | Remove selection |932| `invert` | `()` | Invert the selection |933| `select` | `(region, type, feather, antiAlias)` | Select polygon region. Region is array of `[x,y]` points. SelectionType: `REPLACE`, `ADD`, `SUBTRACT`, `INTERSECT` |934| `feather` | `(radius)` | Feather the selection edges |935| `contract` | `(radius)` | Contract (shrink) selection |936| `expand` | `(radius)` | Expand selection |937| `grow` | `(tolerance, antiAlias)` | Grow selection to similar adjacent pixels |938| `similar` | `(tolerance, antiAlias)` | Select similar pixels throughout document |939| `smooth` | `(radius)` | Smooth selection edges |940| `selectBorder` | `(width)` | Select only the border of the current selection |941| `resize` | `(widthPct, heightPct, anchor)` | Resize selection boundary |942| `rotate` | `(angle, anchor)` | Rotate selection boundary |943| `translate` | `(deltaX, deltaY)` | Move selection boundary |944| `fill` | `(fillWith, mode, opacity, preserveTransparency)` | Fill selection with color or content. fillWith is SolidColor or string |945| `stroke` | `(strokeColor, width, location, mode, opacity, preserveTransparency)` | Stroke selection border. StrokeLocation: `INSIDE`, `OUTSIDE`, `CENTER` |946| `copy` | `(merged)` | Copy selection to clipboard |947| `cut` | `()` | Cut selection to clipboard |948| `clear` | `()` | Delete selection content |949| `load` | `(from, type, invert)` | Load selection from channel |950| `store` | `(into, type)` | Save selection as channel |951| `makeWorkPath` | `(tolerance)` | Convert to work path |952953**Practical examples:**954```js955var sel = doc.selection;956957// Rectangle select (top-left to bottom-right)958sel.select([[0,0],[500,0],[500,300],[0,300]]);959960// Select all961sel.selectAll();962963// Add to existing selection964sel.select([[600,0],[900,0],[900,300],[600,300]], SelectionType.ADD);965966// Feather 10px967sel.feather(10);968969// Contract by 5px970sel.contract(5);971972// Fill with red973var red = new SolidColor();974red.rgb.red = 255; red.rgb.green = 0; red.rgb.blue = 0;975sel.fill(red);976977// Stroke selection with black, 3px, inside978var black = new SolidColor();979black.rgb.hexValue = "000000";980sel.stroke(black, 3, StrokeLocation.INSIDE);981982// Copy, paste as new layer983sel.copy();984doc.paste();985986// Invert and delete (remove background)987sel.invert();988sel.clear();989sel.deselect();990```991992---993994## `BlendMode` Enum — All Values995996Used in `layer.blendMode` (string form in Photopea) and `BlendMode` constant (standard):997998| `BlendMode` Constant | Photopea String | Name |999|----------------------|-----------------|------|1000| `BlendMode.NORMAL` | `"norm"` | Normal |1001| `BlendMode.DISSOLVE` | `"diss"` | Dissolve |1002| `BlendMode.DARKEN` | `"dark"` | Darken |1003| `BlendMode.MULTIPLY` | `"mul "` | Multiply |1004| `BlendMode.COLORBURN` | `"idiv"` | Color Burn |1005| `BlendMode.LINEARBURN` | `"lbrn"` | Linear Burn |1006| `BlendMode.DARKERCOLOR` | `"dkCl"` | Darker Color |1007| `BlendMode.LIGHTEN` | `"lite"` | Lighten |1008| `BlendMode.SCREEN` | `"scrn"` | Screen |1009| `BlendMode.COLORDODGE` | `"div "` | Color Dodge |1010| `BlendMode.LINEARDODGE` | `"lddg"` | Linear Dodge (Add) |1011| `BlendMode.LIGHTERCOLOR` | `"lgCl"` | Lighter Color |1012| `BlendMode.OVERLAY` | `"over"` | Overlay |1013| `BlendMode.SOFTLIGHT` | `"sLit"` | Soft Light |1014| `BlendMode.HARDLIGHT` | `"hLit"` | Hard Light |1015| `BlendMode.VIVIDLIGHT` | `"vLit"` | Vivid Light |1016| `BlendMode.LINEARLIGHT` | `"lLit"` | Linear Light |1017| `BlendMode.PINLIGHT` | `"pLit"` | Pin Light |1018| `BlendMode.HARDMIX` | `"hMix"` | Hard Mix |1019| `BlendMode.DIFFERENCE` | `"diff"` | Difference |1020| `BlendMode.EXCLUSION` | `"smud"` | Exclusion |1021| `BlendMode.SUBTRACT` | `"fsub"` | Subtract |1022| `BlendMode.DIVIDE` | `"fdiv"` | Divide |1023| `BlendMode.HUE` | `"hue "` | Hue |1024| `BlendMode.SATURATION` | `"sat "` | Saturation |1025| `BlendMode.COLOR` | `"colr"` | Color |1026| `BlendMode.LUMINOSITY` | `"lum "` | Luminosity |1027| `BlendMode.PASSTHROUGH` | `"pass"` | Pass Through (groups only) |10281029```js1030// Use either form:1031layer.blendMode = BlendMode.SCREEN; // constant1032layer.blendMode = "scrn"; // string (Photopea internal form)1033```10341035---10361037## `LayerKind` Enum10381039| Constant | Description |1040|----------|-------------|1041| `LayerKind.NORMAL` | Regular pixel layer |1042| `LayerKind.TEXT` | Text layer |1043| `LayerKind.SMARTOBJECT` | Smart Object / linked layer |1044| `LayerKind.SOLIDFILL` | Solid color fill layer |1045| `LayerKind.GRADIENTFILL` | Gradient fill layer |1046| `LayerKind.PATTERNFILL` | Pattern fill layer |1047| `LayerKind.BRIGHTNESSCONTRAST` | Brightness/Contrast adjustment layer |1048| `LayerKind.CURVES` | Curves adjustment layer |1049| `LayerKind.LEVELS` | Levels adjustment layer |1050| `LayerKind.HUESATURATION` | Hue/Saturation adjustment layer |1051| `LayerKind.COLORBALANCE` | Color Balance adjustment layer |1052| `LayerKind.CHANNELMIXER` | Channel Mixer adjustment layer |1053| `LayerKind.GRADIENTMAP` | Gradient Map adjustment layer |1054| `LayerKind.INVERSION` | Invert adjustment layer |1055| `LayerKind.POSTERIZE` | Posterize adjustment layer |1056| `LayerKind.THRESHOLD` | Threshold adjustment layer |1057| `LayerKind.SELECTIVECOLOR` | Selective Color adjustment layer |1058| `LayerKind.PHOTOFILTER` | Photo Filter adjustment layer |1059| `LayerKind.EXPOSURE` | Exposure adjustment layer |1060| `LayerKind.VIBRANCE` | Vibrance adjustment layer |1061| `LayerKind.COLORLOOKUP` | Color Lookup adjustment layer |1062| `LayerKind.LAYER3D` | 3D layer (not generally useful in Photopea) |1063| `LayerKind.VIDEO` | Video layer |10641065```js1066// Identify layer type1067var layer = doc.activeLayer;1068if (layer.kind === LayerKind.TEXT) /* text layer */;1069if (layer.kind === LayerKind.SMARTOBJECT) /* smart object */;1070if (layer.typename === "LayerSet") /* group */;10711072// Filter: collect all text layers recursively1073var textLayers = [];1074function collectText(parent) {1075 for (var i = 0; i < parent.layers.length; i++) {1076 var l = parent.layers[i];1077 if (l.typename === "LayerSet") collectText(l);1078 else if (l.kind === LayerKind.TEXT) textLayers.push(l);1079 }1080}1081collectText(doc);1082```10831084---10851086## `AnchorPosition` Enum10871088| Constant | Position |1089|----------|----------|1090| `AnchorPosition.TOPLEFT` | Top left |1091| `AnchorPosition.TOPCENTER` | Top center |1092| `AnchorPosition.TOPRIGHT` | Top right |1093| `AnchorPosition.MIDDLELEFT` | Middle left |1094| `AnchorPosition.MIDDLECENTER` | Center |1095| `AnchorPosition.MIDDLERIGHT` | Middle right |1096| `AnchorPosition.BOTTOMLEFT` | Bottom left |1097| `AnchorPosition.BOTTOMCENTER` | Bottom center |1098| `AnchorPosition.BOTTOMRIGHT` | Bottom right |10991100---11011102## `ElementPlacement` Enum11031104Used with `layer.move(relativeObject, placement)`:11051106| Constant | Effect |1107|----------|--------|1108| `ElementPlacement.PLACEBEFORE` | Above the target layer in the p11091110…(truncated)