Using Adobe’s JavaScript-based ExtendScript (jsx) to Automate Photoshop Workflow Tasks

Andrew Rapo
7 min readFeb 27, 2019

--

Automating Photoshop Workflow Tasks

This article describes an approach to automating the Photoshop workflow for an interactive storybook project. The project involves dozens (maybe hundreds) of Photoshop files with numerous layers that must be turned into bezier paths and exported for use in an interactive storybook engine. Although many of the steps can be done manually, a much better approach is to use ExtendScript, a variation of JavaScript that is built into many of Adobe’s applications. Not only can this save time, it can also provide a way to extract data from PSD files that can’t be accessed manually — like bezier path data.

ExtendScript (jsx)

Not to be confused with React’s .jsx files, ExtendScript files usually have a .jsx extension and contain a combination of JavaScript and special, Adobe-specific directives.

Examples Repo

All of the code examples described in this article are available in this github repo: https://github.com/mitmedialab/storybook-photoshop-jsx

Hello World

A good way to get started is with hello-world.jsx (jsx-examples/hello-world.jsx)

alert('Hello, world.');

Run the script by opening it in Photoshop: File->Scripts->Browse… then navigate to the script. You should see:

A Real-world Example: Peaches.psd

The peaches.psd file (psd/peaches.psd) contains a hypothetical scene from an interactive storybook and includes four layers:

  • guide_tl (a guide layer)
  • seed (an object layer containing an image of the peach seed/pit)
  • peach (an object layer containing an image of the peach)
  • bg (the background layer)
peaches.psd

The above-mentioned storybook engine needs information from each layer so it can manipulate the objects in the scene. In particular, the outlines of each object need to be exported as as bezier point data. The engine also needs info from the guide layer which indicates where text can be drawn in the scene — in this case, the top left corner (i.e. ‘tl’). Finally the engine needs a jpeg image that includes all the layers except the guide layer(s).

The Scripts

All of this is accomplished by a script called process-file.jsx which #includes a handful of scripts so it can make use of the functions defined in each of them. All of the functions could be included in one long script, but this modular approach has advantages. It makes it easy to utilize and troubleshoot each script individually. And it makes the project easier to explain. The complete set of scripts includes:

  • get-layers.jsx: gathers layer data into a JSON object
  • get-layer-path.jsx: creates a new Work Path for the layer
  • get-bounds.jsx: gets the bounds of the current selection
  • get-path-data.jsx: gathers bezier point data into a JSON object
  • export-svg.jsx: exports an svg file version of a path
  • save-image.jsx: saves a jpeg version of the file, excluding guide layers
  • process-file.jsx: choreographs all of the above scripts

Testing the script with: do-[script-name].jsx

Each of the above-mentioned scripts acts as a mini library, defining a number of functions. To make running/testing easier, each script has a companion script that can be used to run/test it. These are named like do-[script-name].jsx. For example, do-get-layers.jsx will execute the primary function in get-layers.jsx, getLayerData() , and return the resulting JSON object.

get-layers.jsx

get-layers.jsx defines a function, getLayerData(), that returns metadata about all the layers in the PSD. The code looks like:

In the above code, var allLayers = app.activeDocument.layers; returns an array containing references to all of the layers in the active document (PSD) and allLayers[i].name provides access to each layer’s name. The JSON returned by getLayerData() looks like:

{
"layerNames": [
"guide_tl",
"seed",
"peach",
"bg"
],
"layerCount": 4,
"layers": [
{
"type": "GUIDE",
"name": "guide_tl"
},
{
"type": "OBJ",
"name": "seed"
},
{
"type": "OBJ",
"name": "peach"
},
{
"type": "BG",
"name": "bg"
}
]
}

get-layer-path.jsx

get-layer-path.jsx defines getPathWithLayerName(layerName) which creates a new Work Path and returns both metadata about that path and a reference to the new path, itself.

The getTightSelectionWithLayerName(layerName) function creates a new selection that includes only the non-transparent pixels on the specified layer. This is accomplished by simulating the menu action Select->All followed by simulating a click on the Move tool and then simulating using the arrow keys to nudge the selection right and then left. (If anyone knows a more code-oriented way to accomplish this, let me know!)

Btw: A good way to figure out how to simulate manual UI interactions in Photoshop is to use the ScriptingListenerPlugin provided by Adobe: https://helpx.adobe.com/photoshop/kb/downloadable-plugins-and-content.html#ScriptingListenerplugin

Finally, selectionToWorkPath() generates a new path (Work Path) based on the selection.

The get-layer-path.jsx code looks like this:

get-bounds.jsx

get-bounds.jsx defines the function, getSelectionBounds(), which return a JSON object containing parameters describing the bounds of the current selection:

The JSON returned looks like:

{
"xLeft": 363,
"yTop": 281,
"xRight": 837,
"yBottom": 606
}

get-path-data.jsx

get-path-data.jsx defines two functions which take a path object as an argument and return either an SVG or bezier representation of the path. pathToSvgData(path) returns an SVG string which looks like:

M689 281C710.435221285203 282.050722610512 734.938543937045 281.48934292098 751 288C823.962562426532 317.576037297225 869.579783631659 413.905746219374 810 493C787.119662197185 523.374451440617 755.729558281528 536.583852141727 708 543C687.6687 542.3334 667.3313 541.6666 647 541C638.632720583811 545.328833790592 635.113792760834 555.358477449161 628 561C603.358734520226 580.541510353059 574.502693773079 596.390451588511 538 604C436.012088397763 625.260950097228 348.762579462231 531.646136186465 365 434C367.957429932036 416.215056558162 367.058265877123 400.856198210928 373 387C397.407713092162 330.080908117657 455.515078242546 356.540803684843 518 346C533.154518570513 343.443530363851 571.662669952291 351.680893824889 574 350C594.880015894225 324.146012684335 609.3388741798 298.195654143152 644 286

pathToBezierData(path) returns a JSON object containing an array of bezier points, each of which has an anchor point, a leftDirection point and a rightDirection point:

[
{
"anchor": {
"x": 689,
"y": 281
},
"leftDirection": {
"x": 710.435221285203,
"y": 282.050722610512
},
"rightDirection": {
"x": 675.597084969992,
"y": 286.152049962991
}
},
...
{
"anchor": {
"x": 644,
"y": 286
},
"leftDirection": {
"x": 655.968336099604,
"y": 281.788893629799
},
"rightDirection": {
"x": 609.3388741798,
"y": 298.195654143152
}
}
]

export-svg.jsx

export-svg.jsx defines exportSvgWithPathData(svgPathData, filename, documentBounds) which embeds the SVG path data in an xml object and saves it to the filesystem:

save-image.jsx

save-image.jsx defines saveJpeg(name) which writes the current PSD to a .jpg file.

process-file.jsx

As noted at the start of the article, process-file.jsx choreographs the activities of all the other scripts to accomplish the following:

  • Creates a JSON object to hold metadata from the PSD
  • Resizes the PSD, if necessary
  • Finds the bounds (size) of the PSD
  • Iterates through the layers of the PSD
  • Determines the type of each layer
  • Generates a path outline for each object layer
  • Saves the path data to an SVG file
  • Adds the bezier path data to the JSON object
  • Hides all guide layers
  • Saves the JSON data to a file
  • Saves a .jpg version of the PSD to a file

do-storybook-palette.jsx

All of the scripts mentioned above can be easily invoked through the File-Scripts->Browse… menu, but there are ways to add custom UI to Photoshop. The best way to do this is by developing a custom Panel using the ExtendScript SDK. However, this is a fairly involved process. A simpler approach that may be adequate in some situations is to use a non-modal dialog — a Palette. The do-storybook-palette.jsx script instantiates a UI Palette that looks like this:

do-storybook-palette.jsx

This Palette is persistent and will float above the standard Photoshop UI until it is canceled. Because it is non-modal, you can keep working as normal while it is active. The buttons on the panel can be used to trigger scripts and it is easy to configure the panel with additional buttons and fields. The code looks like this:

As a way to streamline the process of invoking scripts, this approach works well.

Note: The responsiveness of the standard Photoshop UI is degraded when this kind of panel is active so use it only when needed.

Summary

Photoshop scripting is powerful. It has been around for a long time and it is easy to use. The examples in this article are designed to support a specific, interactive storybook project, but they can be repurposed and adapted and should make it easy to get started with Adobe ExtendScript.

(This code in this article was developed for Adobe Photoshop CC 2019 and has only been tested on this version.)

--

--

Andrew Rapo

AI Designer/Developer. Formerly at Disney, Warner Bros., Hasbro, Jibo, MIT Media Lab and Nuance. Now at NTT Disruption as Conversational/Character AI lead.