| name | adobe-plugin-development | ||
|---|---|---|---|
| description | Use when building, modifying, or debugging Adobe Photoshop UXP plugins or Lightroom Classic plugins — manifest.json/Info.lua setup, panel and dialog UI with Spectrum sp-* widgets, batchPlay action descriptors, executeAsModal, imaging.getPixels pixel access, UXP file I/O and settings persistence (storage.localFileSystem, session tokens, secureStorage), .psjs scripts, LrView data binding, LrDevelopController develop-slider observation, export/publish services, LrCatalog/LrPhoto metadata, develop presets, memory management in long-lived LrC processes, hybrid C++ native bridges, and UXP HTML/CSS/Canvas limits | ||
| license | MIT | ||
| metadata |
|
Reference for writing Photoshop UXP plugins (JavaScript/HTML) and Lightroom Classic plugins (Lua). Covers what the SDKs allow, what they don't, and the non-obvious pitfalls that burn hours.
API baseline: Photoshop API changelog 27.4 (Feb 2026) / UXP 9.2.0 / Manifest v5 — Adobe versions the plugin API by changelog entry, not app release, so this stays current for later 27.x builds until the API changelog advances. (PS 27.8 began replatforming UXP plugin rendering — no documented API changes.) LrC SDK 15.3 (latest published) / Lua 5.1.5. Last verified: 2026-07.
Use when:
- Writing or editing a UXP panel/command plugin (
manifest.json,entrypoints.setup, Spectrumsp-*widgets) or a one-off.psjsscript - Writing or editing a Lightroom Classic plugin (
Info.lua,LrView,LrDevelopController) - Reading pixel data from Photoshop (
imaging.getPixels) - Reading/writing files or persisting settings and secrets from UXP (
storage.localFileSystem, tokens,secureStorage) - Wiring develop-slider observation in LrC
- Displaying generated images inside a UXP panel (no HTML canvas pixel ops!)
- Bridging LrC to a native binary for pixel processing
- Debugging memory leaks in long-lived LrC floating dialogs
- Using batchPlay for Photoshop action descriptors
- Manipulating Document/Layer DOM (create, modify, filter, composite)
- Recording or replaying Photoshop actions programmatically
Don't use for:
- General web UI — UXP is NOT a browser; different rules apply
- ExtendScript / CEP (legacy Adobe plugin systems) — this covers UXP only
- Lightroom Cloud REST APIs — those are separate from the LrC desktop SDK
| Capability | Photoshop UXP | Lightroom Classic |
|---|---|---|
| Language | JavaScript (HTML/CSS) | Lua 5.1.5 |
| Pixel read | imaging.getPixels() |
None — must export rendition + decode externally |
| Pixel write | imaging.putPixels() |
None |
| UI | HTML/CSS/JS + Spectrum sp-* |
LrView declarative widgets only |
| Canvas/drawing | Limited 2D (no drawImage/getImageData/toDataURL) |
None |
| Document DOM | Full: 36 Document props + 29 methods, 28 Layer props + 55 methods (38 filter + 17 other) | Catalog-centric: LrCatalog, LrPhoto |
| Selection API | Full Selection class (v25.0+) |
LrSelection namespace (rating, flag, label, nav) |
| AI features | generativeUpscale (v27.2+) |
denoise, reflection removal, distraction detection |
| Develop events | action.addNotificationListener |
LrDevelopController.addAdjustmentChangeObserver |
| File I/O | uxp.storage.localFileSystem — permission-gated pickers, plugin folders, tokens |
Full Lua io library |
| Plugin types | Panel, Command, .psjs script |
Export, Publish, Metadata, Filter, Web Gallery, Library menu |
| Develop-panel UI | Panels dock anywhere | Cannot add Develop-right-panel UI — only floating dialog |
| Action recording | recordAction API (v25.0+) |
Not available |
Fundamental constraint: LrC cannot read pixels from Lua. Any LrC plugin needing pixel access must export a thumbnail or rendition and hand it to a compiled external binary (C++/Rust).
<canvas>exists (v7.0.0+) but has nodrawImage,getImageData,putImageData,toDataURL,toBlob.- To display a generated image: build pixel buffer ->
imaging.createImageDataFromBuffer->imaging.encodeImageData({ base64: true })-> set<img src="data:image/jpeg;base64,...">. - Lossless alternative (v7.0+, PS-only):
new ImageBlob(buffer, { width, height, components, componentSize, pixelFormat, colorSpace, hasAlpha })->img.src = URL.createObjectURL(blob)— skips the JPEG round-trip; accepts aPhotoshopImageDatadirectly as options. - Windows extra sharp edges:
createLinearGradient,createRadialGradient,clearRectmay fail entirely. - Real-render options: software renderer in JS, embedded WebView, or hybrid C++ plugin.
- 16-bit range is 0-32768 by default, not 0-65535. Pass
{ fullRange: true }togetData()for full range. - Always call
imageData.dispose()aftergetData(). UDT warns at 600MB. - Pass
targetSizeto leverage Photoshop's pyramid cache — dramatically faster than full-res reads. encodeImageDataonly supports RGB (not Lab, not Grayscale).getPixels()usually works withoutexecuteAsModal, but any pixel write requires it.- Default layout is "chunky" (interleaved
RGBRGB); planar isRRGGBB.
- No CSS Grid, no
float, notransition/@keyframes, notext-transform, nofontshorthand, noposition: sticky. box-shadow,transform-origin,scaleX/scaleY,translaterequire theCSSNextSupportfeature flag in manifest. Without it, CSS silently ignores these properties.window.devicePixelRatioalways returns 1 — cannot detect HiDPI.hidepanel lifecycle callback never fires;showfires only once (PS-57284). Don't rely on them for refresh gating.<label for="id">doesn't work — wrap the label around the control instead.<option>needs an explicitvalueattribute.
Enable in manifest.json to unlock additional CSS properties:
{
"featureFlags": { "CSSNextSupport": true }
}Unlocks: box-shadow, transform-origin, scaleX, scaleY, translate. Also accepts a granular array (["boxShadow", "transformFunctions", "transformProperties"]). Without the flag these properties are silently ignored — no error, no warning — except that "enableSWCSupport": true implicitly enables CSSNextSupport.
Both are available as global storage APIs — contrary to some older references listing them as unavailable. However, they are not available inside WebView when loading local content. Never store secrets in them — use storage.secureStorage (encrypted; getItem returns a Uint8Array; treat it as a re-obtainable cache, not durable storage).
document.saveAs.*() and app.open() take UXP storage File entries — never path strings. batchPlay descriptors take neither: convert with fs.createSessionToken(entry) and pass it as _path. For ask-once access across restarts, store a createPersistentToken(entry) and re-prompt when redemption fails.
WebView works in panels since UXP 6.4 / PS 24.1 (not just modal dialogs). Local HTML files supported since UXP 8.0. Domain restrictions optional since UXP 9.0. The permissions.webview.allow field was removed in UXP 9.1 (PS 27.4) — configure with domains only.
- Never swallow exceptions:
try { await batchPlay(...) } catch(e) {}prevents automatic cancellation termination. Let exceptions propagate. timeOutoption (v25.10): modal collisions now retry fortimeOutduration instead of immediate error.updateUI()(v26.0) has no effect outside tracking contexts (slider handlers).
batchPlay execution errors resolve (not reject) with error objects in the result array. Always check return values:
const results = await action.batchPlay([descriptor], {});
if (results[0].message) {
// error occurred — results[0].message contains description
}A floating dialog running for hours in LrC's sandbox is unforgiving. Non-negotiable rules:
- Frame alternation is mandatory.
f:picturemust receive a different path each update. Writing the same path repeatedly causes LrC to cache every version internally without releasing — this caused a 40GB leak in chromascope. Alternatescope_0.jpg/scope_1.jpg. - Guard
requestJpegThumbnailcallbacks. Afterdone = true, subsequent callbacks must return immediately. Nil outjpegDataafter writing. Must hold reference to the returned request object or it may be garbage collected. - Debounce async tasks with a version counter. Slider drags spawn hundreds of coroutines without debouncing. Use a
_settleVersion/_adjustVersioncounter and drop stale callbacks. - Busy-guard + pending flag coalescing. If a render is in progress, set a pending flag instead of spawning another. When render completes, check pending and re-run once.
- No unbounded module-level state. Only fixed-size vars: busy flag, frame index, pending flag, settings hash. Never grow a list.
- Clean up temp files on dialog open. LrC sandbox blocks
collectgarbage— there's no GC escape hatch.
LrC SDK exposes Library menu items, export dialogs, metadata fields — never a Develop-right-panel UI. The only realistic develop-time feedback loop is a floating dialog (LrDialogs.presentFloatingDialog) launched from a Library menu item.
addAdjustmentChangeObserver covers slider changes only. In a floating dialog, pass selectionChangeObserver (and sourceChangeObserver) to LrDialogs.presentFloatingDialog — LrC calls it when the selected photos/videos change; then read catalog:getTargetPhoto(). Outside a floating dialog there is no event — poll catalog:getTargetPhoto() in a LrTasks.startAsyncTask loop with coalescing.
Must hold a reference to the returned request object. If the return value is not stored, Lua's garbage collector may collect it and the callback never fires.
Known typos in parameter names returned by getDevelopSettings: HueAdjustmentMagenha (should be Magenta), LuminanceAdjustmentAque (should be Aqua), Parametriclights (inconsistent casing). Use exact typo'd names when reading these values.
processRenderedPhotos runs in a task LrC creates — do not create your own LrTasks.startAsyncTask inside it. Doing so nests tasks incorrectly.
The LrDialogs namespace is not available during LrShutdownApp. Do not attempt to show dialogs in shutdown scripts.
On Mac, successive withWriteAccessDo calls without user interaction coalesce into a single undo event. Design accordingly if undo granularity matters.
const { core, app, imaging } = require('photoshop');
await core.executeAsModal(async () => {
const doc = app.activeDocument;
const result = await imaging.getPixels({
documentID: doc.id,
targetSize: { width: 256, height: 256 }, // pyramid cache
colorSpace: "RGB",
componentSize: 8
});
const pixels = await result.imageData.getData(); // Uint8Array, interleaved RGB
// ... process ...
result.imageData.dispose(); // mandatory
}, { commandName: "Analyze" });const imageData = await imaging.createImageDataFromBuffer(buffer, {
width, height, components: 3, colorSpace: "RGB"
});
const jpegBase64 = await imaging.encodeImageData({ imageData, base64: true });
img.src = "data:image/jpeg;base64," + jpegBase64;
imageData.dispose();const doc = app.activeDocument;
await core.executeAsModal(async (ctx) => {
await doc.suspendHistory(async () => {
const layer = await doc.createLayer();
await layer.applyGaussianBlur(5.0);
}, "My Edit");
}, { commandName: "Edit Layer" });const { action } = require('photoshop');
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" }
}], {});
// 5 reference forms: ID (_id), Index (_index), Name (_name), Enum (_enum/_value), Property (_property, bare — no _ref)multiGet for bulk reads (more efficient than multiple get calls):
const result = await action.batchPlay([{
_obj: "multiGet",
_target: [{ _ref: "document", _enum: "ordinal", _value: "targetEnum" }],
extendedReference: [["width", "height", "resolution", "mode"]]
}], {});Discovery: Use Photoshop's "Copy As JavaScript" from the Actions panel to capture descriptors. Use action.addNotificationListener to observe descriptors fired by manual operations.
| Component | Key attributes / events |
|---|---|
sp-action-button |
quiet, selected, disabled, click |
sp-action-group |
compact, justified, quiet — SWC-originated, needs enableSWCSupport |
sp-action-menu |
placement, quiet, change — menu with sp-menu-item children; SWC-originated, needs enableSWCSupport |
sp-body |
Typography body text |
sp-button |
variant (cta default/primary/secondary/warning/overBackground), quiet (not with cta), disabled, click |
sp-button-group |
Container for button sets — SWC-originated, needs enableSWCSupport |
sp-checkbox |
checked, indeterminate, disabled, change |
sp-detail |
Typography detail text |
sp-divider |
size (small/medium/large) |
sp-dropdown |
placeholder, quiet, change — wraps sp-menu |
sp-heading |
Typography heading text |
sp-icon |
name, size (xxs/xs/s/m/l/xl/xxl) — 36 built-in icons |
sp-label |
Typography label text |
sp-link |
href, quiet |
sp-menu |
Wraps sp-menu-item and sp-menu-divider |
sp-menu-item |
selected, disabled |
sp-progressbar |
max, value, value-label, show-value, size — label via <sp-label slot="label"> |
sp-radio |
checked, disabled — listen for change on the group |
sp-radio-group |
column, change (read evt.target.value) |
sp-slider |
min, max, value, disabled, variant="filled", show-value, input/change — no step |
sp-textfield |
placeholder, quiet, type, disabled, input/change |
sp-textarea |
placeholder, quiet, disabled, input/change |
SWC (Spectrum Web Components): npm-wrapped packages for advanced Spectrum UI. Requires enableSWCSupport: true in manifest (which implicitly enables CSSNextSupport). Locked to v0.37.0 in UXP 8.0. Provides components not available as sp-* (picker, number-field, toast, tabs, accordion, table, etc.).
Supported: flexbox (all properties), box model (margin, padding, border, width/height, overflow), typography (font-family, font-size, font-weight, line-height, text-align, color, text-decoration, white-space, word-wrap), backgrounds (background-color, background-image incl. linear-gradient/radial-gradient since v2.0 — v8.1 aligned gradient start/end syntax with the web), opacity, calc(), CSS custom properties (--var), pseudo-classes (:hover, :nth-child, :first-child, …) and ::before/::after (v3.0+).
With CSSNextSupport flag: box-shadow, transform-origin, scaleX, scaleY, translate.
NOT supported: Grid, transition, animation/@keyframes, float, text-transform, font shorthand, position: sticky.
Theme CSS variables (auto-update on theme change):
--uxp-host-background-color, --uxp-host-text-color, --uxp-host-text-color-secondary, --uxp-host-border-color, --uxp-host-link-text-color, --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. Also supports @media (prefers-color-scheme: dark|light). Spectrum sp-* widgets auto-theme without extra CSS.
const { entrypoints } = require("uxp");
entrypoints.setup({
plugin: { create() {}, destroy() {} },
panels: {
myPanel: {
create() { /* DOM ready — set up listeners */ },
show() { /* fires once, not on every show (PS-57284) */ },
hide() { /* never fires (PS-57284) */ },
destroy() { /* cleanup */ },
menuItems: [
{ id: "reload", label: "Reload", enabled: true, checked: false }
// submenu: [...] for nesting; ids must be unique per panel
],
invokeMenu(menuId) { // flyout clicks arrive here, not per-item
if (menuId === "reload") refresh();
}
}
},
commands: {
myCommand: { run() { /* one-shot execution */ } }
}
});- Panel
show/hideare unreliable (PS-57284). Usedocument.visibilitychangeif available. - 300ms timeout on panel
create— defer heavy init withsetTimeout.
{
"manifestVersion": 5,
"id": "com.example.myplugin",
"name": "My Plugin",
"version": "1.0.0",
"main": "index.html",
"host": { "app": "PS", "minVersion": "24.0.0",
"data": { "apiVersion": 2, "loadEvent": "use" } },
"requiredPermissions": {
"localFileSystem": "fullAccess",
"network": { "domains": ["https://api.example.com"] },
"clipboard": "readAndWrite",
"webview": { "domains": ["https://example.com"] }
},
"featureFlags": { "enableSWCSupport": true, "CSSNextSupport": true }
}apiVersion: 2required for modern imaging APIloadEvent: "use"(lazy) vs"startup"(eager)enableMenuRecording: true— makes the plugin's menu commands recordable as Action steps ("Plugin Menu Command"); the separaterecordActionAPI needs no flag
const { storage } = require('uxp');
const fs = storage.localFileSystem;
const file = await fs.getFileForOpening({ types: storage.fileTypes.images }); // read-only
const out = await fs.getFileForSaving("out.png", { types: ["png"] }); // null if cancelled
const data = await fs.getDataFolder(); // persistent plugin storage
await out.write(buffer, { format: storage.formats.binary });
// batchPlay needs a session token, not an entry or path:
const token = fs.createSessionToken(file);
await action.batchPlay([{ _obj: "open", target: { _path: token, _kind: "local" } }], {});Where to persist: prefs → localStorage or a plugin-data:/ file; secrets → storage.secureStorage (encrypted; getItem returns Uint8Array; can be lost — treat as cache); remembered file access → createPersistentToken. Full API in ps-uxp-sdk.md (File I/O & Storage).
const { action, core } = require('photoshop');
await action.addNotificationListener(
['set', 'select', 'make', 'delete', 'open'],
(name, desc) => scheduleRefresh()
);
// 218 action events available
// Prefer userIdle for heavy re-renders:
await core.addNotificationListener('UI', ['userIdle'], refresh);await core.executeAsModal(async (executionContext) => {
// executionContext provides: isCancelled, onCancel, reportProgress
// hostControl provides: suspendHistory, resumeHistory,
// registerAutoCloseDocument, unregisterAutoCloseDocument
const hostControl = executionContext.hostControl;
const suspensionID = await hostControl.suspendHistory({ documentID: doc.id, name: "My Op" });
// ... operations ...
await hostControl.resumeHistory(suspensionID);
}, {
commandName: "My Operation",
// interactive: true, // allows UI interaction during modal
// timeOut: 5000, // v25.10: retry duration for modal collisions (ms)
});- PS only creates a history state if document was actually modified
- Never swallow exceptions — prevents cancel propagation
- Nested
executeAsModalcalls are allowed
Info.lua → LrInitPlugin → on-demand script loading (or LrForceInitPlugin at startup)
LrShutdownAppreturns{ LrShutdownFunction = function(doneFunc, progressFunc) }with 10-second timeoutLrDialogsis NOT available duringLrShutdownApp- Installation paths:
- macOS:
~/Library/Application Support/Adobe/Lightroom/Modules/ - Windows:
%APPDATA%\Adobe\Lightroom\Modules\
- macOS:
- Auto-loaded plugins can't be removed, only disabled
- Debugging:
LrLoggerwithenable('print')orenable('logfile')- Mac logs:
~/Library/Logs/Adobe/Lightroom/LrClassicLogs/ - Windows logs:
%LOCALAPPDATA%\Adobe\Lightroom\Logs\LrClassicLogs\
- Mac logs:
LrFunctionContext.callWithContext("observer", function(context)
LrDevelopController.addAdjustmentChangeObserver(context, {}, function()
scheduleRerender()
end)
end)local done = false
local request = photo:requestJpegThumbnail(w, h, function(jpegData, errorMsg)
if done then return end -- mandatory guard
if not jpegData then return end
done = true
LrFileUtils.writeFile(tempJpegPath, jpegData)
jpegData = nil -- release reference
LrTasks.execute(binaryPath .. " decode --input " .. tempJpegPath .. " ...")
end)
-- Must hold `request` reference or callback may never fire (GC)local frameIndex = 0
local function nextScopePath()
frameIndex = (frameIndex + 1) % 2
return scopeDir .. "/scope_" .. frameIndex .. ".jpg"
end
viewFactory:picture { value = LrView.bind("scopePath"), width = 256, height = 256 }
-- On update: properties.scopePath = nextScopePath() then write the file therelocal _settleVersion = 0
local _busy = false
local _pending = false
local function scheduleRerender()
_settleVersion = _settleVersion + 1
local myVersion = _settleVersion
LrTasks.startAsyncTask(function()
LrTasks.sleep(0.1) -- settle window
if myVersion ~= _settleVersion then return end -- superseded
if _busy then _pending = true; return end -- coalesce
_busy = true
-- ... export + render ...
_busy = false
if _pending then _pending = false; scheduleRerender() end
end)
end- Required:
LrSdkVersion,LrToolkitIdentifier(reverse-domain ID),LrPluginName(2.0+) - Menus:
LrLibraryMenuItems(Library > Plug-in Extras),LrExportMenuItems(File > Plug-in Extras),LrHelpMenuItems - Providers:
LrExportServiceProvider(export/publish),LrExportFilterProvider,LrMetadataProvider,LrMetadataTagsetFactory - Lifecycle:
LrInitPlugin,LrForceInitPlugin(4.0 — init at startup),LrEnablePlugin/LrDisablePlugin(3.0),LrShutdownPlugin(3.0),LrShutdownApp(4.0 — LrDialogs NOT available) - Plugin Manager:
LrPluginInfoProvider,LrPluginInfoUrl; Misc:LrSdkMinimumVersion,URLHandler(4.0 — script file, string),LrAlsoUseBuiltInTranslations,LrLimitNumberOfTempRenditions(Boolean throttle — plugin must delete each temp rendition),VERSION{ major, minor, revision (numbers), build (string), display (string?) }
-- Basic binding
f:edit_field { value = LrView.bind("myKey") }
-- Transform binding (display conversion)
f:static_text { title = LrView.bind { key = "tempC", transform = function(value)
return string.format("%.1f F", value * 9/5 + 32)
end } }
-- Conditional enablement
f:push_button { enabled = LrBinding.keyEquals("mode", "advanced") }
-- Cross-table binding
f:edit_field { value = LrView.bind { bind_to_object = otherTable, key = "sharedKey" } }
-- Observer pattern (guard flag prevents loops)
local updating = false
props:addObserver("inputKey", function(t, k, v)
if updating then return end
updating = true
t.outputKey = v * 2
updating = false
end)lrc-sdk.md is the single source of truth for: every develop parameter by panel (adjustPanel, tonePanel, mixerPanel, colorGradingPanel, detailPanel, effectsPanel, lensCorrectionsPanel, calibratePanel, lensBlurPanel, plus straightenAngle), local-adjustment parameters by process version (v2: 8 · v3/v4: 18 · v5: 25 · v6: 27), plugin develop presets, and all 95 LrDevelopController functions with exact signatures (get/set + tracking, tools, masks SDK 11+, spots SDK 14.1+, AI/enhance SDK 14.5+, point color SDK 13.2+, lens blur SDK 13.3+, color grading, process version, remove panel, navigation/reset).
Most-used calls (parameter names are case-sensitive):
LrDevelopController.getValue(param) -- current value
LrDevelopController.setValue(param, value) -- set; wrap drags with startTracking(param)/stopTracking()
LrDevelopController.getRange(param) -- min, max
LrDevelopController.addAdjustmentChangeObserver(context, {}, callback)
LrDevelopController.revealPanel("adjustPanel") -- panel ID or param name
LrDevelopController.selectTool("masking")| Mistake | Fix |
|---|---|
Calling canvas.toDataURL() in UXP |
Not supported. Use imaging.encodeImageData({ base64: true }). |
Skipping imageData.dispose() |
Leaks native memory. UDT warns at 600MB. |
Writing LrC f:picture to same path repeatedly |
LrC caches every version — massive leak. Alternate two paths. |
Running collectgarbage() in LrC |
Blocked by sandbox. Design around it (fixed-size state, path alternation). |
| No debounce on LrC develop observer | Drags spawn hundreds of coroutines. Use version-counter pattern. |
CSS Grid / transform / transition in UXP |
Grid and transitions not supported. Transforms require CSSNextSupport flag. |
z-index for overlays in UXP |
Supported, but nothing can overlay a text-editing widget. Prefer DOM/flex order for overlays. |
| 16-bit UXP pixels expecting 0-65535 | Default is 0-32768. Pass { fullRange: true } to getData(). |
| Trying to add LrC UI to Develop panel | Not a supported plugin type. Use floating dialog from Library menu. |
Polling inside requestJpegThumbnail callback without done guard |
Callback may fire more than once. Set and check done. |
Using <label for="id"> in UXP |
Not supported. Wrap the label around the control. |
| Swallowing batchPlay exceptions in try/catch | Prevents cancel propagation. Let exceptions from executeAsModal propagate. |
Using CSS transforms without CSSNextSupport flag |
Properties silently ignored. Add featureFlags: { "CSSNextSupport": true } to manifest. |
Creating tasks inside processRenderedPhotos |
LrC already runs it in a task. Nesting tasks causes incorrect behavior. |
Calling LrDialogs during LrShutdownApp |
Namespace unavailable during app shutdown. Use LrShutdownPlugin instead. |
Not holding requestJpegThumbnail return value |
May be garbage collected, callback never fires. Store in a local variable. |
Using break in LrC directoryEntries/files/recursiveFiles loops |
Not safe. Collect results into a table or use a flag + early return. |
Modifying nested tables in LrPrefs expecting auto-save |
Only top-level key reassignment triggers persistence. Reassign the root key after mutating nested values. Use prefs:pairs() not Lua pairs(prefs). |
Passing a path string to saveAs/open/batchPlay in UXP |
DOM methods take storage File entries; batchPlay _path takes fs.createSessionToken(entry). |
Storing secrets in UXP localStorage |
Use storage.secureStorage — but treat it as a re-obtainable cache; getItem returns a Uint8Array. |
lrc-sdk.md— Full Lightroom Classic SDK reference: all plugin types (export, publish, filter, metadata),Info.luamanifest, packaging (.lrplugin/.lrdevplugin),LrViewwidgets and data binding,LrDevelopController(95 functions) + develop parameter catalogs, plugin develop presets, LrCatalog method inventory, programmatic export (LrExportSession+ destination keys), publish-service callback catalog, full module catalog (45+ modules), LrC memory-leak prevention details, external communication (LrSocket, LrHttp, Controller SDK), and the processor CLI spec for LrC-to-Rust bridging.ps-uxp-sdk.md— Full Photoshop UXP reference: Manifest v5, Document/Layer DOM (36+29 Document, 28+55 Layer properties/methods, 38 filter methods), Selection class, Imaging API full signatures + ImageBlob display, File I/O & storage (pickers, plugin folders, session/persistent tokens, secureStorage), batchPlay and action system (5 reference forms, action recording), executeAsModal details,.psjsscripts,photoshop.constants, UDT workflow, XMP metadata, programmatic path creation, localization, Spectrum UXP + SWC component catalogs, HTML/CSS support and limitations, Canvas API limits, event system, text/typography API, color management, external communication (WebView, hybrid C++ bridge, network, shell), and the known-issues catalog.
When to load deep references:
ps-uxp-sdk.md— when using batchPlay descriptors, Document/Layer DOM methods, Selection class, filter methods, file I/O and tokens, SWC components, text/typography APIs, or debugging known issueslrc-sdk.md— when building export/publish services (callbacks, programmatic export), working with catalog queries (findPhotos, custom metadata, keywords, collections), applying develop presets, building UIs withLrDialogs.presentFloatingDialog, using LrSocket/Controller SDK, or needing the full module API reference
The quick-ref snippets above cover common cases. Load deep references only for non-trivial implementations.