Creative Developer Tools (CRDT) is a growing suite of tools aimed at script developers
and plug-in developers for the Adobe Creative Cloud eco-system.
This module provides functions that are specific to Adobe Photoshop.
Started (2026) with a per-document config-layer API - a dedicated layer group used as an INI-style settings store inside the PSD itself, since Photoshop (unlike InDesign) has no pasteboard and most of its own preferences are global rather than per-document. See PXConfig (https://github.com/zwettemaan/PXConfig) for the first consumer.
Intended to grow into a general collection of Photoshop-specific UXP/UXPScript helpers over time, the way crdtuxpIDSN.js did for InDesign - not just the config-layer API.
and plug-in developers for the Adobe Creative Cloud eco-system.
This module provides functions that are specific to Adobe Photoshop.
Started (2026) with a per-document config-layer API - a dedicated layer group used as an INI-style settings store inside the PSD itself, since Photoshop (unlike InDesign) has no pasteboard and most of its own preferences are global rather than per-document. See PXConfig (https://github.com/zwettemaan/PXConfig) for the first consumer.
Intended to grow into a general collection of Photoshop-specific UXP/UXPScript helpers over time, the way crdtuxpIDSN.js did for InDesign - not just the config-layer API.
- Source:
Methods
(static) applyConfigFromGroup(doc, groupNameMarker, callback)
Find a config layer group, read it as INI data, and hand that data to a
caller-supplied callback - the "apply" part is always specific to
whatever product built the config layer (which sections/keys it
supports, what each one means), so that logic lives in the calling
script, not here. This function only owns the generic, reusable part:
find the group, extract the data, dispatch it.
- Source:
Parameters:
| Name | Type | Description |
|---|---|---|
doc |
Document
|
a Photoshop UXP `Document` |
groupNameMarker |
string
|
substring identifying the config layer group (e.g. `"[photoshop-config]"`) |
callback |
function
|
`function(configData, group, doc)`, called once the group is found and its content parsed; `configData` is whatever `readINI` returns (may be `undefined` if the group had no readable INI content - callbacks should handle that). May be `async` and/or return a Promise; whatever it returns/resolves to is returned by `applyConfigFromGroup` in turn. Mutating the document from inside the callback still needs `crdtuxpPHXS.runModal`, same as any other document edit. |
Returns:
the callback's return value, or `undefined` if no matching
group was found (callback is never called in that case) or the
arguments are invalid
(static) collectTextLayersSortedByX(group) → {Array.<Layer>}
Collect all text layers directly inside a layer group, sorted
left-to-right by the left edge of their bounds.
- Source:
Parameters:
| Name | Type | Description |
|---|---|---|
group |
Layer
|
a Photoshop UXP layer group |
Returns:
- Type:
-
Array.<Layer>
text layers, sorted by `bounds.left` ascending (empty
array if `group` has no text layers or is falsy)
(static) concatenateTextLayerContents(textLayers) → {string}
Concatenate the text contents of a list of text layers, one per line,
in the order given (see `collectTextLayersSortedByX` for left-to-right
column ordering).
- Source:
Parameters:
| Name | Type | Description |
|---|---|---|
textLayers |
Array.<Layer>
|
array of Photoshop UXP text layers |
Returns:
- Type:
-
string
contents joined with `"\n"` (empty string if
`textLayers` is empty or falsy)
(static) createLayerGroup(doc, name) → {Layer}
Create a new, empty top-level layer group.
Not independently verified against a real Photoshop install (no local
precedent to crib from when this was written), but `Document.createLayerGroup`
is Adobe's documented, commonly-shown UXP API for this - higher
confidence than `createTextLayer`'s `textItem` styling guesses below.
Must be called from inside `crdtuxpPHXS.runModal`.
- Source:
Parameters:
| Name | Type | Description |
|---|---|---|
doc |
Document
|
a Photoshop UXP `Document` |
name |
string
|
group name |
Returns:
- Type:
-
Layer
the new layer group, or `undefined` on failure
(static) createTextLayer(doc, parentGroup, name, contents, optionsopt) → {Layer}
Create a text layer inside a group, with position and basic style.
UNVERIFIED AGAINST A REAL PHOTOSHOP INSTALL - the overall shape
(`document.createLayer` with `LayerKind.TEXT`, then set `.textItem`
properties) matches Adobe's documented UXP Photoshop API, but the exact
`textItem` property names used for font/size/color have not been
confirmed live. Treat this as a first draft to verify/fix in one place
(both NewConfig and CleanConfig call this), not a proven implementation.
Must be called from inside `crdtuxpPHXS.runModal`.
- Source:
Parameters:
| Name | Type | Attributes | Description |
|---|---|---|---|
doc |
Document
|
a Photoshop UXP `Document` | |
parentGroup |
Layer
|
group to create the layer inside | |
name |
string
|
layer name | |
contents |
string
|
text contents | |
options |
object
|
<optional> |
`{ left, top, fontName, fontSizePt, color: {r,g,b} }`, all optional |
Returns:
- Type:
-
Layer
the new text layer, or `undefined` on failure
(static) deleteTextLayersInGroup(group, preserveNameSubstringopt) → {boolean}
Delete all text layers directly inside a group, optionally preserving
one whose name contains `preserveNameSubstring` (e.g. a border/frame
layer that isn't part of the text content). Must be called from inside
`crdtuxpPHXS.runModal`.
- Source:
Parameters:
| Name | Type | Attributes | Description |
|---|---|---|---|
group |
Layer
|
a Photoshop UXP layer group | |
preserveNameSubstring |
string
|
<optional> |
substring identifying a layer to leave alone (e.g. `"[config-border]"`) |
Returns:
- Type:
-
boolean
true on success
(static) deselectAllLayers(doc) → {boolean}
Deselect all layers in a document. Shorthand for `selectLayers(doc, [])`.
- Source:
Parameters:
| Name | Type | Description |
|---|---|---|
doc |
Document
|
a Photoshop UXP `Document` |
Returns:
- Type:
-
boolean
true on success
(static) findLayerGroupByNameSubstring(doc, nameSubstring) → {Layer}
Find the first top-level layer group in a document whose name contains
`nameSubstring`.
- Source:
Parameters:
| Name | Type | Description |
|---|---|---|
doc |
Document
|
a Photoshop UXP `Document` (e.g. `app.activeDocument`) |
nameSubstring |
string
|
substring to look for in top-level group names |
Returns:
- Type:
-
Layer
the matching layer group, or `undefined` if not found
(static) getBooleanFromINI(value) → {boolean}
Interpret a string as a boolean, the way an INI value would be: `y`, `n`,
`yes`, `no`, `true`, `false`, `t`, `f`, `0`, `1` (case-insensitive, only
the first character is actually examined, matching CRDT_ES's
`crdtes.getBooleanFromINI`).
- Source:
Parameters:
| Name | Type | Description |
|---|---|---|
value |
string
|
ini value |
Returns:
- Type:
-
boolean
value
(static) getPhotoshop() → {object}
Get the Photoshop UXP `photoshop` module's `app`/`constants` objects.
Centralises the `require("photoshop")` call so every function below goes
through the same spot.
- Source:
Returns:
- Type:
-
object
`{ app, constants }`, or `undefined` if not running
inside Photoshop's UXP host (e.g. required from a non-Photoshop context).
(static) moveLayerToBeginning(layer) → {boolean}
Move a layer to the top of its own stack (within its parent group, or
within the document if it's a top-level layer).
- Source:
Parameters:
| Name | Type | Description |
|---|---|---|
layer |
Layer
|
a Photoshop UXP layer (or layer group); must have a `.parent` (a top-level layer's parent is the document itself) |
Returns:
- Type:
-
boolean
true on success
(static) moveLayerToEnd(layer) → {boolean}
Move a layer to the bottom of its own stack (within its parent group, or
within the document if it's a top-level layer).
- Source:
Parameters:
| Name | Type | Description |
|---|---|---|
layer |
Layer
|
a Photoshop UXP layer (or layer group); must have a `.parent` (a top-level layer's parent is the document itself) |
Returns:
- Type:
-
boolean
true on success
(static) parseHexColor(hexString) → {object}
Parse a `#RRGGBB` or `RRGGBB` hex color string (leading `#` optional).
- Source:
Parameters:
| Name | Type | Description |
|---|---|---|
hexString |
string
|
e.g. `"#AACCFF"` or `"AACCFF"` |
Returns:
- Type:
-
object
`{ r, g, b }` as integers 0-255, or `undefined` if
`hexString` is not a valid 6-digit hex color
(static) readConfigFromGroup(doc, groupNameMarker) → {object}
Find a config layer group in a document, read its text layers and parse
them as INI data, in one call. Equivalent to:
`readINI(concatenateTextLayerContents(collectTextLayersSortedByX(findLayerGroupByNameSubstring(doc, groupNameMarker))))`
- Source:
Parameters:
| Name | Type | Description |
|---|---|---|
doc |
Document
|
a Photoshop UXP `Document` |
groupNameMarker |
string
|
substring identifying the config layer group (e.g. `"[photoshop-config]"`) |
Returns:
- Type:
-
object
the parsed INI object, or `undefined` if no matching
group was found or it had no readable INI content
(static) readINI(text) → {object}
Parse INI-formatted text into a nested object, one property per section,
one property per key within each section. Direct port of CRDT_ES's
`crdtes.readINI` (character-by-character state machine) - keep both in
sync if either changes. Behaviour:
- `[section-name]` starts a section; everything until the next `[section]` line belongs to it.
- `key = value` sets an attribute (spaces around `=` optional).
- Section/attribute names are lowercased, de-spaced, and stripped down to `[-a-zA-Z0-9_$]` (section names additionally keep `:`).
- The original, un-normalised section name is kept on the section object as `__rawSectionName`.
- A value wrapped in matching straight or curly quotes (`"..."`, `'...'`) has the quotes stripped; internal spaces are preserved.
- Duplicate section or attribute names are suffixed `_2`, `_3`, etc.
- A line starting with `#` is a comment. Any other line that isn't a `[section]` or `key = value` line (including one starting with `;`) is silently ignored - it never reaches a `=`, so nothing is stored.
- `[section-name]` starts a section; everything until the next `[section]` line belongs to it.
- `key = value` sets an attribute (spaces around `=` optional).
- Section/attribute names are lowercased, de-spaced, and stripped down to `[-a-zA-Z0-9_$]` (section names additionally keep `:`).
- The original, un-normalised section name is kept on the section object as `__rawSectionName`.
- A value wrapped in matching straight or curly quotes (`"..."`, `'...'`) has the quotes stripped; internal spaces are preserved.
- Duplicate section or attribute names are suffixed `_2`, `_3`, etc.
- A line starting with `#` is a comment. Any other line that isn't a `[section]` or `key = value` line (including one starting with `;`) is silently ignored - it never reaches a `=`, so nothing is stored.
- Source:
Parameters:
| Name | Type | Description |
|---|---|---|
text |
string
|
raw text, which might or might not contain some INI-formatted data mixed with normal text |
Returns:
- Type:
-
object
the parsed INI data, or `undefined` if `text` is empty/
not a string, or contains no recognisable `[section]`
(static) reformatINILines(text) → {Array.<string>}
Reformat raw text as a tidy, consistent set of INI lines, WITHOUT parsing
it into an object - unlike `readINI`, comments and blank lines are
preserved so this is safe to use on content a human has annotated. Per
line:
- Blank lines are kept as-is.
- Lines starting with `;` or `#` (comments) are kept as-is.
- `[section]` headers are normalised the same way `readINI` normalises section names (lowercased, de-spaced, stripped to `[-a-zA-Z0-9_$:]`).
- `key = value` lines have their spacing normalised to `key = value` (the key itself is trimmed but NOT otherwise normalised/case-folded, unlike `readINI` - this is a display reformat, not a re-parse).
- Any other line is discarded.
- Blank lines are kept as-is.
- Lines starting with `;` or `#` (comments) are kept as-is.
- `[section]` headers are normalised the same way `readINI` normalises section names (lowercased, de-spaced, stripped to `[-a-zA-Z0-9_$:]`).
- `key = value` lines have their spacing normalised to `key = value` (the key itself is trimmed but NOT otherwise normalised/case-folded, unlike `readINI` - this is a display reformat, not a re-parse).
- Any other line is discarded.
- Source:
Parameters:
| Name | Type | Description |
|---|---|---|
text |
string
|
raw text to reformat |
Returns:
- Type:
-
Array.<string>
the reformatted lines (empty array if `text` is
empty/not a string)
(static) runModal(callback, commandName) → {Promise}
Run a callback inside Photoshop's required `executeAsModal` wrapper -
needed for any document-mutating call (layer create/move/visibility/
selection, etc.), which Photoshop's UXP API rejects outside of one.
Centralised here so every PXConfig-style script uses the same wrapper
instead of each reimplementing it.
- Source:
Parameters:
| Name | Type | Description |
|---|---|---|
callback |
function
|
function to run inside the modal scope; may be `async` and/or return a Promise |
commandName |
string
|
label shown in Photoshop's Undo history |
Returns:
- Type:
-
Promise
resolves with `callback`'s return value, rejects if
`callback` throws or this isn't running inside Photoshop
(static) selectLayers(doc, layers) → {boolean}
Set the document's selected layers.
- Source:
Parameters:
| Name | Type | Description |
|---|---|---|
doc |
Document
|
a Photoshop UXP `Document` |
layers |
Array.<Layer>
|
layers to select; pass an empty array (or `undefined`) to deselect everything |
Returns:
- Type:
-
boolean
true on success
(static) setLayerGroupExpanded(group, expanded) → {boolean}
Expand or collapse a layer group's twirl-down in the Layers panel.
NOT YET IMPLEMENTED - the Layers-panel expand/collapse state has no
documented direct UXP DOM property (same gap exists in classic
ExtendScript), and likely needs `batchPlay` - not yet researched/
verified. Exists as a named stub so callers (ShowConfig/HideConfig) have
one place to call and one place to fix later, instead of each
reimplementing/skipping this independently. Note: selecting a layer
inside a collapsed group auto-expands it in the UI as a side effect, so
a caller that only needs the group's content visible/selectable (e.g.
ShowConfig selecting its first text layer via `selectLayers`) may not
need this at all.
- Source:
Parameters:
| Name | Type | Description |
|---|---|---|
group |
Layer
|
a Photoshop UXP layer group |
expanded |
boolean
|
true to expand, false to collapse |
Returns:
- Type:
-
boolean
always `false` currently
(static) setLayerVisible(layer, visible) → {boolean}
Show or hide a layer.
- Source:
Parameters:
| Name | Type | Description |
|---|---|---|
layer |
Layer
|
a Photoshop UXP layer (or layer group) |
visible |
boolean
|
true to show, false to hide |
Returns:
- Type:
-
boolean
true on success
(static) splitLinesIntoColumns(lines, linesPerColumn) → {Array.<string>}
Split a list of lines into column-sized chunks, each chunk joined back
into a single string with `"\n"` - the inverse of
`concatenateTextLayerContents`/`collectTextLayersSortedByX`.
- Source:
Parameters:
| Name | Type | Description |
|---|---|---|
lines |
Array.<string>
|
lines to split |
linesPerColumn |
number
|
max lines per column (must be > 0) |
Returns:
- Type:
-
Array.<string>
one string per column, left-to-right order