Comprehensive reference for UXP (Unified Extensibility Platform) plugins targeting Photoshop. Based on UXP 9.2.0 / Manifest v5 / Photoshop 27.4 (February 2026).
UXP is not a browser — it has a custom layout engine, limited DOM, and no WebGL/Workers. Don't assume web platform features work.
- Panel: Persistent UI docked in Photoshop. Lifecycle:
create(rootNode),show(rootNode, data),hide(rootNode, data),destroy(rootNode),invokeMenu(menuId). Size viaminimumSize,maximumSize,preferredDockedSize,preferredFloatingSize. - Command: Headless execution from menu. Lifecycle:
run(),cancel(). No persistent UI.
A single plugin can declare multiple entry points of mixed types.
Standalone .psjs files run from File > Scripts > Browse, drag-and-drop onto Photoshop, or an Action step — no manifest required. Scope notes:
- Lifetime ends when the script finishes. Dialog UI only (no panels); no plugin-data folder and no
localStorage. Permissions are PS-managed: file pickers + temporary folder, fonts (read), clipboard (read/write) — no network, launchProcess, WebView, or IPC. - Scripts execute in an executeAsModal-like context: DOM writes and batchPlay run without wrapping.
require('uxp').script.executionContextexposeshostControl(suspend/resume history, auto-close documents) andonCancel; PS shows a progress bar after ~2-3 s; top-levelawaitworks;awaitany modal UI or it won't appear. - Scripts can't invoke other scripts, can't be launched from plugins, take no arguments, and
recordActionis incompatible (no plugin environment holds the handler). Double-click file association is documented as "coming soon", not shipped.
mainfield in manifest points to an HTML file (e.g.,index.html)- Photoshop API:
require('photoshop')— submodules:.app,.core,.action,.imaging,.constants - Plugins loaded/debugged through UXP Developer Tool (UDT): Load, Reload, Watch, Unload
- Panel
create/show/hidehave a 300ms default timeout for promise resolution (manifest v5+)
{
"manifestVersion": 5,
"id": "com.example.myplugin",
"name": "My Plugin",
"version": "1.0.0",
"main": "index.html",
"host": {
"app": "PS",
"minVersion": "23.3.0",
"data": { "apiVersion": 2, "loadEvent": "use", "enableMenuRecording": true }
},
"featureFlags": { "enableSWCSupport": true, "CSSNextSupport": true },
"entrypoints": [
{
"type": "panel",
"id": "mainPanel",
"label": { "default": "My Plugin" },
"minimumSize": { "width": 230, "height": 230 },
"preferredDockedSize": { "width": 300, "height": 350 },
"preferredFloatingSize": { "width": 400, "height": 450 },
"icons": [
{ "width": 23, "height": 23, "path": "icons/icon-light.png", "scale": [1, 2], "theme": ["light", "lightest"] },
{ "width": 23, "height": 23, "path": "icons/icon-dark.png", "scale": [1, 2], "theme": ["dark", "darkest"] }
]
}
],
"icons": [
{ "width": 48, "height": 48, "path": "icons/plugin-icon.png", "scale": [1, 2], "theme": ["darkest", "dark", "medium", "light", "lightest"], "species": ["generic"] }
],
"requiredPermissions": {
"network": { "domains": [] },
"clipboard": "readAndWrite",
"localFileSystem": "request"
}
}| Permission | Values | Notes |
|---|---|---|
network.domains |
string[] | HTTPS domains the plugin can access |
clipboard |
"readAndWrite" | "read" |
Clipboard access level |
localFileSystem |
"request" | "plugin" | "fullAccess" |
File system access |
launchProcess |
{ schemes, extensions } |
Required for openExternal/openPath |
ipc |
{ enablePluginCommunication } |
Inter-plugin messaging |
webview |
{ domains } |
WebView in panels (v6.4+) and dialogs (v6.0+). Domains optional since v9.0. allow field removed in UXP 9.1 / PS 27.4. |
enableUserInfo |
boolean | User GUID access (PS 25.2, UXP 7.3+) |
Feature flags: CSSNextSupport enables box-shadow, transform-origin, scaleX, scaleY, translate. Can be true or an array (["boxShadow", "transformFunctions", "transformProperties"]) for selective enablement. enableSWCSupport enables Spectrum Web Components (npm-wrapped SWC packages, locked to v0.37.0 in UXP 8.0) and implicitly enables CSSNextSupport. Manifest v5 requires PS 23.3.0+ / UXP 6.0+.
entrypoints.setup({
plugin: {
create() { /* plugin loaded */ },
destroy() { /* plugin unloaded */ }
},
panels: {
mainPanel: {
create(rootNode) { /* panel DOM created */ },
show(rootNode, data) { /* panel becomes visible */ },
hide(rootNode, data) { /* panel hidden */ },
destroy(rootNode) { /* panel destroyed */ },
invokeMenu(menuId) { /* flyout menu item clicked */ }
}
}
});Callbacks can return Promises. create, show, and hide have a documented 300ms default timeout (manifest v5+); destroy and invokeMenu have no documented timeout.
Known issues (PS-57284): show fires only once (not on each re-show). hide never fires. Don't rely on either for refresh gating.
| Property | Description | Notes |
|---|---|---|
minimumSize |
Smallest allowed dimensions | Host may not honor in all docking configs |
maximumSize |
Largest allowed dimensions | Same caveat |
preferredDockedSize |
Initial size when docked | Preference, not guaranteed |
preferredFloatingSize |
Initial size when floating | Preference, not guaranteed |
Panel scrolling via CSS overflow: auto/scroll. Does NOT auto-scroll to keep focused controls visible on macOS. Panel icons required for marketplace: 23x23 with theme variants.
Photoshop CSS variables update with theme (Darkest, Dark, Light, Lightest):
Colors: --uxp-host-background-color, --uxp-host-text-color, --uxp-host-border-color, --uxp-host-link-text-color, --uxp-host-text-color-secondary, --uxp-host-link-hover-text-color, --uxp-host-label-text-color, --uxp-host-widget-hover-background-color, --uxp-host-widget-hover-text-color, --uxp-host-widget-hover-border-color
Font sizes: --uxp-host-font-size, --uxp-host-font-size-smaller, --uxp-host-font-size-larger
body {
background-color: var(--uxp-host-background-color);
color: var(--uxp-host-text-color);
font-size: var(--uxp-host-font-size);
}Use @media (prefers-color-scheme: dark/light) for coarse theme detection. Spectrum UXP components are automatically theme-aware.
Everything lives in the plugin's •••/Actions menu in UDT:
- Load — workspace plugins don't auto-load into the host; Load launches the development copy (a marketplace copy can coexist). Success shows a green toast; failure shows red with a Details link to the host logs.
- Silent failure mode: if Photoshop is in a modal state (open dialog, uncommitted crop), the plugin won't load and you get no message either way. Exit the modal state and it loads.
- Reload — reloads code only, not
manifest.json. After manifest changes, use Load again. - Watch/Unwatch — auto-reload on file changes.
- Debug — customized Chrome DevTools (breakpoints, console, source maps work; editing CSS in the property inspector doesn't). Logs jumps straight to the console.
- Package — writes the
.ccx, always named after the plugin ID. Open Folder reveals the source. - Options: "break on start" pauses in the debugger at load; Advanced > build folder points UDT at a bundler's output directory (React etc.), relative to
manifest.json.
No built-in i18n API — branch on the host locale and set strings yourself (innerText/innerHTML):
const { uiLocale, name, version } = require('uxp').host; // "en_US", "photoshop", "27.0.0"
const platform = require('os').platform(); // a method, not a property: "darwin" | "windows".ccx format via UDT (Package from the ••• actions menu next to the plugin name). Marketplace: submit through the Developer Distribution portal to the Creative Cloud Marketplace — users install from the CC desktop app. Direct: share the .ccx; users double-click it to install via the Creative Cloud app (not UDT). Development: load unpacked folder, Watch mode auto-reloads.
Properties:
| Property | Type | R/W | Min Ver | Notes |
|---|---|---|---|---|
id |
number | R | 22.5 | Unique document ID |
name |
string | R | 23.0 | With extension |
title |
string | R | 22.5 | Document title |
path |
string | R | 22.5 | Full file path; identifier for cloud docs |
width / height |
number | R | 22.5 | Canvas dimensions in pixels |
resolution |
number | R | 22.5 | PPI |
mode |
DocumentMode | R | 23.0 | RGB, CMYK, Grayscale, Lab, etc. |
colorProfileName |
string | R/W | 23.0 | Returns "None" when type not CUSTOM/WORKING |
colorProfileType |
ColorProfileType | R/W | 23.0 | WORKING / CUSTOM / NONE |
bitsPerChannel |
BitsPerChannelType | R/W | 23.0 | 8, 16, or 32 |
pixelAspectRatio |
number | R/W | 22.5 | Non-square pixel ratio |
activeLayers |
Layers | R/W | 22.5 | Selected layers (R/W since 26.9) |
layers |
Layers | R | 22.5 | Top-level layer collection |
backgroundLayer |
Layer | R | 22.5 | Background layer if present |
artboards |
Layers | R | 22.5 | Artboards in document |
activeHistoryBrushSource |
HistoryState | R/W | 22.5 | History brush source |
activeHistoryState |
HistoryState | R/W | 22.5 | Current history state |
historyStates |
HistoryStates | R | 22.5 | History state collection |
activeChannels |
Channel[] | R/W | 23.0 | Currently active channels |
channels |
Channels | R | 23.0 | All channels in document |
componentChannels |
Channel[] | R | 24.5 | Component channels |
colorSamplers |
ColorSamplers | R | 24.0 | Color sampler points |
countItems |
CountItems | R | 24.1 | Count items |
guides |
Guides | R | 23.0 | Document guides |
pathItems |
PathItems | R | 23.3 | Vector paths |
layerComps |
LayerComps | R | 24.0 | Saved layer comp states |
selection |
Selection | R | — | Selection object (no explicit minver; Selection class is 25.0) |
histogram |
number[] | R | 23.0 | 256-entry; only valid for RGB/CMYK/INDEXEDCOLOR |
quickMaskMode |
boolean | R/W | 23.0 | True if app is in Quick Mask mode |
cloudDocument |
boolean | R | 23.0 | True if Photoshop cloud document |
cloudWorkAreaDirectory |
string | R | 23.0 | Local dir for cloud doc |
saved |
boolean | R | 23.0 | True if no unsaved changes |
typename |
string | R | 23.0 | "Document" |
zoom |
number | R | 25.1 | Zoom factor in percent |
Methods:
| Method | Description | Min Ver |
|---|---|---|
createLayer(kind?, options?) / createLayerGroup(options?) |
Create layer (LayerKind.NORMAL/GROUP/TEXT; no args = pixel layer) or group |
23.0 |
createPixelLayer(options?) |
Create pixel layer | 24.1 |
createTextLayer(options?) |
Create text layer | 24.2 |
duplicate(name?, mergeLayersOnly?) |
Duplicate document | 23.0 |
duplicateLayers(layers, doc?) |
Duplicate; cross-document if doc given |
23.0 |
flatten() |
Merge all into background | 22.5 |
mergeVisibleLayers() |
Merge visible (result is NOT background) | 23.0 |
groupLayers(layers) / linkLayers(layers) |
Organize layers | 23.0 |
paste(intoSelection?) |
Paste from clipboard | 23.0 |
rasterizeAllLayers() |
Rasterize all layers | 23.0 |
crop(bounds, angle?, w?, h?) / trim(type?, ...) / revealAll() |
Canvas manipulation | 23.0 |
resizeCanvas(w, h, anchor?) / resizeImage(w, h, res?, method?) |
Resize | 23.0 |
rotate(angle) |
Rotate canvas (previously rotateCanvas) |
23.0 |
save() |
Save to current path | 23.0 |
saveAs |
Object with .psd(), .psb(), .jpg(), .png(), .bmp(), .gif() methods — each takes a storage File entry, not a path (see File I/O & Storage) |
22.5 |
close(saveDialogOptions?) / closeWithoutSaving() |
Close document | 22.5 |
convertProfile(profile, intent, ...) / changeMode(mode) |
Color management | 23.0 |
suspendHistory(callback, name) |
Batch edits as single undo | 23.0 |
sampleColor(position) |
Sample color at point (returns SolidColor | NoColor) |
24.0 |
calculations(options) |
Channel calculations | 24.5 |
splitChannels() |
Split into separate documents per channel | 23.0 |
trap(width) |
Trap colors (was trapColors in older refs) |
23.0 |
generativeUpscale(model, options?) |
AI upscaling; model: GenerativeUpscaleModel.FIREFLY (requires executeAsModal) |
27.2 |
Gotchas: calculations() requires one unlocked pixel layer. suspendHistory scope limited to single document. mergeVisibleLayers result is NOT background (unlike flatten). No rotateCanvas / flipCanvas — use rotate(angle) for rotation; horizontal/vertical flip is via batchPlay descriptors.
Properties:
| Property | Type | R/W | Min Ver | Notes |
|---|---|---|---|---|
id |
number | R | 22.5 | Unique layer ID |
name |
string | R/W | 22.5 | |
kind |
LayerKind | R | 22.5 | pixel, text, group, smartObject, etc. |
opacity |
number | R/W | 22.5 | 0-100 |
fillOpacity |
number | R/W | 23.0 | 0-100 |
blendMode |
BlendMode | R/W | 22.5 | Validation throws errors (v24.2+) |
visible |
boolean | R/W | 22.5 | |
locked |
boolean | R | 22.5 | True if any lock property is set (read-only aggregate) |
allLocked / pixelsLocked / positionLocked / transparentPixelsLocked |
boolean | R/W | 22.5 | Individual lock variants |
isClippingMask |
boolean | R/W | 23.0 | Releasing affects layers above |
isBackgroundLayer |
boolean | R | 22.5 | |
bounds / boundsNoEffects |
Bounds | R | 22.5 | {left,top,right,bottom} / without effects |
parent |
Layer | R | 22.5 | null for top-level layers |
document |
Document | R | 23.0 | Owning document |
layers |
Layers | R | 23.0 | Child layers (groups only) |
linkedLayers |
Layers | R | 22.5 | |
textItem |
TextItem | R | 24.2 | Text properties (text layers only) |
layerMaskDensity / layerMaskFeather |
number | R/W | 23.0 | User mask density (%) / feather (0-1000) |
vectorMaskDensity / vectorMaskFeather |
number | R/W | 23.0 | Vector mask density / feather |
filterMaskDensity / filterMaskFeather |
number | R/W | 23.0 | Filter mask density / feather |
typename |
string | R | 23.0 | "Layer" |
Non-Filter Methods:
| Method | Description | Min Ver |
|---|---|---|
delete() |
Delete layer | 23.0 |
duplicate(relativeObject?, insertionLoc?, name?) |
Duplicate layer | 23.0 |
move(relativeLayer, insertionLoc) |
Move in layer stack | 23.0 |
link(layer) |
Link with another layer (returns array of linked layers) | 23.0 |
unlink() |
Unlink this layer from its link group | 23.0 |
translate(horizontal, vertical) |
Move layer content | 23.0 |
scale(width, height, anchor?, options?) |
Scale layer; options: { interpolation?: InterpolationMethod } |
23.0 |
rotate(angle, anchor?, options?) |
Rotate layer; same options object | 23.0 |
flip(axis) |
Flip horizontal/vertical | 23.0 |
skew(angleH, angleV, options?) |
Skew transform; same options object | 23.0 |
merge() |
Merge down | 23.0 |
rasterize(target) |
Rasterize content | 23.0 |
bringToFront() / sendToBack() |
Reorder in stack | 23.0 |
copy(merge?) / cut() / clear() |
Clipboard / clear ops | 23.0 |
Note: there is no moveAbove / moveBelow — use move(relativeLayer, insertionLoc). There is no dedicated convertToSmartObject method on Layer — use batchPlay (newPlacedLayer descriptor).
All require executeAsModal. Grouped by category.
Blur Filters:
| Method | Key Parameters | Notes |
|---|---|---|
applyGaussianBlur(radius) |
radius: 0.1-250 | |
applyMotionBlur(angle, distance) |
angle: -360..360, distance: 1-2000 | |
applySmartBlur(radius, threshold, quality, mode) |
radius: 0.1-100 | |
applyLensBlur(options?) |
source, focalDist, shape, radius | |
applyAverage() / applyBlur() / applyBlurMore() |
None | Method is applyAverage (not applyAverageBlur) |
Sharpen Filters:
| Method | Key Parameters | Notes |
|---|---|---|
applySharpen() / applySharpenEdges() / applySharpenMore() |
None | |
applyUnSharpMask(amount, radius, threshold) |
amount: 1-500 (whole-number %), radius: 0.1-1000, threshold: 0-255 |
Noise Filters:
| Method | Key Parameters | Notes |
|---|---|---|
applyAddNoise(amount, distribution, mono) |
amount: 0.1-400 | Not Indexed/Bitmap |
applyMedianNoise(radius) |
radius: 1-500 | Not 32-bit |
applyDustAndScratches(radius, threshold) |
radius: 1-100, threshold: 0-255 | Not 32-bit |
applyDespeckle() |
None | Not 32-bit |
Distortion Filters (unsupported only in Indexed/Bitmap; exception: applyOceanRipple is 8-bit only — RGB/Duotone/Grayscale/Multichannel):
| Method | Key Parameters |
|---|---|
applyDisplace(hScale, vScale, type, undefinedAreas, mapPath) |
scale: -999..999 |
applyPinch(amount) |
-100..100 |
applyWave(generators, minWavelength, maxWavelength, ...) |
Complex |
applyZigZag(amount, ridges, style) |
amount: -100..100 |
applyRipple(amount, size) |
amount: -999..999 |
applyShear(curve, undefinedArea) |
Array of {x,y} |
applyPolarCoordinates(conversion) |
rectToPolar/polarToRect |
applyTwirl(angle) |
-999..999 |
applySpherize(amount, mode) |
-100..100 |
applyOceanRipple(size, magnitude) |
size: 1-15, mag: 1-20 |
Effects Filters:
| Method | Key Parameters | Notes |
|---|---|---|
applyHighPass(radius) |
0.1-1000 | |
applyClouds() / applyDifferenceClouds() |
Uses fg/bg colors | |
applyLensFlare(brightness, center, type) |
10-300 | RGB only |
applyDiffuseGlow(graininess, glow, clear) |
8-bit only (RGB/Duotone/Grayscale/Multichannel) | |
applyGlassEffect(distortion, smoothness, ...) |
8-bit only (RGB/Duotone/Grayscale/Multichannel) |
Other Filters:
| Method | Key Parameters | Notes |
|---|---|---|
applyOffset(h, v, undefinedAreas) |
||
applyCustomFilter(matrix5x5, scale, offset) |
5x5 kernel | |
applyDeInterlace(eliminate, create) |
odd/even | Not Indexed/Bitmap/CMYK 8+16-bit/Duotone 8-bit (32-bit OK) |
applyNTSC() |
RGB only | |
applyMaximum(radius, preserveShape?) / applyMinimum(radius, preserveShape?) |
radius: int 1-500 with SQUARENESS (default), float 0.2-500 with ROUNDNESS | PreserveShape enum |
Compositing: applyImage(options) — source layer/channel, blending, opacity, mask (v24.5+)
Properties: bounds (Bounds), solid (boolean — true if single rectangle), docId (number — NOT a Document reference), parent, typename
Methods:
| Method | Description |
|---|---|
selectAll() |
Select entire canvas |
deselect() |
Remove selection |
selectPolygon(points, mode?, feather?, antiAlias?) |
Polygonal selection |
selectEllipse(bounds, mode?, feather?, antiAlias?) |
Elliptical selection |
selectRectangle(bounds, mode?, feather?, antiAlias?) |
Rectangular selection |
selectColumn(x, mode?) / selectRow(y, mode?) |
Single column/row |
inverse() |
Invert selection |
expand(by) / contract(by) |
Expand/contract by pixels |
feather(by) |
Feather edge |
smooth(radius) |
Smooth selection |
grow(tolerance, antiAlias?) |
Grow by color similarity |
selectBorder(width) |
Select border of current |
translateBoundary(dX, dY) |
Move selection boundary |
rotateBoundary(angle, anchor?, interpolation?) |
Rotate selection boundary |
resizeBoundary(...) |
Resize selection boundary |
makeWorkPath(tolerance?) |
Convert selection to work path |
save(channelName?) / saveTo(channel, mode?) |
Save selection to alpha channel |
load(from, mode?, invert?) |
Load selection from channel |
Mode parameter takes Constants.SelectionType: REPLACE, EXTEND, DIMINISH, INTERSECT.
| Class | Min Ver | Key Members |
|---|---|---|
| LayerComp | 24.0 | name, comment, appearance, position, visibility; apply(), recapture(), remove() |
| ColorSampler | 24.0 | position, color; move(), remove() |
| CountItem | 24.1 | position; remove() |
| PathItem | 23.3 | name, kind, subPathItems; makeSelection(), strokePath(), fillPath() |
| Guide | 23.0 | direction, coordinate; delete() |
| HistoryState | 22.5 | name, snapshot |
Creating paths programmatically (23.3+): build PathPointInfo objects (anchor, leftDirection, rightDirection — each [x, y]; kind: Constants.PointKind.CORNERPOINT/SMOOTHPOINT), collect them in a SubPathInfo (closed, operation, entireSubPath), then doc.pathItems.add(name, [subPathInfo]). For straight segments all three coordinates are identical; offsetting the direction points bends the curve. The resulting SubPathItem/PathPoint objects are read-only.
const start = new PathPointInfo();
start.anchor = start.leftDirection = start.rightDirection = [100, 100];
start.kind = Constants.PointKind.CORNERPOINT;
const stop = new PathPointInfo();
stop.anchor = stop.leftDirection = stop.rightDirection = [200, 200];
stop.kind = Constants.PointKind.CORNERPOINT;
const spi = new SubPathInfo();
spi.closed = false;
spi.operation = Constants.ShapeOperation.SHAPEXOR;
spi.entireSubPath = [start, stop];
const line = doc.pathItems.add("Line", [spi]); // then makeSelection() / fillPath() / ...
line.strokePath(Constants.ToolType.PENCIL);const imaging = require('photoshop').imaging;Pixel writes require executeAsModal. Pixel reads via getPixels() often work outside modal.
| Property | Type | Description |
|---|---|---|
width, height |
number | Dimensions |
colorSpace |
string | "RGB", "Grayscale", "Lab" |
colorProfile |
string | e.g. "sRGB IEC61966-2.1" |
hasAlpha |
boolean | Alpha channel present |
components |
number | 3=RGB, 4=RGBA, 1=Gray |
componentSize |
number | 8, 16, or 32 |
pixelFormat |
string | "RGB", "RGBA", "Grayscale", "GrayscaleAlpha", "LAB", "LABAlpha" |
isChunky |
boolean | true=interleaved RGBRGB, false=planar RRGGBB |
Methods: getData(options?) returns Promise<Uint8Array|Uint16Array|Float32Array> (options: chunky default true, fullRange default false). dispose() synchronous — always call when done.
| Bit Depth | Type | Range | Notes |
|---|---|---|---|
| 8 | Uint8Array | 0-255 | Standard |
| 16 | Uint16Array | 0-32768 default | Photoshop non-standard. fullRange: true for 0-65535. |
| 32 | Float32Array | 0.0-1.0+ | HDR may exceed 1.0. 32-bit linear profiles use different naming. |
Memory layout: Chunky (default): [R0,G0,B0, R1,G1,B1, ...]. Planar: [R0,R1,..., G0,G1,..., B0,B1,...].
const result = await imaging.getPixels({
documentID: doc.id, // optional, defaults to active
layerID: layer.id, // optional, omit for composite
historyStateID: stateId, // optional
sourceBounds: { left: 0, top: 0, right: 300, bottom: 300 },
targetSize: { width: 200, height: 200 }, // downsample via pyramid
colorSpace: "RGB",
colorProfile: "sRGB IEC61966-2.1",
componentSize: 8, // -1 (source), 8, 16, 32
applyAlpha: false // premultiply alpha with white
});
// Returns: { imageData, sourceBounds, level } (level = pyramid level used)
const pixels = await result.imageData.getData();
result.imageData.dispose(); // MANDATORYPerformance: targetSize leverages Photoshop's image pyramid cache — dramatically faster for previews.
await imaging.putPixels({
layerID: layer.id, // REQUIRED
documentID: doc.id, // optional
imageData: imageData, // REQUIRED
replace: true, // false = blend with existing
targetBounds: { left: 0, top: 0 },
commandName: "My Edit"
});const imageData = await imaging.createImageDataFromBuffer(buffer, {
width: w, height: h, components: c, // ALL REQUIRED
colorSpace: "RGB", // REQUIRED
colorProfile: "sRGB IEC61966-2.1",
chunky: true, fullRange: false
});Buffer element count must equal width * height * components.
const jpegBase64 = await imaging.encodeImageData({ imageData, base64: true });
img.src = "data:image/jpeg;base64," + jpegBase64;Only RGB encodable — not Lab or Grayscale. Without base64, returns Number[] (raw JPEG bytes).
Skips the lossy base64-JPEG round-trip: wrap raw pixels in an ImageBlob and display via an object URL.
const blob = new ImageBlob(arrayBuffer, {
width, height,
colorSpace: "RGB", // or "Grayscale"
hasAlpha: false,
components: 3, // 3 = RGB, 4 = RGBA
componentSize: 8, // 8 or 16
pixelFormat: "RGB" // "RGB" | "RGBA" | "Gray"
});
const url = URL.createObjectURL(blob);
img.src = url; // no encode step, no quality loss
URL.revokeObjectURL(url); // when donePhotoshopImageDatais compatible — pass it directly as the options object.- Also accepts compressed data:
new ImageBlob(pngBuffer, { type: "image/png" }). - Buffer length must equal
width * height * components; support is host-gated ("currently supported by Photoshop").
// Layer mask read (single-channel grayscale)
const maskObj = await imaging.getLayerMask({
layerID: layer.id, kind: "user", // or "vector"
sourceBounds: { left: 0, top: 0, right: 300, bottom: 300 },
targetSize: { height: 100 }
});
// Mask write (pixel masks only — kind must be "user", not "vector")
await imaging.putLayerMask({
layerID: layer.id, kind: "user",
imageData: grayImageData, replace: true, commandName: "Edit Mask"
});
// Selection read/write
const selObj = await imaging.getSelection({ documentID: doc.id, sourceBounds: {...} });
await imaging.putSelection({ documentID: doc.id, imageData: grayImageData, replace: true });const color = await document.sampleColor({ x: 100, y: 100 }); // SolidColor
const rgbProfiles = await require('photoshop').app.getColorProfiles('RGB');- Request smallest possible region via
sourceBounds - Use
targetSizeto scale down — leverages pyramid cache dispose()immediately after extracting pixel data- Work in document's native
colorSpace/colorProfilewhen possible - All imaging methods are async — always
await
batchPlay executes Photoshop action descriptors — the same commands used by Actions panel recording. Every descriptor has _obj (command name), _target (reference array), _options (execution options).
| Form | Syntax | Example |
|---|---|---|
| ID | { _ref: "layer", _id: 42 } |
Specific layer by ID |
| Index | { _ref: "layer", _index: 3 } |
Layer at stack position |
| Name | { _ref: "layer", _name: "Background" } |
Layer by name |
| Enum | { _ref: "layer", _enum: "ordinal", _value: "targetEnum" } |
Active/first/last |
| Property | { _property: "opacity" } |
Bare property ref — no _ref wrapper |
References chain as arrays (innermost target first):
const result = await action.batchPlay([{
_obj: "get",
_target: [
{ _property: "opacity" },
{ _ref: "layer", _enum: "ordinal", _value: "targetEnum" },
{ _ref: "document", _enum: "ordinal", _value: "targetEnum" }
],
_options: { dialogOptions: "silent" }
}], {});const result = await action.batchPlay([{
_obj: "multiGet",
_target: [{ _ref: "layer", _enum: "ordinal", _value: "targetEnum" }],
extendedReference: [["name", "opacity", "visible", "bounds"]],
_options: { dialogOptions: "silent" }
}], {});Valid-but-failed commands resolve with an error-descriptor list [{ _obj: "error", message, result }] — check result[0].message. The promise rejects on malformed/invalid calls (e.g., wrong argument types) — and note PS 23.4.1 made batchPlay throw more often on bad requests (e.g., layer move is sensitive to document state and throws where it previously didn't). batchPlaySync (v23.1+) available for quick property reads — blocks UI, never for modifications.
Two separate mechanisms: (1) host.data.enableMenuRecording: true in the manifest makes the plugin's menu commands recordable as Action steps ("Plugin Menu Command"). (2) action.recordAction({ name, methodName }, info) records a custom step — no manifest flag required. methodName names a global function invoked at playback as handler(executionContext, info). The second argument info is a required data object (pass {} if none). Record Again (25.1) replaces the recorded info with the handler's return value.
- Copy As JavaScript: Actions panel > Copy As JavaScript (PS 23.3+, April 2022; requires developer mode)
- Notification listener: Log all events to discover descriptor shapes
- Developer menu: Preferences > Plugins > Show Developer Menu
| Function | Min Ver | Description |
|---|---|---|
addNotificationListener(events, cb) |
23.0 | Subscribe to action events |
removeNotificationListener(events, cb) |
23.0 | Unsubscribe |
batchPlay(descriptors, options) |
23.0 | Execute descriptors async |
batchPlaySync(descriptors, options) |
23.1 | Execute synchronously (blocks UI) |
getIDFromString(string) |
24.0 | Convert string to runtime ID |
recordAction(options, callback) |
25.0 | Record actions |
validateReference(ref) |
23.1 | Check reference validity |
Required for any operation that modifies Photoshop state.
const { core } = require('photoshop');
async function myDocumentEdit(executionContext, descriptor) {
if (executionContext.isCancelled) throw "User cancelled";
executionContext.reportProgress({ value: 0.5, commandName: "Processing..." });
const suspensionID = await executionContext.hostControl.suspendHistory({
documentID: app.activeDocument.id, name: "My Edit"
});
// ... do work ...
// To rename the resulting history state, set `finalName` on the suspensionID object:
suspensionID.finalName = "Completed Edit";
await executionContext.hostControl.resumeHistory(suspensionID, true /* commit */);
}
try {
await core.executeAsModal(myDocumentEdit, {
commandName: "My Edit",
descriptor: { myParam: "value" },
interactive: false,
timeOut: 5000 // v25.10+: retry duration for modal collisions
});
} catch (e) {
if (e.number === 9) console.log("Another plugin holds modal state");
}| Property/Method | Description |
|---|---|
isCancelled |
Check if user pressed Escape |
onCancel |
Register cancellation callback |
reportProgress({ value, commandName }) |
Update progress bar (0-1) |
hostControl.suspendHistory(options) |
Batch edits as single undo step |
hostControl.resumeHistory(suspensionID, commit?) |
End suspension; commit is boolean (default true). To rename state, set suspensionID.finalName before calling. |
hostControl.registerAutoCloseDocument(docId) |
Auto-close on modal exit |
hostControl.unregisterAutoCloseDocument(docId) |
Cancel auto-close |
- Only one plugin holds modal state at a time; menu items disabled during modal
- Progress bar appears after 2+ seconds; Escape cancels
- Event notifications suppressed during non-interactive modal scope
- Nested modal scopes share global modal state
- PS only creates a history state if the document was actually modified
- Modal collision retry (v25.10+): with
timeOut, requests retry instead of immediate error 9 getPixels()is read-only and often works outside modal
Never swallow exceptions inside executeAsModal — this prevents cancellation from propagating:
// BAD: catch swallows cancel signal
try { await action.batchPlay([...], {}); } catch(e) { }
// GOOD: check isCancelled, let errors propagate
if (ctx.isCancelled) throw "Cancelled";
await action.batchPlay([...], {});await action.addNotificationListener(
['modalJavaScriptScopeEnter', 'modalJavaScriptScopeExit'], handler
);Subset of standard HTML. Confirmed working:
Structural: <html>, <head>, <body>, <script>, <style>, <link>, <div>, <span>, <p>, <a>, <h1>-<h6>, <hr>, <footer>
Form:
<input>— types:text,password,number,range,checkbox,radio,search. NOT:file,color.<select>—<option>requires explicitvalueorselect.valuereturns undefined.<textarea>— size via CSS only, notrows/cols.<button>,<progress>(not theme-aware).<label>—for="id"NOT supported; wrap label around control.<form>— onlymethod="dialog".
Media:
<img>— needs explicit dimensions in dialogs. Ignores EXIF rotation. Grayscale fails to render.<video>— full API (v7.4+).timeUpdateevent (v9.1+). No<audio>element —<video>can play audio files (v9.1+).<canvas>— v7.0.0+ with many limitations (see Canvas API below).<webview>— panels (v6.4+/PS 24.1+) and dialogs (v6.0+). Local HTML (v8.0+). Domains optional (v9.0+). Script injection (v9.0.2+). DnD (v9.1+).
Other: <dialog> (modal/non-modal, showModal() returns Promise), <template>, <slot>, <menu>, <menuitem>.
NOT supported (treated as <div>): <ul>, <ol>, <li>, <i>, <em>, <title>, <iframe> (use <webview>).
Unsupported attributes: aria-*, contenteditable, draggable/dropzone, hidden, spellcheck. tabindex partial. Use addEventListener not inline handlers. Unitless dimensions don't work (v3.1.0+).
Layout (flexbox-based): display (none/inline/block/inline-block/flex/inline-flex), flex/flex-basis/flex-direction/flex-grow/flex-shrink/flex-wrap, align-content/align-items/align-self/justify-content, position with offsets, overflow/overflow-x/overflow-y
Box model: width/height/min-*/max-*, margin, padding, border (+ directional), border-radius (+ corner-specific)
Typography: font-family, font-size, font-style, font-weight, color, letter-spacing, text-align, text-overflow, white-space
Background: background, background-color, background-image (url('plugin://assets/...'), linear-gradient(), radial-gradient() — gradients supported since UXP 2.0; v8.1 aligned linear-gradient start/end value syntax with the web), background-size, background-attachment. background-repeat declared but non-functional. Visual: opacity, visibility
Units: px, em, rem, vh, vw, vmin, vmax, cm, mm, in, pc, pt
Features: calc(), CSS Variables, var(), prefers-color-scheme
Selectors: type, class, id, *, attribute, child >, descendant, adjacent +, general sibling ~
Pseudo-classes: active, checked, defined, disabled, empty, enabled, first-child, focus, hover, last-child, nth-child, nth-last-child, nth-of-type, only-child, root
Pseudo-elements: ::after, ::before. Media queries: width, height, prefers-color-scheme
Requires "featureFlags": { "CSSNextSupport": true } in manifest. Without this flag, these properties are silently ignored:
box-shadowtransform-originscaleX,scaleYtranslate
| Feature | Status |
|---|---|
font shorthand |
Use individual font-* |
text-transform |
No uppercase/lowercase/capitalize |
transition, @keyframes |
No animations |
grid layout |
Flexbox only |
position: sticky |
Not supported |
float, clear |
Not supported |
cursor |
Limited; may not revert |
background-repeat |
Declared but non-functional |
baseline alignment |
Buggy/unreliable |
Note: z-index is supported, but no element can overlay a widget with text-editing capability (text fields always render above).
UXP uses a custom layout engine, not a browser engine. Flexbox is the primary mechanism.
const canvas = document.createElement('canvas');
canvas.width = 400; canvas.height = 400;
const ctx = canvas.getContext('2d'); // Only '2d' supportedAvailable: lineWidth, lineJoin, lineCap, globalAlpha, fillStyle, strokeStyle; paths (beginPath, closePath, moveTo, lineTo, arc, arcTo, bezierCurveTo, quadraticCurveTo, rect); drawing (fill, stroke, fillRect, strokeRect, clearRect); gradients (createLinearGradient, createRadialGradient); Path2D.
NOT available: toDataURL, toBlob, drawImage, getImageData, putImageData, createImageData, fillText, strokeText, measureText, save, restore, transforms (translate, rotate, scale, setTransform), clip, createPattern, globalCompositeOperation, shadow properties, imageSmoothingEnabled, WebGL.
Windows breakage: clearRect, createLinearGradient, createRadialGradient may fail.
Workarounds: software renderer -> createImageDataFromBuffer -> encodeImageData({ base64: true }) -> <img src="data:...">. SVG for vector plots. WebView (panels v6.4+ or dialogs). Hybrid C++ plugin.
Native Photoshop-styled widgets following Adobe's Spectrum design system. Since UXP v4.1. Components are automatically theme-aware.
<!-- Variants: cta (default), primary, secondary, warning, overBackground -->
<sp-button variant="primary">Click Me</sp-button>
<sp-button variant="warning" quiet>Delete</sp-button>
<sp-button variant="primary">
<sp-icon name="ui:Magnifier" size="s" slot="icon"></sp-icon>
Search
</sp-button>Event: click. sp-action-button: same API with quiet, selected attributes.
<sp-checkbox checked>Pre-checked</sp-checkbox>
<sp-checkbox indeterminate>Partial</sp-checkbox>Events: change, input — access evt.target.checked
<sp-dropdown placeholder="Select..." style="width: 320px">
<sp-menu slot="options">
<sp-menu-item>Option 1</sp-menu-item>
<sp-menu-item disabled>Option 2 (disabled)</sp-menu-item>
<sp-menu-divider></sp-menu-divider>
<sp-menu-item>Option 3</sp-menu-item>
</sp-menu>
</sp-dropdown>Event: change — access evt.target.selectedIndex
Known issues: needs explicit width or displays oddly. Doesn't respond to arrow keys.
<sp-slider min="0" max="100" value="50">
<sp-label slot="label">Opacity</sp-label>
</sp-slider>
<sp-slider min="0" max="360" value="0" variant="filled" show-value>
<sp-label slot="label">Rotation</sp-label>
</sp-slider>Attributes: min, max, value, disabled, variant="filled", fill-offset, value-label, show-value
Events: input (during drag), change (on release) — access evt.target.value
<sp-textfield placeholder="Enter text">
<sp-label isrequired="true" slot="label">Name</sp-label>
</sp-textfield>
<sp-textfield quiet placeholder="Search..."></sp-textfield>Types: text (default), number (-214748.36 to 214748.36), search, password
Known issue: Password value unreadable on macOS. Workaround: toggle type between password/text on focus/blur.
<sp-textarea placeholder="Enter description...">
<sp-label slot="label">Description</sp-label>
</sp-textarea>
<sp-radio-group>
<sp-label slot="label">Color Space:</sp-label>
<sp-radio value="ycbcr">YCbCr BT.601</sp-radio>
<sp-radio value="hsl">HSL</sp-radio>
</sp-radio-group>sp-radio-group event: change — access evt.target.value. Add column for vertical layout.
| Component | Key Attributes | Events/Notes |
|---|---|---|
sp-menu / sp-menu-item |
disabled; sp-menu-divider |
change (.selectedIndex). Divider only renders in dropdown. |
sp-icon |
name="ui:...", size (xxs-xxl) | 36 built-in icons |
sp-progressbar |
max, value, show-value, size | |
sp-divider |
size (large/medium/small) | |
sp-link |
href, quiet | href navigation can't be prevented |
sp-heading |
size (XXS-XXXL) | |
sp-body |
size (XS-XXXL) | |
sp-detail / sp-label |
size / isrequired, slot="label" | |
sp-tooltip |
location controls tip direction, not position |
|
sp-action-menu |
change (SWC-originated — requires enableSWCSupport) |
Note: sp-action-group, sp-action-menu, sp-button-group originate from Spectrum Web Components (SWC) — they are not in the core UXP widget set and require "enableSWCSupport": true in featureFlags.
36 built-in icons: chevrons, arrows, checkmarks, alerts, magnifier, crosses, star (Star/StarOutline), FolderBreadcrumb, info, help (HelpMedium/HelpSmall), grippers, etc. No "settings" icon exists.
SWC provides npm-wrapped packages with additional components not available as sp-* elements. Requires "enableSWCSupport": true in manifest featureFlags. Locked to v0.37.0 in UXP 8.0.
SWC-only components include: accordion, avatar, badge, banner, card, dialog, field-group, illustrated-message, meter, number-field, picker, popover, search, sidenav, split-button, status-light, swatch, switch, table, tabs, toast, tooltip, top-nav, tray.
Custom element events don't flow through React's synthetic event system. Use refs + addEventListener:
function MySlider() {
const ref = useRef(null);
useEffect(() => {
const handler = (e) => console.log(e.target.value);
ref.current.addEventListener('input', handler);
return () => ref.current.removeEventListener('input', handler);
}, []);
return <sp-slider ref={ref} min="0" max="100" value="50" />;
}Document-modifying events (layer changes, selections, filters). Since v23.0. 218 action events available.
const { action } = require('photoshop');
await action.addNotificationListener(
['open', 'close', 'save', 'select', 'make', 'set', 'delete'],
(eventName, descriptor) => {
console.log(`Event: ${eventName}`, descriptor);
}
);
await action.removeNotificationListener(['open', 'close', 'save'], handler);Common events: layer ops (merge, duplicate, group, ungroup), selection changes, pixel mods (blur, sharpen, levels, curves), layer effects, transforms (rotate, flip, crop, resize).
UI and OS events (v23.3+):
const { core } = require('photoshop');
await core.addNotificationListener('UI', ['userIdle'], (event, descriptor) => {
if (descriptor.idleEnd) console.log('User returned from idle');
});
await core.addNotificationListener('OS', ['activationChanged'], handler);UI events: userIdle, minimizeAppWindow, panelVisibilityChanged, workspace events. OS events: activationChanged, displayConfigurationChanged.
userIdle is the right trigger for deferred heavy re-renders — fires after the user stops interacting.
Important: Event notifications are suppressed during non-interactive executeAsModal scope.
Standard DOM events work with limits:
Supported: click, change, input, focus, blur, mousedown/mouseup/mousemove/mouseenter/mouseleave, pointerdown/pointerup/pointermove, keydown/keyup, wheel, resize (via ResizeObserver), load (images).
NOT supported: keypress, dragstart/drag/dragend/drop (DnD fully unsupported). Interactive elements swallow most events — clicks on buttons don't bubble to parents.
Both IntersectionObserver and ResizeObserver are available.
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
console.log(entry.contentRect.width, entry.contentRect.height);
}
});
observer.observe(myElement);MutationObserver is not available.
| API | Status |
|---|---|
document.createElement('iframe') |
Use <webview> in panels (v6.4+) or dialogs |
window.devicePixelRatio |
Always returns 1 — cannot detect HiDPI |
localStorage / sessionStorage |
Available as global storage APIs. NOT inside WebView with local content. |
XMLHttpRequest cookies |
Not supported |
XMLHttpRequest Blob send |
Use ArrayBuffer |
WebSocket extensions |
Not supported |
fetch |
Available with limits |
CustomEvent |
Available |
CustomElementRegistry |
Available |
ShadowRoot |
Available |
TreeWalker |
Available |
MutationObserver |
Not available |
requestAnimationFrame |
Limited |
| Web/Service Workers | Not available |
IndexedDB |
Not available |
WebGL |
Not available |
WebAssembly |
Not available |
URLSearchParams |
Available (v9.1+) |
Navigator.language |
Available (v26.0+) |
HTMLElement.append/prepend/replaceChildren |
Available (v26.0+) |
SecureStorage |
Web Storage API compliant (v9.0+) |
System sleep/awake events available (v27.4+).
Document, DocumentFragment, Element, Node, Text, Comment; querySelector(All), getElementById; createElement, createTextNode, createDocumentFragment; appendChild, removeChild, insertBefore, replaceChild; setAttribute, getAttribute, removeAttribute; classList; innerHTML, outerHTML, textContent, innerText; clientWidth/Height, offsetWidth/Height, scrollLeft/Top/Width/Height; getBoundingClientRect; getComputedStyle; AbortController/AbortSignal; alert/confirm/prompt.
- Closed dialogs remain in DOM — must call
element.remove() showModal()returns a Promise (not void like browsers)showModal()does not put Photoshop into a modal state by itself — pass the Photoshop-only optionshowModal({ lockDocumentFocus: true })to lock the Photoshop UI while the dialog is up- Only one dialog at a time
HTMLDialogElement.REJECTION_REASON_NOT_ALLOWED— another dialog already openHTMLDialogElement.REJECTION_REASON_DETACHED— node removed from DOM
Inline event handlers (onclick="...") in innerHTML are parsed but behavior is unreliable. Always use addEventListener.
Available on text layers via layer.textItem.
Properties: contents (R/W string), isParagraphText (R), isPointText (R), orientation (R/W), textClickPoint (R/W, {x, y})
Methods: convertToParagraphText(), convertToPointText(), convertToShape(), createWorkPath()
33 properties via textItem.characterStyle (all R/W): alternateLigatures, antiAliasMethod, autoKerning, baseline, baselineShift, capitalization, characterAlignment, color (SolidColor), fauxBold, fauxItalic, font, fractionalWidths, fractions, horizontalDiacriticPosition, horizontalScale, kashidas, language, leading, ligatures, middleEasternDigitsType, middleEasternTextDirection, noBreak, ordinals, size, strikeThrough, stylisticAlternates, swash, titlingAlternates, tracking, underline, useAutoLeading, verticalDiacriticPosition, verticalScale. Note the casing: strikeThrough, useAutoLeading. Also has reset() (restores Character panel defaults).
14 properties via textItem.paragraphStyle: features, firstLineIndent, hyphenation, hyphenationFeatures, justification (Constants.Justification: LEFT, CENTER, RIGHT, LEFTJUSTIFIED, CENTERJUSTIFIED, RIGHTJUSTIFIED, FULLYJUSTIFIED), justificationFeatures, kashidaWidth, kinsoku, layoutMode, leftIndent, mojikumi, rightIndent, spaceAfter, spaceBefore. Also has reset().
Via textItem.warpStyle: style (16 types: arc, arcLower, arcUpper, arch, bulge, flag, fish, fisheye, inflate, none, rise, shellLower, shellUpper, squeeze, twist, wave), bend (-100..100), direction (horizontal/vertical), horizontalDistortion (-100..100), verticalDistortion (-100..100)
const color = new (require('photoshop').app.SolidColor)();
color.rgb.red = 255; color.rgb.green = 128; color.rgb.blue = 0;
app.foregroundColor = color; // v23.0+; non-RGB setter bug fixed in 24.2Accessors: .rgb, .cmyk, .hsb, .lab, .gray, .nearestWebColor. Method: isEqual(other).
| Class | Properties | Ranges |
|---|---|---|
| RGBColor | red, green, blue | 0-255 |
| CMYKColor | cyan, magenta, yellow, black | 0-100 |
| HSBColor | hue, saturation, brightness | hue 0-360, sat/bright 0-100 |
| LabColor | l, a, b | l 0-100, a/b -128..127 |
| GrayColor | gray | 0-100 |
const { constants } = require('photoshop');
const labColor = core.convertColor(rgbColor, constants.ColorConversionModel.Lab); // v23.0+
const profiles = await app.getColorProfiles('RGB'); // v24.1+convertColor takes a ColorConversionModel enum (Lab, etc.), not a string — it's the internal API underlying the SolidColor color classes.
const { storage } = require('uxp');
const fs = storage.localFileSystem; // Manifest v5+
// User-picked (permission "request" or higher)
const file = await fs.getFileForOpening({ types: storage.fileTypes.images }); // read-only File; empty result if cancelled
const out = await fs.getFileForSaving("output.png", { types: ["png"] }); // read-write File; null if cancelled
const dir = await fs.getFolder(); // picked folder — files within are read-write
// Plugin-scoped (no picker, no permission prompt)
const data = await fs.getDataFolder(); // persistent, survives host-app upgrades
const tmp = await fs.getTemporaryFolder(); // cleared when the plugin is disposed
const pkg = await fs.getPluginFolder(); // read-only packaged assetsgetFileForOpening(options):types(extension array, default['.*']),allowMultiple(default false),initialDomain/initialLocation. Returned files are read-only.getFileForSaving(suggestedName, { types }): extensions have no "." prefix;suggestedNamerequired whentypesabsent; prompts on overwrite.- URL-based entries:
getEntryWithUrl("plugin-data:/cache/x.json")(throws if missing),createEntryWithUrl(url, { type: storage.types.folder, overwrite: true }). Schemes:plugin:/(read-only package),plugin-data:/,plugin-temp:/, plusfile:paths. AlsogetFsUrl(entry)/getNativePath(entry). storage.fileTypes.imagesis not an exhaustive list (PS-57601) — pass your own extension array when in doubt.- Manifest
requiredPermissions.localFileSystem:"request"(pickers),"plugin"(plugin storage only),"fullAccess"(all volumes; user consents at install). Without it, onlyplugin:/plugin-data:/plugin-temp:are reachable.
const text = await file.read(); // UTF-8 by default
const bytes = await file.read({ format: storage.formats.binary }); // ArrayBuffer
await out.write(buffer, { format: storage.formats.binary }); // returns bytes written; { append: true } appends
const entry = await data.createFile("settings.json", { overwrite: true }); // entry only — file materializes on write()
const sub = await data.createFolder("cache");
const items = await data.getEntries();Entry members: isFile/isFolder/name/url/nativePath, copyTo(folder), moveTo(folder), delete(), getMetadata() (size, dateCreated, dateModified). write throws FileIsReadOnly / OutOfSpace. The Node-style require('fs') API also accepts plugin URLs: fs.readFile("plugin-data:/x.txt", { encoding: "utf-8" }).
batchPlay descriptors don't take Entry objects or native paths — convert the entry to a token:
const entry = await fs.getFileForOpening();
const token = fs.createSessionToken(entry); // valid for this session only
await action.batchPlay([{
_obj: "open",
target: { _path: token, _kind: "local" } // _path expects a session token
}], {});- DOM APIs take the Entry itself (
app.open(entry),document.saveAs.*(entry, ...)) — Photoshop converts it to a session token internally. Tokens are only needed for batchPlay descriptors. createPersistentToken(entry)/getEntryForPersistentToken(token)— ask-once file/folder access across restarts (store the token, e.g. in localStorage). Not guaranteed forever (file moves, permission changes) — on failure, re-prompt and store a fresh token.getEntryForSessionToken(token)throws a ReferenceError for unknown tokens.
const psd = await fs.getFileForSaving("target.psd", { types: ["psd"] });
await doc.saveAs.psd(psd); // each: (entry: File, saveOptions?, asCopy?)
await doc.saveAs.jpg(jpgEntry, { quality: 12 }, true);
await doc.saveAs.psb(psbEntry, { embedColorProfile: true });
const opened = await app.open(entry); // or app.open() for the OS pickerAll six saveAs sub-methods take a UXP storage File entry, never a path string.
| Store | Persistence | Use for |
|---|---|---|
localStorage / sessionStorage |
Restarts / current session | Small prefs, UI state. Never secrets. |
Data folder file (plugin-data:/) |
Persistent; survives host upgrades | Settings files, caches, large data |
secureStorage |
Encrypted at rest, but treat as a cache — can be lost (host uninstall, crypto damage) | Secrets the plugin can re-obtain |
createPersistentToken |
Until the file moves or permissions change | Remembering user-granted file access |
const { secureStorage } = require('uxp').storage;
await secureStorage.setItem("apiKey", key); // string | ArrayBuffer | TypedArray
const bytes = await secureStorage.getItem("apiKey"); // Uint8Array — decode yourself
const key2 = new TextDecoder().decode(bytes);secureStorage encrypts values (not keys) under the current user's OS account — it does not protect data from the user, and Adobe says to treat it as regenerable cache. getItem returns bytes, not a string. Web Storage API error semantics since UXP 9.0.
JavaScript access to Adobe's XMP Core and XMP Files libraries. Classes: XMPMeta (create/read/modify packets), XMPFile (read/write metadata directly on a file), XMPUtils, XMPDateTime, XMPConst, plus XMPIterator/XMPProperty/XMPFileInfo/XMPPacketInfo.
const { XMPMeta, XMPConst, XMPFile } = require("uxp").xmp;
const meta = new XMPMeta(); // or new XMPMeta(xmpString)
meta.setProperty(XMPConst.NS_XMP, "CreatorTool", "My Plugin");
const packet = meta.serialize(); // XML string
const xmpFile = new XMPFile(file.nativePath, XMPConst.UNKNOWN, XMPConst.OPEN_FOR_UPDATE);
const xmp = xmpFile.getXMP(); // legacy metadata auto-converts to XMP
xmp.deleteProperty(XMPConst.NS_DC, "creator");
xmp.appendArrayItem(XMPConst.NS_DC, "creator", "Judy", 0, XMPConst.ARRAY_IS_ORDERED);
if (xmpFile.canPutXMP(xmp)) xmpFile.putXMP(xmp);
xmpFile.closeFile(XMPConst.CLOSE_UPDATE_SAFELY);fetch (recommended): standard API with HTTPS domain whitelisting. FormData multipart (v7.3+).
XMLHttpRequest: no cookies, binary via ArrayBuffer only (not Blob).
WebSocket: available; extensions unsupported; self-signed certs fail on macOS.
See the File I/O & Storage section above for the full API (pickers, plugin folders, tokens, secureStorage). Streaming reads: fs.createReadStream() (v8.2+).
const { shell, host } = require('uxp');
await shell.openExternal("https://example.com/docs", "Open documentation"); // no file: URLs
await shell.openPath("/path/to/folder", "Reveal output folder"); // system default app
// host.name ("photoshop"), host.version (e.g. "27.0.0"), host.uiLocale ("en_US")Both shell calls require the launchProcess permission ({ schemes, extensions }) and show a runtime consent dialog displaying your second-argument text (localize it). They resolve with "" on success or an error-message string. openExternal rejects file: URLs — use openPath for those.
Panels (v6.4+/PS 24.1+), dialogs (v6.0+). Local HTML (v8.0+). Domains optional (v9.0+). Script injection (v9.0.2+). DnD (v9.1+). localStorage/sessionStorage NOT available in WebView with local content.
The allow field was removed from permissions.webview in UXP 9.1 / PS 27.4. Configure via domains only:
"webview": { "domains": ["https://example.com"] }(Older examples showing "allow": "yes" still parse but the field is ignored on UXP 9.1+.)
Combine JS with native C++ (.dylib/.dll). Use for: high-performance pixel processing, custom rendering, relaxed sandbox, Photoshop C++ SDK (PIUXPSuite). Entry: export SPErr PSDLLMain(const char* selector, SPBasicSuite* basicSuite, PIActionDescriptor descriptor);
JS->C++: require('photoshop').messaging.sendSDKPluginMessage(componentId, data) / C++ sUxpProcs->AddUXPMessageListener(ref, handler)
C++->JS: C++ sUxpProcs->SendUXPMessage(ref, pluginId, desc) / JS require('photoshop').messaging.addSDKMessagingListener(cb) (plus removeSDKMessagingListener(cb))
Build: Hybrid Plugin SDK from Adobe Developer Console. macOS unsigned need security approval. Debug: JS via UDT, C++ via debugger attach.
require('photoshop').app
Properties:
| Property | Type | R/W | Min Ver | Description |
|---|---|---|---|---|
activeDocument |
Document | R/W | 23.0 | Currently active document |
foregroundColor |
SolidColor | R/W | 23.0 | Foreground color; non-RGB setter bug fixed 24.2 |
backgroundColor |
SolidColor | R/W | 23.0 | Background color; setter added + non-RGB bug fixed 24.2 |
currentTool |
Tool | R | 23.0 | Active tool object — currently just { id } |
documents |
Documents | R | 23.0 | Open document collection |
fonts |
TextFonts | R | 23.0 | Available fonts |
preferences |
Preferences | R | 24.0 | Application preferences |
actionTree |
ActionSet[] | R | 23.0 | Recorded action sets |
displayDialogs |
DialogModes | R/W | 23.0 | Dialog display mode |
Methods:
| Method | Description | Min Ver |
|---|---|---|
createDocument(options?) |
Create new document | 23.0 |
open(entry?) |
Open file (UXP storage File entry, not a path) or the OS picker when no arg | 23.0 |
bringToFront() |
Bring Photoshop to front | 23.0 |
showAlert(message) |
Display alert dialog | 23.0 |
convertUnits(value, from, to) |
Convert between units | 23.4 |
getColorProfiles(mode) |
List color profiles for mode | 24.1 |
updateUI() |
Force UI refresh (no effect outside tracking contexts) | 26.0 |
require('photoshop').core
| Function | Description | Min Ver |
|---|---|---|
executeAsModal(callback, options) |
Enter modal editing scope | 22.5 |
convertColor(sourceColor, targetModel) |
Convert between color models | 23.0 |
convertGlobalToLocal(point) |
Screen to panel coords | 26.0 |
getActiveTool() |
Get active tool info | 22.5 |
performMenuCommand(options) |
Execute menu command by ID | 22.5 |
getLayerTree(options?) / getLayerTreeSync(options?) |
Layer hierarchy | 23.1 |
getLayerGroupContents(opts) / getLayerGroupContentsSync(opts) |
Group children | 23.1 |
isModal() |
Check if in modal state | 23.1 |
historySuspended() |
Check if history suspended | 23.1 |
calculateDialogSize(options) |
Compute dialog dimensions | 22.5 |
getCPUInfo() / getGPUInfo() |
Hardware information | 23.1 |
setExecutionMode(mode) |
Set plugin execution mode | 23.2 |
translateUIString(key) |
Localize string | 22.5 |
redrawDocument(docId) |
Force document redraw | 24.1 |
Hybrid C++ bridge messaging lives on photoshop.messaging, not core:
photoshop.messaging.addSDKMessagingListener(callback)— listen for C++ → JS messagesphotoshop.messaging.removeSDKMessagingListener(callback)— remove listenerphotoshop.messaging.sendSDKPluginMessage(componentId, data)— send JS → C++ messages (componentId from C plugin'sPiPLresource)
const { app, constants } = require("photoshop");
await app.activeDocument.resizeImage(800, 600, 100, constants.InterpolationMethod.AUTOMATIC);100 enum groups (AnchorPosition … ZigZagType). Pass enum members, not strings, wherever the DOM expects typed values. Most-used:
LayerKind(27 values incl. adjustment kinds; GRAIN and CLARITY new in PS 27.4)SelectionType— REPLACE / EXTEND / DIMINISH / INTERSECT (PathItem.makeSelection,Selection.saveTo/load)ElementPlacement— PLACEBEFORE / PLACEAFTER / PLACEINSIDE / PLACEATBEGINNING / PLACEATEND (Layer.move; PLACEINSIDE throws on non-group layers)BlendMode,NewDocumentMode,InterpolationMethod,ColorConversionModel
12 groups via app.preferences: cursors, fileHandling, general, guidesGridsAndSlices, history, interface, notifications (v26.11), performance, tools, transparencyAndGamut, type, unitsAndRulers
5 extensible classes: Photoshop (app.__proto__), Document, Layer, ActionSet, Action. Add via prototype:
Document.prototype.getVisibleLayers = function() {
return this.layers.filter(l => l.visible);
};Note: Adobe's known issues page is not systematically updated for fixed items. Test against your target PS version.
Forward-compat: Photoshop 27.8 Beta (May 2026) began replatforming the UXP plugin rendering infrastructure for cross-app consistency. Adobe expects no look-and-feel change for most plugins and has documented no API changes — but if a plugin renders subtly differently on 27.8+, this replatform is the likely cause.
- Canvas APIs broken on Windows:
createLinearGradient,createRadialGradient,clearRectmay fail - No
toDataURL/toBlob— cannot export canvas content directly - No
drawImage— cannot composite images on canvas - No
getImageData/putImageData— cannot read/write individual pixels on canvas window.devicePixelRatioalways returns 1 — cannot detect HiDPI- Panel
show/hideunreliable:showfires once only,hidenever fires (PS-57284) - No CSS transitions/animations — all visual changes immediate
- No CSS Grid — flexbox only (box-shadow/transforms available with
CSSNextSupportflag)
- Widgets with text-editing capability render above other content — no element can overlay text-editing widgets (affects
z-indexplanning) - Complex SVG may fail or render unexpectedly
- Grayscale
<img>fails to render but occupies DOM space <img>in dialogs needs explicit width/height or dialog resizes incorrectly<img>ignores embedded EXIF rotationsp-dropdownneeds explicit widthsp-tooltiplocationcontrols tip direction, not position- Numeric
sp-textfieldrange: -214748.36 to 214748.36 - DnD fully unsupported
- Scroll views don't auto-scroll to focused controls when tabbing on macOS
<label for="id">unsupported — wrap label around control<option>requires explicitvalue<option>doesn't supportdisabled- HTML5 input validation unsupported
<textarea>size via CSS only, notrows/cols<form>only supportsmethod="dialog"
- Self-signed certs unsupported for secure WebSockets on macOS
- WebSocket extensions unsupported
- XHR can only send binary via ArrayBuffer, not Blob
- XHR doesn't support cookies
- Closed dialogs remain in DOM — must call
remove() entrypoints.setup()delayed >20ms may cause uncatchable errors- Clipboard APIs throw in panel-less plugins
keypressunsupported — usekeydown/keyup- Password field values unreadable on macOS
fontshorthand CSS unsupportedtext-transformCSS unsupported- Dialog
showModal()doesn't create a Photoshop modal state — pass the Photoshop-onlyshowModal({ lockDocumentFocus: true })option to lock the UI while the dialog is up - batchPlay throws more aggressively since PS 23.4.1 — commands like layer
movethrow on document states they previously tolerated