You are on page 1of 408

Bought To You By

VFXdownload.net
Professional Photoshop Scripting
Davide Barranca

Version 1.0.0 published on 2019-02-12


© 2016 - 2019 Davide Barranca

This book is licensed to One Single User only.


Enterprise license for teams up to ten people strong are available for purchase.
I trust you to not abuse my confidence, no DRM is involved.

This is a Leanpub book. Leanpub empowers authors and publishers with the Lean Publishing
process. Lean Publishing is the act of publishing an in-progress ebook using lightweight tools
and many iterations to get reader feedback, pivot until you have the right book and build
traction once you do.

Adobe® Photoshop® and Adobe® Creative Cloud® are registered trademarks of Adobe Systems
Incorporated in the United States and/or other countries. All other trademarks are the property
of their respective owners.

THIS PRODUCT IS NOT ENDORSED OR SPONSORED BY ADOBE SYSTEMS INCORPORATED,


PUBLISHER OF ADOBE PHOTOSHOP AND THE ADOBE CREATIVE CLOUD.
To my wife Elena, and my daughter Anita
Contents

Foreword ......................................................................................................................................... i

Course presentation ...................................................................................................................... ii


Why I wrote this book ............................................................................................................. ii
Audience and Assumptions .................................................................................................... iii
What you need to get started................................................................................................... iv
Conventions ..............................................................................................................................v
Version History and Errata .......................................................................................................v
1.0.0 ..............................................................................................................................v
0.2 EAP .........................................................................................................................v
0.1 EAP .........................................................................................................................v
Feedback ................................................................................................................................. vi
Piracy ...................................................................................................................................... vi

Content at a glance...................................................................................................................... vii

1. Photoshop Scripting............................................................................................................... 1
1.1 Photoshop Extensibility overview ................................................................................4
Actions ..........................................................................................................................4
Scripting ........................................................................................................................6
HTML Panels ................................................................................................................6
Plug-ins (Photoshop SDK)............................................................................................7
1.2 How Scripting works ....................................................................................................8
1.3 Languages and Documentation ...................................................................................11
ExtendScript vs. JavaScript ........................................................................................11
Photoshop and Core ExtendScript ..............................................................................12
Documentation ............................................................................................................13
JavaScript Primer for starters ......................................................................................14

2. Writing Scripts ..................................................................................................................... 16


2.1 Files, Extensions and Folders .....................................................................................16
2.2 Adobe ExtendScript Toolkit .......................................................................................16
2.3 Better Code Editors .....................................................................................................22
2.4 Running Scripts...........................................................................................................23
CONTENTS

3. Hello World by Examples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24


3.1 The Great-Grand Father of all Greetings . . . . . . . . . . . . . . . . . . . . . . . . . . 24
3.2 An Average problem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
3.3 Layers Spring cleaning . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
3.4 ESTK debugging . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
3.5 Homeworks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58

4. Climbing the DOM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60


4.1 The Tree of Scripting Life . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
4.2 Collections, Classes and Instances . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
4.3 Scripting practice with Algorithmic Art . . . . . . . . . . . . . . . . . . . . . . . . . . 63
4.4 Layers and LayerSets: the DOM approach . . . . . . . . . . . . . . . . . . . . . . . . . 77
4.5 Preferences . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84

5. The ExtendScript Domain . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87


5.1 Old but Gold . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
5.2 The Dollar object . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
5.3 The Reflection Interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
5.4 Preprocessor Directives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
5.5 Filesystem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
Folders . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
Files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
5.6 Notification Dialogs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
5.7 XML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108
5.8 Sockets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
5.9 Graphic User Interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
5.10 External Object . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
5.11 XMP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114
5.12 Localization . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
5.13 Operator overloading, Constants and UnitValue . . . . . . . . . . . . . . . . . . . . . 118

6. Action Manager . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121


6.1 Action Manager is not the DOM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
6.2 Action, Events, and historical perspective . . . . . . . . . . . . . . . . . . . . . . . . . 122
6.3 ScriptListener . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
6.4 charIDs and stringIDs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
6.5 ActionDescriptors and DialogModes: executeAction . . . . . . . . . . . . . . . . . . . 131
6.6 Building AM functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133
6.7 Extending the DOM with Action Manager . . . . . . . . . . . . . . . . . . . . . . . . . 138
6.8 Nested Descriptors and ActionLists . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
6.9 Getting data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
6.10 ActionReference . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150
6.11 Inspecting Descriptors: executeActionGet . . . . . . . . . . . . . . . . . . . . . . . . . 153
Application . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
CONTENTS

Descriptor Inspectors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154


Documents . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163
Layers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165
Channels . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172
Paths . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
History . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174
ActionSets and Actions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175
6.12 Getting Descriptors as Streams . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177
6.13 AM Setters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 181

7. User Interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 190


7.1 The technology dilemma . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 190
Compatibility . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 190
Behaviour . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 192
Appearance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193
Technology . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 194
7.2 ScriptUI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 195
7.3 Documentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 197
7.4 The Window object . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 197
7.5 Containers and Controls . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199
7.6 Layout . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201
Orientation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 202
align and alignChildren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 203
Margins and Spacing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 207
7.7 Coding Conventions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 208
7.8 Components minimal reference . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212
7.9 Events, and Event handlers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 223
Window and Controls Callbacks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 223
Event Listeners . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 226
Event Propagation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 232
Create and Simulate Events . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 234
7.10 Styling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 237
Custom elements and onDraw() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 241
7.11 Putting it all together . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 244
7.12 CEP Panels . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 258

8. Working with Metadata . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 264


8.1 XMP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 264
Schemas and Namespaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 265
XMPFile, XMPMeta, and XMPProperty . . . . . . . . . . . . . . . . . . . . . . . . . . 267
Editing the XMP Metadata . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 272
Custom Namespaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 277
Per Layer Metadata . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 280
CONTENTS

8.2 Generator Metadata . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 283


8.3 Photoshop Registry . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 283
8.4 External Files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 285
JSON Example: saving the GUI status . . . . . . . . . . . . . . . . . . . . . . . . . . . . 286
XML Example: a complete Preset system . . . . . . . . . . . . . . . . . . . . . . . . . . 290

9. Events . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 296
9.1 Script Events Manager . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 296
9.2 Notifiers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 300
Arguments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 302

10. Adobe Generator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 307


10.1 Plug-In development . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 309
Generator Getting Started . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 310
Document Info Options . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 317
10.2 Network Events . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 317
10.3 Debugging . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 318
10.4 Generator to JSX Communication . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 319
Using external .jsx files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 321
10.5 Bitmaps and Pixmaps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 322
Pixmap Options . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 327
10.6 Metadata . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 331
10.7 Connecting to external services . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 336
Artificial Intelligence: Face Detection . . . . . . . . . . . . . . . . . . . . . . . . . . . . 336
10.8 Socket.io Server/Client communication . . . . . . . . . . . . . . . . . . . . . . . . . . . 346
Sockets 101 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 346
Generator as a Server: CEP Panel interaction . . . . . . . . . . . . . . . . . . . . . . . 349
Generator as a Client: Photoshop remote control . . . . . . . . . . . . . . . . . . . . . 356
10.9 The Kevlar API for Generator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 365

11. Cross-Application Communication . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 367


11.1 Cross-DOM API . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 367
Startup Scripts folders . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 368
11.2 BridgeTalk concepts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 369
Static Methods and Properties . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 369
The BridgeTalk Instance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 370
Synch and Asynch behavior . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 373
11.3 Event handling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 374
DOM Objects returns . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 375
Receiving messages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 376
Sending intermediate responses . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 380
11.4 Wrap-up . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 383
12. Appendix . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 384
CONTENTS

12.1 Deploying Scripts .....................................................................................................384


Testing ........................................................................................................................... 384
Obfuscation ...............................................................................................................385
Installation ................................................................................................................387
12.2 Resources ..................................................................................................................389
Adobe’s Official .......................................................................................................389
Third-party Documentation ......................................................................................389
Third-party Forums and Groups ...............................................................................389
Software Developers’ websites .................................................................................389
ScriptUI .....................................................................................................................390
Text Editors Plug-ins.................................................................................................390
Libraries, Frameworks, and Repositories .................................................................390
Books ........................................................................................................................390

Acknowledgments ..................................................................................................................... 391

Bio ............................................................................................................................................... 392


Davide Barranca – Author ....................................................................................................392
Sandra Voelker – Technical Editor .......................................................................................392
Foreword
Since its inception in 1991, Photoshop has become the most powerful tool in the world of image
editing. Many people have pushed Photoshop to its limit for creative purposes, often to the point
where they daydream about tasks that Photoshop could more easily, more automatically. Enter
Photoshop’s scripting and extensibility capabilities, you an artist or a developer have the power in
your hands to solve more complex problems, be more creative, and save the day.
I still remember the day when I first went down the Photoshop scripting rabbit hole. My friend
John Nack, a former product manager for Photoshop and I, having recently started at Adobe then,
were attending a design conference. Michael Ninness, one of the presenters at the conference, was
lamenting that the Export Layers To Files functionality in Photoshop could only produce PSD or
JPEG files from the entire document width and height. John emailed to me asking if we could use
Export Layers To Files to trim each layer to the exact size of the object on each layer and save them
as PNG files. I wasn’t fully sure at that moment but I agreed to try. By the next morning, I had a
working script that I sent to John, who in turn gave it to Michael. Michael demoed the script at the
conference and shared it on the web. We literally had thousands of designers downloading the script
from the web until we shipped the changes with the next version of Photoshop. We had solved a
problem overnight. That is the power of scripting in Photoshop!
Anyone who has written a book can tell you that it is one of the hardest things they’ve done in their
life. There are hundreds or maybe thousands of books dedicated to teaching the use of Photoshop.
However, there are only a handful of people who have tackled writing a book that covers scripting
and extensibility, including Chandler McWilliams, Geoff Scott, and myself. I can attest that writing
a book about a subject so specific and niche as scripting Photoshop is a highly challenging task
that requires labor of love to accomplish. Davide Barranca, undoubtedly, has carefully and lovingly
assembled the most complete book on the subject.
I hope you appreciate and enjoy what Davide has put together with this book and you find a new
labor of love scripting Photoshop yourself.

Jeffrey Tranberry
Sr. Product Manager, Digital Imaging at Adobe Systems
Course presentation
Why I wrote this book
Back in 2014 I started a blog post series about Photoshop HTML Panels as a way to share with other
developers my own experiences, and help beginners (that is: me, a few months earlier) finding their
way through the forest. In late 2015 I looked back at the twenty-something strong collection of posts
and decided that time was ready for me to re-elaborate examples, tips, code chunks, workarounds,
and everything else I discovered on my daily fight with Panels, into a more proper form. I started
authoring the Adobe Photoshop HTML Panels Development Course (book and videos), published
in March 2016, which proved to be a success in filling gaps of the somehow erratic official Adobe’s
documentation. My primary goal was to let those approaching the topic for the first time master the
basics, give them an updated minefield map, and cover lots of the more advanced subjects on the
edge between Creative Cloud apps extensibility and web development for those who wanted to dig
deeper.
One year later from the HTML Panels first draft, here I am looking at Photoshop Scripting with the
same attitude. I’ve been part of the scripting community for quite some years now, and I owe to the
amazingly talented people in there most of what I know. Moreover, I’ve been a pest with Adobe’s
engineers - submitting bugs and following the scripting implementation over too many Photoshop
versions - so I’ve possibly a good grasp of the subject’s many facets, even if I’m far from mastering
each and every one of them. Knowledge is a cumulative process, and this very Course would like to
go in the direction of leveraging the community’s expertise at all levels. Compared to (say) InDesign,
I have the feeling that we, as a community, have a lower average understanding of scripting, with
a smaller elite of developers really mastering it – whereas the InDesign community has been able
to bring more people up to an averagely higher level – and they have their peak-talents as well, of
course. Is this fact a mere reflection of the importance one Adobe team has given over the years
to scripting, compared to the other? Is perhaps InDesign inherently more apt to be scripted? Just a
random fact? I cannot say¹.
What I know for sure first hand, is that if you want or need to approach Photoshop scripting
(especially as a total coding newcomer) the learning curve is terribly steep. You have language
problems, debugging problems, DOM problems, ActionManager problems, backward compatibility
problems, self-confidence, and alcohol problems, all mixed together.
When I initially got into scripting, back in two-thousand-something, I started fresh. I had previous
Fortran and ActionScript development experience, but by the time I put my hands on Photoshop
willing to write some code, I had forgotten every bit of them: my problem-solving skills were just
awful. My self-taught scripting curriculum hasn’t been linear at all: first, a lot of trial and error
¹It may also be that Photoshop has a larger script developers base, therefore the lower average level – but it’s just a supposition.
Course presentation iii

served with copy & paste aside. Then I’ve followed what I would describe like a vulture path: you
circle and circle, on and on, cyclically revisiting the same old topics, adding bits of understanding
each time. Meanwhile, I’ve deepened JavaScript and ExtendScript principles (from the language
point of view), so that I could strengthen my understanding; i.e., backing it up with some theory.
It’s a time-consuming process, dressed with a fair share of frustration.
This Course aims at disentangling as much as possible the Photoshop Scripting learning path – filling
the gaps in the official documentation by means of community-shared knowledge, and countless
head-banging-on-the-wall hours of personal experimentation.

Audience and Assumptions


The most challenging task, for me as an author, is to collect topics in a meaningful and helpful
order so that you, the reader, can exploit my work and prove it worth the money you’ve spent.
However, I can’t possibly do so without assessing what this Course’s target actually is. Should I start
from zero, and what would zero mean anyway? Back in September 2016, when beginning the first
draft, I’ve asked around to feel the pulse among colleagues and friends; with few exceptions, I’ve
been suggested to skip language basics and assume street-level JavaScript knowledge. We live in an
apparently web-driven world, and everybody vaguely interested in Photoshop scripting, they said,
either already nows JS, or has access to JS education of excellent quality – free or paid.
I would have embraced this approach blindly for several reasons. It leaves me with time and
resources to focus on actual scripting topics; variables, types, closures and the like are pain-in-the-
butt topics to cover, and I don’t want to risk being inaccurate; plenty of authors have already done it
way better than I could ever do; and so on. On the other side, when I started this journey long ago, I
struggled to learn JS and apply those concepts in Photoshop at the same time. Back then, strange as
it might appear now, the JS hype wasn’t exploded yet. JavaScript lived in the browser only, and so
were targeted – by default – all the available training resources. I had hard times reading about the
global window object, node elements, etc. and trying to understand how on earth that could fit in the
Photoshop DOM. True, years went by, and JS is everywhere (in the browser, outside the browser,
server-side…) – but isn’t this confusing even more the newcomer? Sure it is.
So I’ve decided to try a hybrid approach, crazy as it might sound. Total beginners, you won’t feel lost.
The first four Chapters lay down the essential elements that will help you with both the language
syntax and, perhaps more importantly, a proper coding mindset. You’ll have time to familiarize with
the Scripting architecture through examples: if/when needed, you can refer to the dedicated learning
resources that I’ve listed. Anyone else: do not skip to Chapter 5! The Course starts slow, but you need
to have a solid understanding of the basics if you want to master what follows. Anyway, I’m going
to stop, and cover in more detail useful programming concepts each time I find it appropriate –
because scripting doesn’t end with language knowledge: it starts with it.

Dedicated sections like this one dealing with advanced topics might punctuate the text flow
– feel free to skip them if you still need to grasp the basics of the discussed subject and get
back here later to dig deeper.
Course presentation iv

Conversely, these sections will contain guidance to better understand basic topics, with links
to external JavaScript resources, etc.

I must assume, instead, Photoshop knowledge: scripting a program means to operate it with code.
I can explain you with excruciating details, say, the reasons why when you turn left the steering
wheel, then the car’s front wheel turn left, the mechanic involved, etc. Yet, this doesn’t automatically
lead to driving knowledge – you must know yourself that suddenly turning left in the middle of a
bridge crossing a river is a bad idea.
That said, there are lots of people out there who struggle and fail to improve because of some topics
which have proved, over the years, to be true show-stoppers. One for all, ActionManager: as powerful
and as undocumented as voodoo – and not any less dangerous! In this Course, I’ve done my best to
give you unprecedented coverage of it, alongside with several other scripting black holes (ScriptUI,
Generator, etc.).
Lastly, if you come from a different Adobe application, be aware that Photoshop scripting has its
quirks and peculiarities, and it’s likely that you won’t be able to use directly, say, InDesign or After
Effect code in Photoshop without tweaking it.

What you need to get started


This Course’s code has been written in/for Photoshop-latest (CC2019) on a Mac, but scripting in PS
goes way back the pre-CC era and is theoretically platform independent². My interest in Photoshop
coding started with CS3, yet the oldest PS version I’ve installed on my machine now is just CS6.
With the obvious exception of features that weren’t available in the past (say, Artboards in CS6)
I’m confident that this code runs fine even in very early PS versions, and all the programming
concepts covered here apply to every PS version that supports scripting. For the sake of simplicity,
I will assume that you own a licensed copy of Photoshop (CS6 to CC-latest), and the corresponding
Adobe Bridge version.
With them, you should be able to download and install the ExtendScript Toolkit (also known as
ESTK, find it among the Creative Cloud available downloads), which is the closest thing we’ve been
given to an integrated IDE for writing and debugging scripts.
I’m editing this presentation in early 2019, few weeks after the public announce that Adobe is going
to replace ESTK with a Microsoft Visual Studio Code plugin, which is currently in beta-testing. I’ve
decided to keep the ESTK content I wrote in 2018, both because ESTK is not going to disappear
suddenly, and also for the concepts still hold. In later versions, when the VSCode plugin is released
and proved to be stable, I’ll progressively edit ESTK away.
You can develop on both OSX and Windows: I strongly suggest you to test the other platform before
releasing (or selling) your product to other people, since running into platform-specific issues is not
unfrequent.
²More on platform and version differences later in the book.
Course presentation v

Conventions
This book follows the standard technical publication conventions – such as highlighted code,
dedicated boxes with hopefully meaningful icons to explain what they’re about, etc. I’ll make use of
a small set of acronyms, e.g., for Adobe applications (PS for Photoshop, ID for InDesign, ESTK for
the ExtendScript Toolkit, etc.), and language names (JS for JavaScript, JSX for ExtendScript).

Version History and Errata


The present edition of this book is final, but it has been distributed from mid-2018 onwards as an
Early Access Program (EAP). Quoting the Lean Publishing Manifesto:
Lean Publishing is the act of publishing an in-progress book using lightweight tools and many
iterations to get reader feedback, pivot until you have the right book and build traction once you
do.
Below you’ll find information about both the Release and EAP versions.

1.0.0

February 2019 First final release. Compared to 0.2 EAP, these are the main differences.

• Two new Chapters: Cross-Application Communication and Appendix.


• Added traversing layers AM style to Chapter 6.
• Mentioned the newly announced VSCode plugin for Scripting development.
• A final round of proofreading: changed some graphics, fixed typos, etc.
• Added a proper Acknowledgements section.

0.2 EAP

June 2018.

• Added Chapter 10, +60 pages about Adobe Generator; it comes with several demo plug-ins that
I’ve built expressly for this course.

0.1 EAP

April 2018. First EAP release, Chapters 1-9 are in place.


Course presentation vi

Feedback
I’d love to hear from you! What is your background, what you’re going to build next, whether you’ve
found this book a good fit or not, and the reason why. If you want to share your thoughts, let me
know at davide.barranca@gmail.com.

Piracy
This Course is going to be pirated, like most of the published books, magazines, newspapers, and
videos for sale in the digital world. Some would also say that it’s a good thing, others would raise
the “let the one who has never sinned throw the first stone!” argument.
I’ve always self-published my works: this means that I do not rely on third-party publishing
companies to produce, design, edit, proofread, advertise, distribute, monitor and market my content.
Even though I earn (and pay taxes on) most of this Course’s revenue share myself, designing, editing,
proofreading, advertising, distributing, monitoring, marketing and producing content in the first
place is a horrible time- (and therefore money-) consuming process for me. I’m confident that you’re
willing to repay my efforts acquiring a legal copy of this Course, for which I thank you very much.
Content at a glance
1. Introduction to Scripting is an overview of the different technologies available to Photoshop
developers, and the peculiarities of scripting. I give you basic information on the ExtendScript
language, as well as reference documentation and links to third-party resources.
2. Writing Scripts deals with some practical aspects of coding, such as file types, default folders,
code editors and plugins. I cover the Adobe ExtendScript Toolkit, other documentation resources,
and how to run scripts.
3. Hello World by Examples is primarily dedicated to beginners with little or no experience at all
in programming. By means of three exercises of mild yet increasing difficulty, essential language
elements are described: from variables and objects to arrays and collections, conditionals and loops,
the dot and new operator, classes, instances, methods, properties, etc. The problems are approached
with a great emphasis on the reasoning, commenting both the code and the intentions behind it
thoroughly; things can go wrong, and they often do! You must learn how to manage both coding
errors and logic flaws, understand the debugging process and know where to look for information.
4. Climbing the DOM introduces the concept of the Document Object Model: the way Scripting
objects are contained and hierarchically ordered, and how to access them. I discuss the difference
between Collections, Classes, and Instances, their properties and methods. An exercise on Algo-
rithmic Art is the pretext to deal also with the coordinate system, colors, selections, and Scripting
logic. Layers, ArtLayers and LayerSets Collections are then put under the DOM microscope to learn
about indexes, itemIndexes, and visible/hidden ids. A Scripting walk through a complex layers stack
introduces, among the rest, the concept of recursive functions; finally, Photoshop Preferences are
covered.
5. The ExtendScript Domain is a long journey paying a visit to all the Points of Interest in the
ExtendScript land. Often (unintentionally?) confused with JavaScript, the ExtendScript program-
ming language has many unique features that make it remarkably versatile and powerful: debugging
objects, FileSystem management, native XML support, Graphical User Interfaces, etc. Each one of
them is presented and corroborated with examples of use; by the end of the Chapter, you’ll have
more solid tools to build your code with.
6. ActionManager is perhaps the densest Chapter of the entire book: the topic is traditionally
perceived as closer to dark magic rather than Scripting; ActionManager may deserve its reputation.
The main reason being, in my opinion, that official documentation is – as a matter of fact – non-
existent; without a glimpse of the big picture, it’s almost impossible to make sense of the six pages or
so appendix (examples included) that the Photoshop Scripting Guide sports about ActionManager.
The kind of learning path that I’m proposing you here is split into stages. First, you need to know
why ActionManager is there, and works, in the first place: this is possibly the least known fact,
which is crucial to understand everything that’s going to follow. Second, you’ll learn how to wrap
Content at a glance viii

with functions ready-made ActionManager code that comes from the ScriptListener plugin. Third,
you must dig into what that ActionManager thing is all about (hint: mostly Events and Descriptors):
we’ll create these unusual objects “in the lab”, and learn how to dissect them for inspection. Fourth,
I’ll be teaching you how to query Photoshop for a variety of information on some of its key elements.
Fifth, we’ll be doing some more ActionManager neuro-surgery, like transplanting into Photoshop
Descriptors that have been injected with external information, to see what happens. Sometimes the
patient’s happy, sometimes it dies, but it’s a lot of fun anyway. This 68 pages Chapter is an original
essay on ActionManager: a skill that you need to master to write production-ready Scripts.
7. User Interfaces deals with the available technologies that you can use to build GUIs (Graphic User
Interfaces) to your scripts. I discuss the differences and peculiarities of both ScriptUI Dialogs and
CEP (HTML) Panels, focusing more on the former ones than the latter (that already have a dedicated
course). The Chapter covers Container and Components (included the somewhat mysterious Custom
Component), code styles and proper styling. A fully functional Sample Project puts everything
together, also demonstrating an Object Oriented approach to ScriptUI code.
8. Working with Metadata focuses on how to store and retrieve, permanently or semi-permanently,
data: either as standardized XMP Metadata (with custom namespaces, both on a Document and
Layer basis), in the Photoshop Registry, or with simpler yet effective XML and JSON object stored
in the FileSystem. A couple of Sample Projects demonstrate these concepts while creating a Preset
System, and saving the last status in a ScriptUI dialog.
9. Events is about the Script Event Manager interface, and its underlying Notifiers class: a powerful
technology that can be used to implement effectively automated control pipelines.
10. Adobe Generator is a fascinating framework that, among the rest, the Real Time Assets
Generation features is based upon. It relies on a Node.js server that runs in the background and
can be exploited as a parallel engine either for integration with traditional ExtendScript code, to
exchange data with external services, or as an in-house server.
11. Cross-Application Communication is about the BridgeTalk API, that lets you send to, and
receive messages from, other Adobe apps such as Bridge or InDesign. This course doesn’t cover
Bridge Scripting strictly speaking, but basic information about the PS-BR interaction are given.
12. Appendix has two sections. The first deals with Script deployment, i.e. best practices to follow
to let others install and run your products. The second is a curated list of links of interest.
1. Photoshop Scripting
Scripting¹ is a branch of the Photoshop extensibility layer: one of the ways developers can add their
own brain juices to the official application, as Adobe engineers ship it.
We’re mostly in the automation bureau here; scripts are – usually, but not exclusively – written to
automate tasks over huge collections of files:

Image Processor Pro, by xbytor

Or apply a long series of smart operations on a single image almost instantaneously:


¹This Chapter might have been called “Introduction”, but nobody reads the introduction, and I wanted you to read this. I feel safe admitting
this here because nobody reads footnotes either.
Photoshop Scripting 2

PPW Panel, by Dan Margulis and Giuliana Abbiati

Or overtake our clumsy human nature with the computer’s pixel precision²:

Parametric Curves, by Davide Barranca

Scripts are used to automate, and sometimes even structure, the workflow of entire graphics
²Can you draw this Curve by hand in an Adjustment Layer yourself? I bet you can’t, and this is fun stuff!
Photoshop Scripting 3

departments within large image-crunching enterprise environments, in businesses like online


fashion retail, gaming, adult entertainment, just to mention few of them. Whenever you see a gallery
online, automation has been at work down the line.
At the same time, Scripting is an invaluable help for savvy freelancers in their day-by-day job; fancy
a new tool that Photoshop lacks? The chances are that you can build it yourself, or pay a fair amount
to somebody to script it.

Split to Layers, by David Jensen

Lastly, what about Generative Art: intriguing and awe-inspiring code-based visuals:

Dan Gries algorithm

But what is peculiar to Scripting, and what does this course cover? Let’s have a look at all the options
that you have, as a developer, to mess with Photoshop.
Photoshop Scripting 4

1.1 Photoshop Extensibility overview

I’d like to borrow an illustration that appears in the book “Power, Speed & Automation with Adobe
Photoshop”³, by Geoff Scott and Jeffrey Tranberry, that I’ve repurposed below:

The idea here is that the larger your knowledge base, the more you can squeeze out from Photoshop:
Adobe engineers have access to the original source code so that they can create and shape new
native features out of nothing. On the other side of the spectrum, a total beginner may tweak the
interface selecting the appropriate workspace, hiding unnecessary complexity. What interests us
now is everything in the middle, specifically from Actions onwards.

Actions

You may be more familiar with the word Macro, which describes the same concept: somebody
records a series of manually performed steps in Photoshop, working on one or more documents,
then saves the process into an Action (which in turn is saved within an Action Set *.atn file).
Download the Action on your computer, and play it back on a single document: it’ll be like having
a lightning fast, stamina-unlimited version of the Action’s author ready to work for you, repeating
those brilliant steps at your will.
Starting from Photoshop CS6⁴, the application has been upgraded to feature Action’s Conditional
Statements, that cover a variety of circumstances. E.g., IF Current Layer is a Shape Layer THEN
play the Action that, say, rasterize the Layer ELSE apply distortion filter, etc. The Actions domain
as we knew it leapfrogged, empowering authors to a quasi-scripting level. Quasi. Not really there,
but hey.

³Focal Press 2012, check it on Amazon.


⁴According to this post by former Photoshop Product Manager John Nack, only the CS6 version that comes with the Creative Cloud
subscription, and not the so-called Perpetual Product (the one version that you could buy, and it was yours forever; that’s the past, you know).
Photoshop Scripting 5

Actions can create more complex workflows call-


ing, in turn, other Actions, very much in a LEGO
fashion: you record your smaller and simpler
building blocks, that can be combined/nested in
fancy ways into one or more main Actions.
Yet, thinking about Actions only as a mean to free
you from monkey-work is limiting, to say the least.
In recent years, Graphic Artists have embraced this
piece of technology to build amazingly creative ef-
fects that push the boundaries of what we believed
possible. Combining Actions with custom presets
such as Brushes and Patterns, Smart Objects and
the entire Photoshop arsenal, it is indeed possible
to create stunning visuals.
This has given rise to a flourishing and vast Ac-
tions market, with usually very low-priced prod-
ucts. When the offer is so large, as you’d guess,
the quality is not always up to the expectations: Architectum 2 by profactions
nonetheless, some producers have been able to
establish remarkably successful businesses selling like crazy awesome Action Sets. Which is
encouraging, since there’s nothing that Actions do that Scripting can’t (in fact, you can do much
more with code).
The last thing to mention about Actions is the Photoshop native Batch dialog that allows you to
apply them on many Files at once.

Photoshop Batch scripted dialog


Photoshop Scripting 6

Scripting

The entire book is devoted to the subject, so in this extensibility overview section let me just briefly
assert that Scripting is a way to programmatically drive Photoshop, without considering for the time
being all the implications – more in the following sections. Scripting overcomes Actions limitations
in many ways, but (or better: because) it requires at least some coding knowledge.
With code, you can be very precise (e.g., “Open the JPGs and PNGs documents, but not PSDs, which
names start with an underscore, but only those created in July 2016, from a directory in the User’s
Documents folder”). Such script knows what to do on a Folder of images because it’s been instructed
to look for a combination of some properties, and will act accordingly. With code, you can listen to
user interaction; with code, you can give to automation the appearance of intelligence. Please note
that you’re not expected, nor required, to write elaborate code with conditionals, loops, recursion
and the like: surprisingly simple scripts can do wonders – you can always refactor (i.e., rewrite in a
more functional, elegant way) your programs later on, when you’re more experienced.
Besides the direct use of scripts to automate operations and build dialogs (such as the Image Processor
Pro, seen in Fig.1), Scripting is used within at least three technologies for different purposes:

• TCP/IP (Connection SDK): Less known option, yet worth exploring. From CS5 onwards, you
can establish a TCP connection (i.e., based on the same protocol that most of the Internet relies
upon) with Photoshop and send/receive Script messages and image data. But who is Photoshop
messaging with? For instance, but not exclusively, a mobile application, built in Java for Google
Android, Swift for iOS, or if you’re inclined even Adobe AIR.
• Generator: First released with Photoshop CC (14.1), it has been primarily marketed as a
technology that lets you export images in the background, based on layer names. To developers,
it’s much more interesting than that: the core is a Node.js server that communicates with
Photoshop via ExtendScript messages - you would mainly use Generator to access/extract
resources from the application in real time.

Photoshop itself heavily uses scripting: many scripts made by Adobe engineers are bundled with the
program. Find them in the 'File > Automate' and 'File > Scripts' menus; the source files belong
to the 'Photoshop <version>/Presets/Scripts' and 'Required'⁵ folders. They’re at the same time
convenient tools, and a nice instructive way to peek at production-ready programs.
The third technology Scripting is involved in deserves its own section.

HTML Panels

Introduced with Photoshop CC, they’re powerful interfaces on top of Scripting. Actually, HTML
Panels are special kinds of Web Applications (running in CEF – the Chromium Embedded Frame-
work, sort of an instance of the Google Chrome Browser hosted in Photoshop, with an embedded
⁵Mac users will find it right-clicking the Photoshop .app and selecting Show Package Content.
Photoshop Scripting 7

Node.js version), allowing a native look and feel, that can do all the crazy things Web Apps usually
do and drive the host application.

Nicolai Grut Brushes Panel

On one side they can be used just as GUIs (Graphic User Interfaces) for Scripting – but they can do
wonders when you use the Panel to integrate Scripting and perform tasks Web Apps excel at (server-
side or database connection to name two of them). In my Photoshop HTML Panels Development
course I’ve depicted Scripting as an actual layer beneath the Web Application – the two technologies
are distinct yet work in pair, communicating via messages.
I tend to consider HTML Panels as a step upward in the pyramid graphics – being publicly released
in 2013, they couldn’t possibly be included in the original illustration.

Plug-ins (Photoshop SDK)

This is something we’re all familiar with so that the layman calls everything a plug-in.
Strictly speaking, you build one writing in some C-like language (C, C++, Objective-C, etc.). There
are several types of plug-ins, the most common of which are:

• Filters: pixel crunching machines.


• Automation: accessing all Photoshop scriptable events.
• Format, Import and Export: input and output for specific devices or additional file formats.

Plug-ins coders are high in the food chain – they do hardcore programming and perhaps have the
largest share of programming frustration too. Interestingly enough, from the point of view of this
book, Plug-ins can be developed to be scriptable.
This means that they become available to the Scripting layer like all other Photoshop tools – it’s
not the default behavior though, third-party plugins aren’t scriptable unless their authors expressly
introduce this feature themselves.
Photoshop Scripting 8

Filter Forge Plug-in

Enough for the Photoshop extensibility pyramid – if you wonder now what are the topics I’ll be
dealing with here, the course is entirely based on what I could call traditional Scripting, including
additional sections on Adobe Generator, HTML Panels and Scriptable Plug-ins.

1.2 How Scripting works


If you think about it from this point of view, Photoshop (like any other software) is an application
that provides you with an interface to a series of commands: menus and buttons let you, say, open
a document, select a portion of it, or create a vector path. The available set of commands are the
leading software features⁶ that define its functionality and value.
⁶UX developers could argue that features are not exclusively bound to users’ interaction: for instance, the Histogram panel in Photoshop
is for sure a feature – it just shows information, it’s not really a command that acts on, create, or transform something.
Photoshop Scripting 9

Yet, in order to work, the software needs you: a user, somebody who intentionally (and for the most
part, intelligently) acts – selects menus and clicks buttons – i.e., accesses the Photoshop features
made available through the interface. Manual labor! But hey, you can always off-load it to the
new intern, a younger colleague, or someone in the Philippines⁷. To successfully carry out your
plan, a detailed list of the required steps needs to be written: commands and arguments. Marc, the
intern, can understand “Open the PSD” but has no clue about the document you’re referring to
in your mind, and would also like a please. “Please open the logo.ok.ok.final.psd that is in my
/Projects/unpaid/ folder” – that is the way to go.

A first clumsy step towards automation: find a worker, tell him/her in a language he/she understands
what you’d like to be accomplished, providing all the needed details, then ask please. Left alone the
very much overloaded intern, for now, let’s say your company has loaned “Marc 2.0”: a robot. He’s
much faster, has tireless robotic hands, but he’s really picky about the way you need to instruct him.
His electronic brain needs you to speak his own language – zeroes and ones – and you must follow
all his language’s rules if you want your instruction to be parsed and executed correctly.
Scripting is very much like this robot, with a notable exception: it doesn’t need the Photoshop
Graphical User Interface at all (GUI: menus, panels and buttons, etc.) – it bypasses them, being
directly wired to the underlying commands. Scripting works at a so-called lower level compared to
robots and humans. “Open a document” is given as a direct instruction to the Photoshop engine, and
executed without actually accessing the 'File > Open...' menu instance.
In more appropriate terms, Photoshop exposes its existing commands to the Scripting layer. As a
consequence, your operativity range as a scripter is based on, and limited to, the available Photoshop
commands. Let me show you a couple of examples to explain what this means.
Say that you want to add a simple styled frame to a picture, such as this one:

Before the recent 'Filter > Render > Picture Frame...' addition, there was no “Add Frame”
filter whatsoever, so you had to come up with a frame algorithm, i.e., a set of instruction, based on
existing Photoshop commands, producing the desired result. The meta-code⁸ for that simple frame
could be something along the following lines:
⁷I don’t know why, but I’ve been receiving plenty of advertising about Virtual Assistants from the Philippines – after China and India, it
seems they are the high-tech outsourcing new frontier.
⁸Meta-code or Pseudo-code is a way to describe in a programming-language agnostic way the steps that a program needs to accomplish.
Photoshop Scripting 10

1 Select the entire Canvas


2 Shrink the Selection by 20%
3 Add a Layer Mask to the existing Layer using the active Selection
4 Apply to it a Gaussian Blur, radius equal to 1/3 of the widest side in px
5 Apply to it a Crystallize Filter of radius 30px
6 Create a white layer beneath the current Layer

The steps above involve existing commands, like 'Select > All', or 'Filter > Blur > Gaussian
Blur...' etc. that are combined to produce the desired output. Therefore this routine can be
successfully scripted, and scripting will also help to calculate on the fly what 20% of the full canvas
selection means since the 'Select > Modify > Contract...' command operates on pixels⁹; or the
blur radius, etc. It could also be useful to create an actual dialog window, that parametrizes some
values (such as the extent of the frame) and let the user play with it in real time – scripting can.
Conversely, say that you want to apply a peculiar image processing effect:

Sketchy Painting by emme, Filter Forge

The algorithm is proprietary – there surely is a way to describe it with meta-code at a pixel level,
but it would not possibly involve the available Photoshop tools only. Therefore this very routine is
not scriptable – you cannot entirely replicate it with a script – you’d need to code a plug-in¹⁰.
⁹You may not need this step altogether, and directly select the calculated rectangle that you need.
¹⁰If the plugin is built as “scriptable” you can then call it from a script, passing it the needed parameters.
Photoshop Scripting 11

To sum up:

• Scripting is a way to programmatically drive Photoshop using code.


• A script is allowed to access only the commands that Photoshop itself has, and are exposed to
the Scripting interface.
• These commands bypass, and won’t need, the User Interface.
• Instruction should be written according to the rules of a language that Photoshop can
understand.

1.3 Languages and Documentation


Speaking about languages, Adobe scriptable applications (Photoshop included) can be driven using
either:

1. AppleScript (Mac only).


2. VBScript (Win only).
3. ExtendScript.

Only the latter option is multi-platform and can be used directly within Photoshop – whereas both
AppleScript and VBScript need to be run from outside the application. If you’re already familiar
with a platform-specific scripting language, it may make sense to look at AppleScript or VBScript
(in a different book…). Otherwise, a one-language-fits-all approach (ExtendScript) is wiser in my
opinion.

ExtendScript vs. JavaScript

ExtendScript is what this course is entirely based upon, so why does the title mention JavaScript?
According to the Adobe After Effects Scripting Guide¹¹:

“After Effects scripts use the Adobe ExtendScript language, which is an extended form
of JavaScript used by several Adobe applications, including Photoshop, Illustrator, and
InDesign. ExtendScript implements the JavaScript language according to the ECMA-262
specification. The After Effects scripting engine supports the 3rd Edition of the ECMA-262
Standard, including its notational and lexical conventions, types, objects, expressions, and
statements. ExtendScript also implements the E4X ECMA-357 specification, which defines
access to data in XML format.”
¹¹The quote is found in this pdf – I don’t know why Photoshop documentation doesn’t mention it.
Photoshop Scripting 12

In other words, the ExtendScript language is a JavaScript superset: it complies with JavaScript specs
and adds extra stuff that will not affect you in the first stages of your learning experience. A lot
more about ExtendScript specificity is found in Chapter 5.
There’s one important fact you need to pay attention to, now: the version of the Standard that
ExtendScript adheres to is what was named, back in 1999, ES3 or ECMAScript version 3. By the
time of this writing (2017-2019) ES5 is ye old JavaScript default implemented in all major browsers,
and first published in 2009; ES6 is getting momentum; and the ES7 specs (aka ES 2016) have already
been announced.¹²
In plain English: true, ExtendScript extends JavaScript with a set of exciting and useful “extra
features”, but it hasn’t caught up at all with the latest core JavaScript evolution – I mean: it’s stuck
at a pre-2009 era. On one side, it’s fair to admit that between standards’ publication and adoption
there might be a sensible delay; on the other side, it’s a huge gap to fill¹³.
This fact has a crucial effect: if you still need to learn the language basics, forget about the new
ES6 syntax (e.g., the so-called arrow operator => etc.), it’ll be of no use and will break your scripts.
There are no basic tutorials covering ES3 only because it’s too old – your best option is to learn ES5
JavaScript (plenty of resources, still) and then prune all the features that don’t apply to ES3.
If you come to Photoshop Scripting with some prior JavaScript knowledge, be aware that many
of the features that you currently rely upon, won’t work – stuff that you take for granted such
as Array.indexOf(), or even JSON support. The good news is that many of the ES5 features are
shimmable (i.e., you can import external libraries that implement them).

For those of you who are more experienced, it’s possible to use either Babel or Bublé and
then transpile modern JS into some sort of ExtendScript-friendly version of JS, using the
ES5 and ES6 shims found in the Resources Chapter. I’m not sure how they would react with
ExtendScript-specific features, like XML literals: obfuscation tools don’t like them, so a bit
of caution is always a good thing.

Photoshop and Core ExtendScript

The entire Chapter 5 is devoted to the language specs: I’ve decided to postpone the in-depth coverage
of ExtendScript’s unique features because it makes the learning curve smoother, in my opinion. Yet,
there is another bit of knowledge that you need to have clear in mind earlier: let me now briefly
sum up what you’ve learned so far:

• The Photoshop Scripting language of choice is ExtendScript.


• ExtendScript is a superset of JavaScript (i.e., JS plus extras from other standards).
• The JS standard that ExtendScript is based upon is the old ES3¹⁴.
¹²ECMA jumped from version 3 to version 5, quoting Wikipedia “due to political differences concerning language complexity”.
¹³With a new Adobe team dedicated to Developers relations, the hope for a modern language is rising once more.
¹⁴Mind you: ES3, ES5 or ES6 means “ECMAScript”, the Standard Specification versions, not the ExtendScript language.
Photoshop Scripting 13

When writing your scripts, you must be aware that you’ll be using both Core ExtendScript
and Application-specific ExtendScript features. The ExtendScript language has its own specs that
are shared among all the Adobe Creative Cloud applications that implement scripting (notably
Photoshop, InDesign, Illustrator, and Bridge). For instance, you can use the peculiar $ object from
the Core Language for debugging purposes, in PS as well as in ID.
Yet, there are entire parts of the language that are exclusive of Photoshop, and will not work in any
other CC app – ActionManager code is one of them: it doesn’t exist in, say, Illustrator. Furthermore,
even shared Core elements have properties and methods that are application-specific. For instance,
the app class exists in both InDesign and Photoshop, but only the former can app.doScript() – a
method that PS lacks.
Lastly, there are remarkable differences in the way an application implements Core ExtendScript
specs compared to another one. As an example, ExtendScript dialogs (built with the ScriptUI class,
you’ll see them in Chapter 7) can theoretically be of type 'dialog' or 'palette' – but the behavior
of the latter makes it almost useless in Photoshop (we’ve been explicitly advised to forget about it),
while the InDesign implementation is perfectly fine. To sum up, add to the bullets listed earlier in
this section the following ones:

• There are parts of the ExtendScript languages specific to Photoshop.


• Core ExtendScript features are shared, yet the implementation may differ from app to app.

Documentation
Now point your browser to this page, where the Adobe Official Documentation about Photoshop
Scripting is found. Download and keep handy the following ones:

• Adobe Introduction to Scripting: Scripting basics, host application independent (AppleScript,


VBScript and ExtendScript).
• Photoshop CC Scripting Guide: Scripting concepts applied to Photoshop (AppleScript,
VBScript and ExtendScript).
• Photoshop CC JavaScript Reference: JavaScript language reference for Photoshop Scripting.

Also locate the JavaScript Tools Guide CC pdf that is in the Adobe ExtendScript Toolkit CC/SDK
folder (find the ESTK download from the ones listed in the Creative Cloud application), which covers
more ExtendScript-related topics.
These documents represent the official words from Adobe: they’re sometimes incomplete, there are
known bugs, but they are for certain invaluable tools in your journey learning Photoshop Scripting
– alongside with this very book!
Several demo files are found in the Adobe ExtendScript Toolkit CC/SDK/Samples/javascript
folder: these are not specific to Photoshop, and you might need to change few things here and there
to run them – but there’s a lot to learn in that code.
User Forums are critical as well – do participate in the developers’ community and let your voice
be heard! Don’t be afraid to ask, and when possible help others. The main ones are:
Photoshop Scripting 14

• Photoshop Scripting Forum: Official Adobe forum.


• ps-scripts: independent Photoshop Scripting community, recently risen from its ashes after a
long hiatus¹⁵.
• InDesign Scripting forum: a place where you can find interesting Core Language related
topics. Let’s learn from our InDesign cousins.

It’s crucial that you get familiar with both the official documentation and the user forums – there’s
no way for you to improve your skills without help from others, and some good hours spent looking
for answers buried in PDFs. Throughout the Course, I’ll mention the documentation very often, and
the sooner you find your way through it, the better.

JavaScript Primer for starters

I’ve been a JS beginner too, long ago; books I’ve bought and studied (such as the huge “JavaScript:
the Definitive Guide”¹⁶) are still useful for sure, but luckily both the technology has made giant
leaps forward, and the language has become much more popular. Today there are more engaging
and effective ways to learn.
Besides my research on modern JavaScript resources, I’ve asked to a dear friend of mine who –
independently and thanks to a career move – recently got into Photoshop Scripting being very green
on coding. He’s followed the interactive JavaScript course made available (for free) by Codecademy
and has been able to jump on the JS boat in a fair amount of time.
Needless to say, even if the estimated time to complete the course is advertised as about 10 hours,
nobody would think that 10 hours can make you an expert. Codecademy JS syllabus covers all the
fundamental topics, and the approach is hands-on – with many exercises down the line. They also
have a paid plan ($19.99 per month) including live-chat code review with advisors, personalized
learning plans, etc. but the free tier can be perfectly fine for you.
Another qualified option you may want to have a look at is Codeschool, which JavaScript learning
path is exhaustive, and entirely based on excellent quality videos – they have five JS courses, of
about four-five hours each. Codeschool requires a paid subscription ($19 per month for the yearly
plan, which in my opinion is fair).
¹⁵Mike Hale, one of the forum founders, passed away in late 2014; all the content was apparently lost forever.
¹⁶Amazon page here.
Photoshop Scripting 15

You can take advantage of the next three or four Chapters – that are still quite soft – to begin a
JavaScript course on your own if you feel the need. It’s crucial that you start with the right foot – it
will save you a lot of time and frustration in the future.
2. Writing Scripts
Time to flex our fingers a bit – this Chapter covers basic information about scripts that you need to
know in order to write, save, and execute them.

2.1 Files, Extensions and Folders


While JavaScript files have the .js extension, ExtendScript default (and what you’ll be using 90% of
the times) is .jsx¹ – bear with me if I’ll be referring to either the language or a script file as “the JSX”
in the following pages. Another convention is to use .jsxinc for include files: separate modules that
you may require into your code using preprocessor directives – a distinctive feature of ExtendScript
(see Chapter 5). Lastly, .jsxbin is for so-called binary exports.
You can store your projects wherever you want: I generally keep them backed-up in a Dropbox
folder, but there are a couple of default locations that you must be aware of, namely:

• Photoshop <version>/Presets/Scripts
– Platform independent; there you can find all the Adobe scripts (like 'Load Files into
Stack.jsx', 'Fit Image.jsx', etc.) bundled with Photoshop. You can save your own
JSXs in a dedicated nested subfolder there, and they will be listed by default within the
Photoshop 'File > Scripts' menu – provided they have the .jsx extension. In case you
want to hide them as menu items, prepend the filename with a ∼ (tilde)².
• [Win] C:\Program Files\Common Files\Adobe\Startup Scripts CC\Adobe Photoshop
• [Mac] /Library/Application Support/Adobe/Startup Scripts CC/Adobe Photoshop
– These are the startup folders: scripts in there will be executed when the application loads,
for instance, to enable Photoshop / Bridge integration – see Chapter 11.

Scripts saved in the Presets/Scripts folder, or its subfolders, can appear in a variety of different
Photoshop own menus: not only 'File > Scripts', but as an example 'Filter', or 'File > Export'.

2.2 Adobe ExtendScript Toolkit

¹Not to be confused with the now popular React JSX format, nor with this other one.
²The tilde ∼ at the beginning of the path means your User’s Home folder.
Writing Scripts 17

Also known as ESTK, it’s listed for download in the Creative


Cloud application, but inside the “Previous Version” section³: it
is the closest thing we have to an IDE (Integrated Development
Environment) for scripts. You can use it to write, run and debug
your JSX code, as well as to access all kind of Documentation,
including code completion!
Given this optimistic premise, be aware that I cannot stand ESTK.
At best, it will be your necessary evil. Let me tell you why I’m so
politically incorrect:

• It’s sluggish and unstable (at least on Mac).


• As an editor, it’s outdated and lacks modern features that are
de facto standards (from themes to multiple cursors).
• Debugging is buggy⁴, and code completion erratic.

To be fair, some experienced developers currently do use it, so let me try to write “ESTK, the good
parts”. First: it has a quite self-explanatory interface based on panels:

Adobe ExtendScript Toolkit, aka ESTK (with highlighted sections)

It even supports Workspaces (top-right corner button, saying “Default”). I’ve highlighted the sections
that are more relevant/useful, with a brief description below:
³Find here detailed instruction to visualize and install Previous Versions
⁴Unavoidable pun. See this thread as one of the examples of developer frustration.
Writing Scripts 18

1. Target Application and Application Status (Yellow). ESTK can connect to, and launch scripts
in, all the available Adobe applications that you’ve installed locally – including ESTK itself.
You can select the targeted app in this dropdown list, that by default says “ExtendScript Toolkit
CC”. The green chain/link icon shows that ESTK has been able to establish a communication
with Photoshop successfully, and so you can run your code. Otherwise, the icon is a red, broken
chain (e.g., PS is not running).
2. Debug buttons (Cyan). You can use them to run (play) your code, or going through debugging
(step in, step over, etc. – more on this later on).
3. JavaScript Console (Red). You’ll spend much time inspecting logged messages in the Console
when debugging. Like with other consoles, you’re allowed to type in there and evaluate
expressions too⁵. In case it’s not visible, you can open it from the 'Window > JavaScript
Console' menu.
4. Result output (Green). Shows the result of code execution, or the error message thrown in case
you’ve not been lucky enough. Keep an eye on it.
5. Data Browser (Magenta). It provides you with a handy hierarchical tree display of the various
objects, properties, and methods in the Photoshop Document Object Model.

Second: Debugging – you have a proper debugging environment, with conditional breakpoints,
Console, Data browser, etc. It doesn’t work under all circumstances, but it’s there.

It’s worth stressing that ESTK itself can be the target of the code you’ve written or loaded
– its own ExtendScript engine can run it. Usually, this is of no use at all. I mean, ESTK has
no clue about what a Layer, a Selection, or a Filter are: it only knows the Core Language,
not the application specific (Photoshop) ExtendScript.
Yet, under some circumstances… say that you’re testing a function working on Strings, or
Files and Folders – something not directly related to Photoshop – it might be faster to target
ESTK and not PS. That’s because it takes a little extra while for ESTK to call and dispatch
to the PS ExtendScript engine the code for evaluation, and we developers tend to become
quite nervous when all those little whiles sum up, at the end of the day.
Per se, it’s not a harmful practice: yet remember to always, thoroughly test your code
targeting the actual application (Photoshop). Similarly to InDesign or Illustrator, ESTK has
its peculiar ExtendScript engine: results between ESTK and PS can be different, and lead to
persistent headaches. One area in which this difference explodes is the ScriptUI class (used
to build dialogs) – you shouldn’t trust anything related to dialogs in there.
Finally, if the code you’ve launched halts for unclear reasons, check that you’re really
targeting PS and not ESTK (cause #1 of these errors not only among beginners). Adding
#target photoshop, a preprocessor directive described in Chapter 5, as the first line of code
helps.

Third: a remarkable highlight of the ExtendScript Toolkit is the Object Model Viewer, a powerful
and handy documentation browser:
⁵Embarrassing as it appears, I did realize that I could type in the ESTK Console myself only in recent years.
Writing Scripts 19

It comes as a separate, floating window and features a dropdown list (left column, near the top) that
lets you pick the dictionary to open – you can choose between:

• Core JavaScript Classes: should be named Core ExtendScript since it includes both JavaScript
ES3 documentation and the special ExtendScript features.
• ScriptUI Classes: used to build dialog windows.
• Photoshop Object Library: the hierarchy of objects, properties, methods and constants relative
to Photoshop.

The Object Model Viewer is a valid alternative to the Photoshop CC JavaScript Reference PDF: some
would say it’s even more up-to-date – depending on your taste, you may better prefer one or the
other⁶.
⁶I’m more at ease with the Reference PDF, which by the way doesn’t involve opening ESTK in the first place. However, that’s just me.
Writing Scripts 20

Auto Generated Documentation: there are two independent projects that aim at
building accurate HTML documentation of the ExtendScript language. In order to do
so, they’re based upon the same source code (as .xml files) used by ESTK itself to
display information in the Object Model Viewer – clever!
The first, by Will Ridgers, is called extendscript-api-documentation and is based
on node.js: I’ve been able to successfully generate a local copy of the Photoshop,
JavaScript (ExtendScript) and ScriptUI classes. The second, extendscriptApiDoc-
Transformations is made by Gregor Fellenz, and uses a different approach (XSLT
Transformations based on the same XML sources); alongside with documentation, it
also produces a Sublime Text Code Completions file.
You can try to compile the Object Model viewer yourself from the available source
code; alternatively, browse online Ridgers and Fellenz nice and clean output.

Gregor Fellenz’ ExtendScript API Web Documentation

Fourth: in the ESTK’s Preferences, you’re allowed to set keyboard shortcuts⁷, Fonts and Color (even
if it doesn’t support themes), and a bunch of other elements. The rest of this discussion may not be
immediately meaningful to beginner developers, so feel free to skip to the code editors.
Still here? Good. Speaking of customization, the ExtendScript Toolkit extensively uses JSX itself,
internally. If you want to look at some unique code and try some customization yourself, at least
on a Mac right click on the ExtendScript Toolkit.app file, and select “Show Package Content”
to access the Content/SharedSupport/Required folder. There you’ll find some forty-three JSX files
setting all kind of elements in the ESTK, from globals to preferences and menus. You’re alone there,
so proceed at your own risk. Whatever JSX you add here (and you can – use the same naming
convention and add yours like 101myFile.jsx) it’s going to be executed while ESTK initializes.
⁷My suggestion is to immediately change ⌘/ that defaults to the Object Model Viewer, but in many other code editors is for block
comments – ESTK uses ⌘K for them.
Writing Scripts 21

An example that you can try yourself more or less safely is the following code; it creates a new
"Docs" menu item in ESTK, which lists documentation files (pdf, chm, txt) belonging to a folder
you’ll specify in line 8. When you select an item, it opens:

1 try {
2 ESTKdocs = {};
3 ESTKdocs.menus = {};
4 var menuItems = [];
5 //Folder where all your documents/helpfiles are:
6 // var docFolder = Folder('~/Documents/ESTK-Docs'); // Ex. Mac
7 // var docFolder = Folder('/c/programData/adobe/help'); // Ex. Win
8 var docFolder = Folder('/your/Folder/here');
9 var fileList = docFolder.getFiles(/\.(chm|pdf|txt)$/i);
10 ESTKdocs.menus.extras = new MenuElement("menu", "Docs",
11 "at the end of menubar", "ESTKdocs");
12 for (var a in fileList) {
13 menuItems[a] = new MenuElement("command",
14 decodeURI(fileList[a].name.replace(/\.[^\.]+$/, '')),
15 "at the end of ESTKdocs", "ESTKdocs/" + a);
16 menuItems[a].onSelect = function() {
17 var count = Number(this.id.match(/\d+$/));
18 File(fileList[count]).execute();
19 }
20 }
21 } catch (e) { alert(e + " - " + e.line); }

Which is very handy, isn’t? Save that into a JSX file and put it alongside the others within the ESTK
.app. Another quite safe hack that you can try is to “de-uglify” the pixelated appearance of ESTK
when/if you’re using a Retina Display (such as an Apple MacBookPro): I’ve documented a fix that
turns almost all the interface back into a cleaner, vector-like aspect.
Other examples of ESTK tweaks are found in this page by Dirk Becker, this forum post by Bob Stucky
about Batch JSXBIN conversion, or this one on implementing the Solarized Theme. I know that also
Writing Scripts 22

Kris Coppieters did some ESTK work – apparently, InDesign developers are the only ones who dare
to enter the dungeon.
Fifth: Code Profiling, found in the 'Profile' menu: you can use it to understand where the slower
sections of your script are (based on color codes and timing) and try to optimize them.
Sixth and last: ESTK has the unique ability to export your ExtendScript code into sort-of binary
(unreadable) form, via the 'File > Export as Binary...' menu. The result is a .jsxbin file, which
content is almost indecipherable.
That said, I personally use ESTK for some debugging, to quickly target and test my scripts in different
Photoshop versions, and for JSXBIN generation, period. Over the years it has got little if no updates
at all: I consciously try to use it as little as possible, mostly due to its sluggishness and bugs⁸.
Nonetheless, you can’t entirely avoid it: if you need more information about its features, please
refer to the ‘JavaScript Tools Guide CC’, which includes comprehensive documentation of ESTK
itself.

2.3 Better Code Editors


Luckily, in your daily work you can use different code editors alongside, and/or in collaboration
with, the ExtendScript Toolkit. They can’t provide proper debugging features themselves – yet, as
editors, each one of them overshadows ESTK.

While I was giving the final proofreading round, Adobe has publicly announced that “The
Future of ExtendScript Development [is] A VSCode Plugin” (confetti fell out from the
sky). Few weeks later a prerelease version has been given to developers for testing, and
results are promising. I will be updating the book with new content as soon as possible.

Several alternatives are available, among them: Sublime Text ($80, the only paid software in this list),
Atoms, Visual Studio Code, Adobe Brackets. For most of them there’s some plugin/extension that
allows you to launch the ExtendScript file/code you’re working on in Photoshop, so you’re able to
test it without switching back and forth from the editor to either PS or ESTK. Find a brief overview
of them below.

• Atom: a project by Javier Aroche is available among the listed Packages if you look for atom-
to-photoshop; build-extendscript by Micky Hulse is found only on GitHub.
• Visual Studio Code: there’s an extension by Adan G. Galvan Gonzalez named vscode-to-
photoshop, plus you can add Syntax Highlighting with ExtendScript by Ole Henrik Stabell.
Also, Types-for-Adobe by Pravdomil provides TypeScript Declaration Files for Photoshop,
InDesign, Illustrator and Audition⁹.
⁸Lately, every other script run fails, and I’ve to jump back and forth from PS to ESTK few times to be able to stop the process in ESTK and
re-run it. Some people report that disabling App Nap on Mac may help.
⁹If you like strongly typed languages, you can give TypeScript a try – I won’t.
Writing Scripts 23

• Sublime Text: I’ve tweaked myself an existing build plugin for After Effects, and made one that
you can find in this GitHub repository. It supports both Sublime Text 2 and 3, with Photoshop
CS6 up to CC-latest.
• Adobe Brackets: to date, the only available extension is brackets-to-photoshop by Javier
Aroche – Mac only.

It must be said that, even if each one of the three editors has an entirely different plugin/build/ex-
tension system, basically on Mac it’s a matter of calling osascript to launch some AppleScript, and
on Windows to run a .bat file or similar.¹⁰

2.4 Running Scripts


Let’s recap all the available ways you have to run/test the JSX code you’re working on:

• ESTK: write your code in ESTK and click the play button (Command+R on Mac, CTRL+R on
Win) - remember to target Photoshop and not ESTK itself.
• Use Sublime Text, Atom, Visual Studio Code, or Adobe Brackets, and run/build with their
ExtendScript plugin – usually, you have to save the file at least once, or it won’t work.
• Open the same file on both your text editor of choice and ESTK. Write in your text editor, save,
then switch to ESTK to just run the code. ESTK should reload the file and update it with the
latest changes. Make sure to set your ESTK preferences under Edit > Preferences > Documents,
and check “Automatically reload of changed files”.
• Write code in your favorite editor, copy, switch to ESTK, paste there, and run/debug it.
Do this two or three times, and the keyboard shortcuts like Command+A, Command+C,
Command+TAB, Command+A, Command+V, Command+R will stick in your brain and fingers
forever.
• Write code in your favorite editor, save it into a JSX file; switch to Photoshop, go to 'File >
Scripts > Browse...' and select the JSX saved on your hard drive to run it.
• Write code in your favorite editor, save it into a JSX file; in Photoshop create a new Action that
records the 'File > Scripts > Browse...' before mentioned. Switch the Action palette to
show Buttons instead of a tree view (flyout menu in the top-right corner, Button Mode). From
now on, type code in your editor, save, switch to Photoshop and click the Action Button to run
your code.

¹⁰I’m not very much into code editor plugins (not at all actually), but it’s funny to see that Sublime is OK with three files made of less than
ten lines each, while Brackets needs 100 times more code.
3. Hello World by Examples
This Chapter, as the title suggests, is primarily dedicated to those of you who are still pretty green
on the programming side of Photoshop.
Instead of writing a boring list of commands and their purpose, I’ve plucked from the Great
Forums Archive selecting threads which will progressively help you familiarizing with Scripting
and software development related concepts. So take your time to understand the basics – I’ll do my
best to cover details that more experienced developers would give for granted.
If you’re confident enough with JavaScript, just bear with me and remember when you were in
those uncomfortable shoes. We’ve all been there!

3.1 The Great-Grand Father of all Greetings

I’m afraid the very first example is the one and only, true and home-grown, “Hello World!”. So please
open Photoshop CC (whatever version you happen to have) and the ESTK, side by side.

Make sure the dropdown selector on top-left says “Adobe Photoshop CC-whatever”¹, and the “chain
icon” on its left is green. If it’s red, click it: ESTK will try to connect with Photoshop. If the Photoshop
version ESTK expects to connect to is closed, ESTK will try to launch it.
Make also sure you have the “Javascript Console” visible. If you’re in the “Default” workspace (button
at the top right corner), the Console is in the right column - I’ve dragged it at the bottom because
I like it better there. Now create a new, blank document: File > New Javascript, and type the
following two lines:
¹This must match with the Photoshop version you’ve opened (CC 2018, CC 2017, etc.). I mention this because it’s not unfrequent to open
Photoshop CC-something and set ESTK to run CC-something-else.
Hello World by Examples 25

1 alert("Hello World!")
2 $.writeln("Hello World!");

The semicolon ; is optional, as in JavaScript as in ExtendScript – but it sounds like “please” at the
end of a statement: I suggest always to use it.
Conversely, the exclamation marks are required², because they proclaim your childish enthusiasm
that’s going to be frustrated by hours of clueless debugging in the following weeks when you’ll deal
with more fancy scripts.

Ready? Click the green “play” button, or select Debug > Run. Two things are going to happen.
First a nice greeting Alert will pop up in Photoshop:

Click the blue “OK” button to dismiss the dialog. Right after, the ESTK will output a similar message
in the Console. It’s your first Photoshop test-drive, congratulations!
²I’m kidding here, in case you’ve missed the subtle irony.
Hello World by Examples 26

If you need a visual reminder of all the relevant bits in the ESTK interface, see this picture from
the previous Chapter. There’s one way even Hello World can fail – and this is a recent fact of life I
mentioned in the previous Chapters: from time to time, running a script in ESTK with Photoshop as
the target hangs both applications. In which case, click the “stop” button (the square one) and then
switch from PS to ESTK back and forth a couple of times, waiting for a second or two. When the
“play” button turns green again, you can retry, and it’ll work.

A practice that, being in my forties, I started appreciating more and more is to use large
font sizes³. People have huge displays yet use tiny fonts for no reason – apparently killing
your eyesight with micro glyphs is considered cool and stylish, I wonder what they teach in
Graphic Design classes. Click on the JavaScript Console flyout menu (top-right of the panel)
and select 'Font: +' until your finger hurts, to actually see what the darn Console wants
to tell you. Feel free to enlarge Font size in the ESTK “Fonts and Color” Preferences as well
– proudly join the Jumbo Fonts Revolution movement.

Anyway: you’ve output a message, in two different ways: you’re going to do this a lot – it’s a quick
and dirty way to extract information from your script while it’s running: popping alerts in Photoshop
or printing messages in the Console. E.g., you’ve written a script that processes many Layers, and
it hangs somewhere: logging meaningful messages like "About to start renaming..." and "Done
with Background Layer!" is a simple method to live trace the whole process.

But what is alert(), that $ symbol, writeln(), and all that jazz? Fair and important questions.
The first one, alert(), is used to build the simplest dialog available in Photoshop: it will pop up
a Window saying whatever you pass it as a string (a chunk of text) wrapped with quotes (either
single: ' or double: ") and enclosed in parenthesis:

1 alert("Hello World!"); // double quotes: " "


2 alert('Hello World!'); // single quotes: ' '
³Ever since I set 36 points as my default font size for the Email client my life got much better.
Hello World by Examples 27

The entire Chapter 7 is dedicated to Dialogs (i.e., graphical interfaces), for now think about alert()
as a generic, very bare popup text generator. You can try with different messages, and Photoshop
will obligingly repeat them. The $.writeln thing is more exciting and will give me the chance to
start exploring how Scripting works.
The $ is a Core ExtendScript exclusive (no native JavaScript equivalent) helper object containing
utilities that you’re going to use in your scripts to access information, and perform tasks, that are
not directly related to Photoshop operations. Think about the $ as a general-purpose toolbox: you
open it and find a lot of interesting goods there. Mind you, if you come from JavaScript, $ has nothing
to do with jQuery at all.
OK, it’s a toolbox, but how do you open such a thing? Like all Objects in JS, you can access its
properties (associated values) and methods (actions) using the dot .operator – see the illustration
below:

So, you have called the $ object, accessing one among its many properties and methods available:
writeln, which stands for “write a line”. writeln is a method, e.g., an action that performs something,
and like all methods it has parenthesis that wrap the parameter(s), so let’s write it like writeln()⁴.
The parameter is what you want the method to act upon or use. Like “Dollar, do that funny thing that
I know is in your repertoire, called ‘write a line’. What should you write, you ask? “Hello World!”.
Please.
"Hello World!" is the parameter, in this case a String, so it has to be surrounded by single ' or double
" quotes (either will work). How do I know all that – i.e., that writeln() is one of the methods of the
$ object, and it accepts a String as a parameter? Because I’ve found it in the Object Model Viewer
(also known as OMV) and/or the Documentation:
⁴If I were orthodox, I’d write “the method named writeln” without parens, because writeln is just its name. Conversely, writeln() is
what you’d write to execute it. However, I’m unorthodox, and for the clarity sake, I’ll name methods with parens throughout the whole book;
I hope you don’t mind.
Hello World by Examples 28

Try it yourself! Open ESTK, and access Help > Object Model Viewer; make sure the Browser
dropdown menu at the top-left corner says “Core JavaScript Classes”, and select the $ item in the
Classes list right below. Always in the left column, in the Properties and Methods section, scroll
down the until you find the last item of the list: writeln (text), which has a red icon (meaning it’s
a method; blue icons represent properties). Double click on it and your Object Model Viewer should
match my screenshot.
That’s great. After methods, let’s now use properties – again, of the $ object. In the same OMV left
column, “Properties and Methods”, scroll and look for os; which you read is a String: “The current
operating system version information.”

So this is how you access a property in JavaScript and ExtendScript: the object it refers to, the dot .
operator, then the property, and a semicolon at the end, as in the following diagram.
Hello World by Examples 29

We can now use both of the recently discovered $.os and $.writeln() to log something slightly
more meaningful:

1 $.writeln("You are running " + $.os + ", correct?");

Please note the use of quotes, white spaces (to make it readable) and + here. This outputs in my case:

You are running Macintosh OS 10.13.3, correct?

You’ve just experienced string interpolation, i.e., combining different elements into a string. A string
that is composed and passed as the argument of the writeln() method on-the-fly.

I’m using parameter and argument quite interchangeably here, but strictly speaking: the
parameter is a variable used in the function declaration, whereas argument is the actual
value of this variable that gets passed to function. Similarly, there’s a difference between
a method and a function: in JavaScript, if an object has a property that is not a primitive
(integer, string, boolean, etc.) but a function, then it’s called the method of that object.

Nothing prevents you from storing in advance the os information into a variable (a key concept in
programming):

1 var yourOS = $.os;


2 $.writeln("You are running " + yourOS + ", correct?");

It will produce the same output. Think about a variable as a box, that you can fill with all kind of
stuff : you can then use the box as the proxy for that stuff. You’ve assigned the value returned by
the $.os property to the yourOS variable. Please note that a “variable” is named such way because
its content can vary, i.e., you can change the box content anytime, for instance:
Hello World by Examples 30

1 var yourOS = "MS-DOS";


2 yourOS = $.os;
3 $.writeln("You are running " + yourOS + ", correct?");

The yourOS variable at first has been assigned the "MS-DOS" string, then $.os. Also note that the
second assignment misses the var keyword: you need to declare a variable only once. Back to our
example, a different variation:

1 var message = "You are running " + $.os + ", correct?";


2 $.writeln(message);

Now you’ve stored the entire, interpolated string into the message variable, passing message to the
$.writeln() function.
Please note when using a variable you don’t need quotes, otherwise:

1 var message = "You are running " + $.os + ", correct?";


2 $.writeln("message");

Would awkwardly just output:

message

I hope this is clear. Before leaving the World of Greetings, let me drive your attention to the very
first image in this Chapter when you were running the first example. Have you noticed that the
Console said:

Hello World!
Result: undefined

What’s that last line? Each time you run a script, ESTK (or Photoshop, for that matter) kindly returns
you something. You can explicitly return a primitive value (1, or true) or an Object, in the context
of the function’s return statement (that you have yet to encounter), or if it’s written as the last line
like:

1 $.writeln("Hello World!");
2 "Lasagna"

That writes in the Console:

Hello World!
Result: Lasagna
Hello World by Examples 31

Not bad at all. Otherwise, ESTK will return undefined.

Clear the Console: when it gets too crowded, you can wipe out the Console content with
Edit > Clear Console, or the equivalent shortcut (see it in the menu itself), that for Mac is
⌘+C. In case you need to perform the same thing from a Photoshop script, do this way:

1 var bt = new BridgeTalk();


2 bt.target = 'estoolkit-4.0';
3 bt.body = function() { app.clc() }.toSource() + "()";
4 bt.send(5);

If you don’t understand this yet, no problem – you will!

3.2 An Average problem


Time for a real world example! When I feel bored, I usually pay a visit to one of the Scripting forums
around (I know, there are more socially acclaimed ways to kill time). Not surprisingly, from my point
of view, I often run into some interesting topic. Three years ago I saved what I’m going to submit
you now because… I don’t know why, I save things for my future self until the backup crashes and
I’m allowed to forget about it.

Problem: given an image of any size, process it such as every column of pixels is averaged.

For simplicity’s sake, we can assume that the image is flattened (just a Background layer). As follows
an example of what would be the starting image and the processed result:

Input image

Processed image
Hello World by Examples 32

Before even touching the keyboard, you must think about an algorithm – that is a plan. Having a 12-
year-old in the house, the mantra from school is “Read the problem, find the data, draw a diagram,
do the math, give the answer”. As always, the problem itself contains a hint of a solution: your script
must somehow loop through each column, and average the values. What’s a column? A selection
100% tall, 1px wide. Plausible. In meta code:

1 For each column of pixels


2 Select the column of pixel
3 Apply 'Filter > Blur > Average'

This very likely involves steps such as retrieving the image width (because you need to know when
to stop), using 1px vertical selections, and knowledge about scripting filters. We might not know
how to get there, but what’s been asked in the first place is within the realm of possibilities. From
a sheer programming point of view, you would need loops – because that’s what you have to do,
don’t you? Loop through all the pixel columns. You can even imagine how that would cinematically
appear: the original picture starts getting striped from left to right until it’s similar to the given demo
result.
You’re allowed to skate over performance concerns – for instance, a 4000px wide image would
require 4000 select, average, deselect steps. Time-consuming for sure, but heck: we’re beginners, we
can.
Surprisingly (or not, knowing the one who answered the original question) the actual script is not
at all like what we’ve figured out ourselves here. The full code, slightly tweaked for clarity’s sake,
is as follows:

1 var doc = app.activeDocument;


2 var currentWidth = doc.width;
3 var currentHeight = doc.height;
4 var currentResolution = doc.resolution;
5
6 doc.resizeImage(currentWidth,
7 new UnitValue(1, "px"),
8 currentResolution,
9 ResampleMethod.BILINEAR);
10
11 doc.resizeImage(currentWidth,
12 currentHeight,
13 currentResolution,
14 ResampleMethod.NEARESTNEIGHBOR);

Even without understanding all the details (you will, in a few pages), it looks much more
straightforward than our take, uh? The talented Photoshop developer Michel Mariani’s idea here
is neat: squeeze the whole image to be 1 px tall, then stretch it back to the original dimension. The
Hello World by Examples 33

resizing step will “automatically” take care of the averaging process, and you don’t have to bother
with loops at all.
That’s a great lesson – writing code is an error-prone, tedious process: pick the algorithm (the plan)
that offers maximum output with minimum effort.
Ok, let’s delve into those few lines, and find out their secrets. The first step is storing into a variable
the document reference:

1 var doc = app.activeDocument;

Do you remember what you did earlier with the dollar object? You’ve accessed one of its properties
(called os) using the dot . operator, like: $.os. Same thing here, you’re accessing a property of the app
object called activeDocument. The activeDocument is, well, the document (image file) that happens
to be the active one in Photoshop.
Open the Object Model Viewer, select “Adobe Photoshop CC-whatever” in the dropdown list just
below the “Browser” title (left column), look for the “Application” Class, “activeDocument” Property:

Here it is: the data type is “Document” (see that it is blue and underlined? Click it):

It reads: “The active containment object for the layers and all other objects in the script; the basic
canvas for the file.” The left column lists for the Document Class lots of Properties and Methods, e.g.
'activeChannel', 'activeHistoryBrushSource', 'activeHistoryState', etc.
Hello World by Examples 34

Storing into a doc variable a reference to the open document makes it easier to access the truckload
of other properties and methods – in fact you then write:

2 var currentWidth = doc.width;


3 var currentHeight = doc.height;
4 var currentResolution = doc.resolution;

width, height, and resolution are properties of doc, a variable that you’ve used as a shortcut (a
reference to) one of the properties of app, namely the activeDocument. Using the doc variable instead
of app.activeDocument each time makes the code less verbose and more readable. Onwards:

6 doc.resizeImage(currentWidth,
7 new UnitValue (1, "px"),
8 currentResolution,
9 ResampleMethod.BILINEAR);

Scrolling down in the “Properties and Methods” for a data type “Document” you find the method
resizeImage() (remember: it has a red icon because it’s a method and not a property):

I know what you’re thinking: yes, basically it’s a Russian doll:

app has a property (blue) called activeDocument, which has a method (red) called resizeImage().
Omitting the optional parameter for clarity purposes, the resizeImage blueprint goes like that:
Hello World by Examples 35

Let’s now examine each parameter, one by one, referring to the actual command from the script,
that I’ll copy below again as a reference:

6 doc.resizeImage(currentWidth,
7 new UnitValue (1, "px"),
8 currentResolution,
9 ResampleMethod.BILINEAR);

The first parameter, width: it won’t change, because we’re resizing the image vertically, not
horizontally. The original image width has been stored in the currentWidth variable (line 2).
The second parameter, height: since we’re crushing the original image into a 1-pixel wafer, the new
height will be just 1px. So what about that new UnitValue (1, "px") of line 7? The proper way to
express “1 pixel” in ExtendScript code is to instantiate the UnitValue class, passing the number and
unit of measure as parameters to the constructor function. I hope you’re sporting your best “?!?”
expression – I would if I hadn’t ever heard about classes and instances. Here it goes, the Classes 101
crash course.
Hello World by Examples 36

Classes and Instances


I’m not the fast food kind of man, but once in a while, my wife and I we bring the 12yo
to McDonald’s or any nearby available equivalent. One time, I made a joyful discovery:
they’ve installed several touch screens as tall as I am, using which you’re able to order your
junk food without interacting with a human being. Coolest thing ever for introverted book
authors around the world. You scroll, and scroll, and scroll through screens of food until you
find the one you think you’re going to like the most.
Those pictures, archetypal representations of sandwiches, are Classes. You can’t eat them;
they’re standing for the actual sandwich – the one you order and pay for, that somebody
will bring you shortly thereafter. This is the actual meal of yours that you can consume.
The squishy thing that you hold in your very hands is an instance of the Cheeseburger
class. To make it, the McDonald’s employee uses a constructor, which is a function that
is run each time s/he is cooking a Cheeseburger instance and returns it. One Class, many
possible instances. Some constructor parameters such as bread, hamburger, and cheese may
be required or implied; others are optional – no cucumber for me, please.
I’m confident that any JavaScript expert reading this has died of an heart-attack about in the
middle of the last paragraph, so s/he won’t mind if I don’t dig too much into the difference
between a language based upon Classes and Instances (such as C and Java) or upon Objects
and Prototypes (JavaScript and ExtendScript). If you’ve grasped the cheeseburger thing, I’m
already happy.
But anyway: in JS everything is an Object (stuff with Methods and Properties), and there’s a
thing called a prototypical object: “an object used as a template from which to get the initial
properties for a new object”⁵. A Cheeseburger has the Sandwich object as its prototype,
which in turn has its own prototype (an Edible Squishy Food), up to the prototypal chain
until you reach the Object object. McDonald’s example shows its limits when it comes to
Class methods and properties as opposed as Instance ones, but that’s OK for a multinational
corporation and Scripting.

Back to the UnitValue class. We need to tell the resizeImage() function that we want 1 pixel, so we
create an instance of the class using the new operator.

How do I know that it works this way? I’ve looked in the Object Model Viewer and found an
⁵Quoted from this page on MDN, more on Object Oriented JS here.
Hello World by Examples 37

almost useless description: “Represents a measurement as a combination of values and unit”. Since
UnitValue is part of the Core ExtendScript language, I’ve looked in the JavaScript Tools Guide PDF,
page 230, where you find all the needed operational details. Yes, the Object Model Viewer isn’t the
bible – like the better known one, we’ve had many scripting evangelists writing different bits, in
different places, at different times. Enough for the height.
The third parameter, resolution: it won’t change, so we use the original one that we’ve previously
stored in the currentResolution variable.
Fourth parameter, resampleMethod: this is what you’d enter in the Photoshop “Resize Image” dialog
yourself:

As you see⁶, there is a set of few, fixed values you can choose from. This happens very frequently:
layer’s blending modes are another example. Scripting deals with that using constants that are
usually named consistently and helpfully. Let’s find out. Documentation is your friend, and bear
with me if in this Chapter I describe every little step, but the sooner you learn to flip doc pages
effectively, the more productive you’ll be. The Object Model Viewer doesn’t help too much. Resize
Image is a Photoshop command (not a Core ExtendScript one), so the place to look at is the Photoshop
JavaScript Reference PDF, which says:

Apparently, the resizeImage method has one optional parameter called resampleMethod, which
in turn is of type: ResampleMethod (capital R). Click it, and you’ll be teleported to the related
documentation section:
⁶Real food in there.
Hello World by Examples 38

If this leaves you slightly confused, it is perfectly fine, let’s see if I can clarify. Methods accept
parameters, this you have already seen. However, what are eventually parameters made of? In
JavaScript and ExtendScript, they can be anything: numbers, strings, booleans, objects, functions,
etc. Their kind is called the type: 18’s type is number, true’s type is boolean, etc.
In the resizeImage case, one of its parameters has a very peculiar type, which doesn’t resemble
anything seen so far: ResampleMethod. When, in the Photoshop Scripting documentation, you run
into weird types (other examples being, e.g. LayerKind and BlendMode) you have found a special
kind of constants.
Instead of just telling us that we should refer to the Bilinear resampling as, say, 3 and the Bicubic
as 4 (perfectly valid, yet rather arbitrary numbers), Adobe engineers have provided us with a utility
object called ResampleMethod, which also gives the name to the parameter type. The resampling
methods are then stored as properties of this object, named according to their nature:

• ResampleMethod.AUTOMATIC
• ResampleMethod.BILINEAR
• ResampleMethod.BICUBIC
• etc.

The naming convention is always ConstantType.CONSTANTVALUE⁷.


To recap: the resizeImage method accepts a parameter, which type is defined as ResampleMethod;
in turn, ResampleMethod happens to be an object, which UPPERCASE properties (the part after the
dot) refer to the available resampling methods. They are listed in the documentation as constants,
and the one we’ve picked is ResampleMethod.BILINEAR.
The last line of the script we’ve to deal with is similar – we’ve squeezed the image to be 1 pixel tall,
we now need to expand it back to its original height, so:

⁷Under the hood, the resampling methods are translated to integer numbers anyway. Nothing prevents you from using 3 and 4 in place of
for ResampleMethod.BILINEAR and ResampleMethod.BICUBIC if you want.
Hello World by Examples 39

11 doc.resizeImage(currentWidth,
12 currentHeight,
13 currentResolution,
14 ResampleMethod.NEARESTNEIGHBOR);

The width doesn’t change, so it’s currentWidth; height is no more 1 pixel but the original
currentHeight; resolution is still currentResolution; finally Michel Mariani has chosen a different
resampling method, namely ResampleMethod.NEARESTNEIGHBOR.

If you wonder why the resampling methods Michel has used are BILINEAR and
NEARESTNEIGHBOR, you need to understand a bit of the math involved. Try creating a
grayscale 1x5 pixels image with some random values (you’ll have to measure each pixel),
shrink it to 1x1 pixel using various resampling methods and find out the one that does
actually average them properly. Then do the opposite, expand back to 1x5 pixels and test
the better ResampleMethod. It turns out that they’re in fact BILINEAR and NEARESTNEIGHBOR.

Time to test your script! I assume you’ve written it in ESTK, so make sure it targets the correct
Photoshop version that you’re running. Open an image, switch to ESTK and run the script (you
know how to do it right? If not, have a look at the first Hello World! example). Does it work? If
not, chances are you’re targeting ESTK and not Photoshop, or you’ve mistyped the code – see the
provided JSX file and run that.

Variable Names
This simple script has used variables named currentWidth, currentResolution or doc. It is a
good practice to choose names that don’t interfere with Globals (aka global variables). These
are pre-defined objects, for they don’t need to be explicitly created by you: they already exist
in what is known as the Global Space. Have a look at this:

var name = "Davide";


alert(name);
// "Adobe Photoshop"

You’ve just assigned to the name variable the string “Davide”, how is that it gets logged as
"Adobe Photoshop"?! It turns out that the ExtendScript engine makes available by default
an object called app (which refers to Photoshop itself). This app object, in turn, has a name
property, that has precedence over the one you’ve defined yourself. I will cover Globals later
on, so just be warned now: use myName, firstName, whatever, but not name.
Funnily enough, if you run the same script targeting ESTK, it logs "Davide" as you’d expect.
So two lessons here: if a variable name sounds too generic to your ears, change it⁸; even if
you test your scripts in ESTK, always give them a run in Photoshop.

⁸I’ve seen several approaches to the variable naming problem, e.g., a very experienced developer always uses just one/two letters, another
one prefixes every variable with an underscore. I’m not dogmatic here: you’ll find your preferred naming convention over time.
Hello World by Examples 40

3.3 Layers Spring cleaning


The previous assignment has covered ExtendScript features such as variables, objects, including the
uber-useful $ object, the dot operator, Classes and Instances, Photoshop constants, not to mention
how to browse Documentation – not bad for an Average exercise! On with a new example that deals
with other language features: I’ll pick up another realistic scenario, which goes as follows:
You’re working in the graphics department of a large company in, say, Sweden⁹. Your team manager
is about to enforce a strict policy on Photoshop retouching: layers must be named according to a
precise two pages guidelines, and before delivery to the backup server, all unused layers should
disappear. Time for a script.

Problem: delete every layer that is switched off (not visible).

The rigid naming convention is there to make the script beginners-friendly: in the following image,
you see the initial Layers Palette state (with garbage items) at left, and the desired, cleaned one at
right:

There is a Group called Retouch, which con-


tains two nested Groups (Skin and Back-
ground) with various stuff in it. On top of the
outer Group, extra layers are allowed – but
not sub-Groups, or other Groups.
What would you do, as a human being, to
accomplish the cleaning task? From top to
bottom, consider a layer: if it’s visible (the eye
icon is switched on) skip to the next layer,
otherwise delete it. If the layer is a Group,
open it and look inside. On and on, until you
reach the bottom of the Layers stack.
Brilliant strategy, you have to formalize it
in a way that the ExtendScript engine un-
Layers palette: given state, and desired result derstands. Programming classes teach the so-
called “Divide and Conquer” approach: split
the main problem into smaller, more manageable, sub-problems; and tackle them one by one until
the sun goes down.
Often, before drawing the workflow, you need to prototype each idea to see if that very LEGO
brick fits in the grand scheme of things. For instance: how do you switch off a layer? As always,
documentation to the rescue:
⁹An Italian friend of mine who has moved up there and returned to Italy some 10 years after, tells me that Sweden is a country where
breaking rules isn’t even conceivable; except for weekends, and after alcohol consumption – she says.
Hello World by Examples 41

A Layer has a property called visible, which is of type boolean: either true or false.

Stop for a moment, and consider this activeDocument.activeLayer thing. You’ll see more about
documents and layers later in this course, but for the time being: Photoshop keeps Collections of
useful things – such as Documents, or Layers – that you can access in a variety of ways:

app.documents; // The Documents collection


app.documents[0]; // Getting one Document, by Index
app.documents.getByName("One.psd"); // Getting one Document, by Name
app.activeDocument; // A special shortcut to the currently active
// Document in the Documents collection

var doc = app.activeDocument; // Save some typing using the 'doc' var
doc.layers; // The Layers collection
doc.layers[0]; // Getting one Layer, by Index
doc.layers.getByName("Background"); // Getting one Layer, by Name
doc.activeLayer; // A special shortcut to the currently
// active Layer in the Layers collection

The first thing to notice: activeDocument and activeLayer share the same concept: they’re both
properties of their respective parent Object – i.e., you’re accessing nested properties. When you
write app.activeDocument.activeLayer you are referencing (read it backward) the active Layer,
from the active Document, from the Photoshop app. In other words: the application object has an
activeDocument property that happens to reference a Document from the Documents collection;
Hello World by Examples 42

which in turn has a property called activeLayer which references the currently active Layer in the
Document’s Layers collection.
The square brackets that you have spotted for instance in doc.layers[0] are used to both create,
and pick elements from, Collections: and Collections are in fact Arrays¹⁰.

1 var myArray = ["Burger", "Pizza", "Tagliatelle"];


2 myArray[0]; // "Burger";
3 myArray[1]; // "Pizza";
4 myArray[2]; // "Tagliatelle";

Arrays are lists of various stuff (in this case Strings, but can be whatever, even non-homogeneous:
like Strings and Numbers and Objects) that you can access by Index, the first one being 0, not 1:
Arrays in ExtendScript are zero based. They are a critical topic that you need to understand – please
refer to the resources I’ve mentioned, I’m just overviewing a lot of different concepts here.
Let’s check if the code to get the Layer visibility actually works: create a brand new document in
Photoshop with just a Background layer, and in the ESTK Console you type (and then hit return):

app.activeDocument.activeLayer.visible;
// Result: true

Yup, the Layer is on! Create a new Layer on top of the Background, click the “eye” icon in the Layers
palette to manually switch it off, and rerun the code. The Console should log: Result: false, does
it? We’ve been able to successfully get the visibility status.
¹⁰In theory, Collections in ExtendScript are a special kind of Arrays, but for practical purposes in Photoshop we can consider them just like
Arrays.
Hello World by Examples 43

If you’re wondering, you can similarly set it too, assigning a boolean value to the .visible property.
Make sure the Layer is switched off (as we left it a moment ago), and try running this line of code,
either typing it directly in the Console (and pressing return) or in a blank ESTK File (then press the
green Play icon).

app.activeDocument.activeLayer.visible = true;

It works, does it? It has switched the “eye” icon in the Photoshop Layers palette on for currently
active Layer. You can now go on drawing the master plan.

Especially when you’re green on coding, the main goal is “Make it work”, and then it’s
just confetti falling from above. Later on, it becomes “Make it elegantly work”, with fetish
detours such as “Make it elegantly work using Patterns”. You’re getting really good when it
is “Make it work fast”.
In the first Chapters I’m writing code that is not fast, nor elegant, but hopefully clear; time
permitting, you (and I here as well) will write better solutions to the same problem. It’s
perfectly fine to make it barely work now! Also, the apparently rigid constraints given in
the exercise (like no sub-groups), make it easier to solve it. In my experience, many efforts
go in dealing with edge-cases, and error management.
Hello World by Examples 44

Thanks to the strict layers policies we’ve to deal with, the script could be pretty easy, here’s the
metacode:

.
├── for each ArtLayer
│ │
│ └── if Layer is not visible
│ └── Delete Layer

└── open Retouch LayerSet

├── open Skin LayerSet
│ └──for each layer that is not LayerSet
│ │
│ └── if Layer is not visible
│ └── Delete Layer

└── open Background LayerSet
└──for each layer that is not LayerSet

└── if Layer is not visible
└── Delete Layer

Cool. Let’s try to implement the first part, dealing with the ArtLayers just above the Retouch
LayerSet. Let me propose you this snippet, which I’m going to break apart and discuss thoroughly
in the next pages:

1 var doc = app.activeDocument;


2 for (var i = 0; i < doc.artLayers.length; i++) {
3 if (!doc.artLayers[i].visible) { // '!' means 'NOT'
4 doc.artLayers[i].remove(); // remove() is a method
5 // of the ArtLayer object
6 }
7 }

The for loop is one of the available loops in JavaScript: a structure you use to repeat the same set
of instruction over and over again. It works as follows.
You create a counter, here the i variable, which is initialized to 0. The counter is then compared
against the document’s ArtLayers total number (doc.artLayers.length¹¹), which is an integer value
(a number like 3 or 312): is the counter less than this number? If this is the case, the counter is
increased by one (i++ means i = i + 1), and the content of the curly brackets is executed. The
¹¹length is a property of all Arrays, which defines how many elements the Array contains: ["a", "b", "c"].length is equal to 3.
Hello World by Examples 45

condition is then tested again, and the curly brackets executed again, no matter how many times,
until the i counter is not anymore less than doc.artLayers.length. The loop then finally ends.
In the code we’re testing, the for loop, in turn, contains another common JavaScript/ExtendScript
structure you need to know: the if conditional, which is in the following form.

if (condition) {
// run when the condition is true
} else {
// run when the condition is false
}

The content of the first set of curly brackets is executed only if the condition evaluates to true; if
it’s false, the content of the else’s curly brackets is executed instead – in case the else is there (it’s
optional).
I repeat the actual if statement below for your convenience:

1 if (!doc.artLayers[i].visible) { // '!' means 'NOT'


2 doc.artLayers[i].remove(); // remove() is a method
3 // of the ArtLayer object
4 }

It can be read as follows. Condition: is the artLayer that sits at index i of the artLayers collection
of the currently active document not visible? If so, then use the remove() method to delete it. The
condition we’re testing is !doc.artLayers[i].visible. The exclamation mark ! is used to reverse
boolean values, i.e., transforming true to false, and vice-versa. You can read it in English as “not”:
if the property we’re looking at is visible, the entire condition evaluates to true when the layer is
!visible, or “not visible”. I hope it makes sense.

The snippet we’re testing is perhaps now less scary than it was at first sight, isn’t it?

1 var doc = app.activeDocument;


2 for (var i = 0; i < doc.artLayers.length; i++) {
3 if (!doc.artLayers[i].visible) {
4 doc.artLayers[i].remove();
5 }
6 }

I.e., we’re for looping through the Layers of the active Document, checking if each one of them is
not visible. If this is true (when it is not visible), we remove it.
Give this a run with a demo file which initial configuration is depicted on the left of the following
illustration, and it works! You are tempted to start implementing the next part, but as a double-
check… let’s try with a different layers stack (see the right side of the illustration) with three inactive
Hello World by Examples 46

layers above the outer LayerSet, and not just one. Rats! It removes just two of the inactive ones, but
leaves a Levels layer there, how’s that possible?

Two different starting points: the second one fails!

Believe me, it is a stroke of luck: imagine running into that problem after having implemented all the
rest! Lot more code to debug. Always test your routines on different inputs: this approach is clearly
flawed, and it’s a quite common error among starters indeed. What’s the deal here? You are looping
through an Array by index, and at the same time you’re removing Array items – sabotaging the
whole process. Let’s see.
If you remember, in the test .psd we have three inactive layers, top to bottom: “Vibrance”, “Levels”,
“Curves”. They should be all deleted, but they’re not: pretend you are the ExtendScript interpreter,
let’s now simulate what happens on each iteration.

// First run
array = ["Vibrance", "Levels", "Curves", "BG"];
i = 0; array.length = 4; 0 < 4 // Yes, the condition is true: go on
array[i] = array[0] = "Vibrance"; // "Vibrance" is inactive, it's deleted
i++; // i is added 1 => (0 + 1) = 1

// Second run
array = ["Levels", "Curves", "BG"];
i = 1; array.length = 3; 1 < 3 // Yes, the condition is true: go on
array[i] = array[1] = "Curves"; // "Curves" is inactive, it's deleted
i++; // i is added 1 -> (1 + 1) = 2

// Third run
array = ["Levels", "BG"];
i = 2; array.length = 2; 2 < 2 // NO! 2 is not less than 2, the loop ends

There’s something wrong with the way we’ve looped through the array… The array.length is
always changing, might that be the problem? What if we store it in a dedicated variable in advance:
Hello World by Examples 47

1 var doc = app.activeDocument;


2 var len = doc.artLayers.length; // store in a variable the original length
3 for (var i = 0; i < len; i++) {
4 if (!doc.artLayers[i].visible) {
5 doc.artLayers[i].remove();
6 }
7 }

No way, you’ll run into an Error: “No such element”. Why? Let’s check ourselves.

// First run
array = ["Vibrance", "Levels", "Curves", "BG"];
len = 4;
i = 0; 0 < 4; // Yes, the condition is true: go on
array[i] = array[0] = "Vibrance"; // "Vibrance" is inactive, it's deleted
i++;

// Second run
array = ["Levels", "Curves", "BG"];
i = 1; 1 < 4; // Yes, the condition is true: go on
array[i] = array[1] = "Curves"; // "Curves" is inactive, it's deleted
i++;

// Third run
array = ["Levels", "BG"];
i = 2; 2 < 4; // Yes, the condition is true: go on
array[i] = array[2] // ?? Only elements 0 and 1 exist! –> Error is fired

We’re totally out of luck, sigh! As I’ve mentioned earlier: never, ever mess with an Array while
looping through it. On the bright side, we’ve been able to understand why the loop has failed –
simulating each iteration. A bit of manual labor, that was possible because the code is rather simple.
When the scenario is more complicated, you need to turn to proper debugging techniques, as we’re
about to see.

3.4 ESTK debugging


Let’s stick with our dear little snippet of broken code:
Hello World by Examples 48

1 var doc = app.activeDocument;


2 var len = doc.artLayers.length;
3 for (var i = 0; i < len; i++) {
4 if (!doc.artLayers[i].visible) {
5 doc.artLayers[i].remove();
6 }
7 }

In ESTK, if you click right beside a line number, a red circle appears: it’s a breaking point. Next time
you run the script (the green play button), it would halt there waiting for your input, highlighting
the line with yellow.

ESTK Debugging

While time is suspended, you’re allowed to write statements in the Console to extract information,
say, about the Layer’s name. Alternatively, look in the “Data Browser” panel, where you’re presented
with a hierarchic tree view of the status of all properties and available methods.
In order to continue, you have several options, either as menu items or buttons (the one besides Play,
Pause and Stop): "Debug > Step Into" will evaluate the current line, and if a function is invoked
there, you’ll be teleported inside that function; "Debug > Step Out" is used to silently complete
the execution of said function (skipping all the line-by-line walk), and return to the end of the line
that called it; "Debug > Step Over", similarly to Step Into, evaluates the current line but will
call the function (if there is any) and present you with its returned value without jumping inside the
function. Alternatively, "Debug > Run" interrupts the debugging and goes on with the code business,
until another breaking point, if any, is reached.
Hello World by Examples 49

Step Over, Step Into, Step Out

In practice, when you’re debugging you keep stepping into or over, line-by-line, inspecting the Data
Browser or manually querying the Console, trying to understand what the heck is happening, and
why it isn’t at all what you would initially expect.
With a similar result, instead of pinning a breaking point on ESTK, you can explicitly write debugger
within your code.

1 var doc = app.activeDocument;


2 var len = doc.artLayers.length;
3 debugger;
4 for (var i = 0; i < len; i++) {
5 if (!doc.artLayers[i].visible) {
6 doc.artLayers[i].remove();
7 }
8 }

ESTK will suspend the execution of the script at the debugger line, waiting for you. In case you need
to activate a breaking point based on a condition, I suggest you to use $.bp() – for instance this
way:

1 var doc = app.activeDocument;


2 var len = doc.artLayers.length;
3 for (var i = 0; i < len; i++) {
4 $.bp(i == 2); // you enter debugging mode only
5 // when i will be equal to 2
6 if (!doc.artLayers[i].visible) {
7 doc.artLayers[i].remove();
8 }
9 }

If you’re not patient enough to manually click and run line by line, you can always debug the old
way, logging messages to the Console:
Hello World by Examples 50

1 var doc = app.activeDocument;


2 var len = doc.artLayers.length;
3 for (var i = 0; i < len; i++) {
4 $.writeln("i = " + i + "; len = " + len); // here
5 var layerName = doc.artLayers[i].name
6 $.writeln("Layer name: " + layerName); // here
7 if (!doc.artLayers[i].visible) {
8 $.writeln("NOT visible"); // here
9 doc.artLayers[i].remove();
10 $.writeln(layerName + " has been removed"); // here
11 }
12 }

When running that code on our test image file, the ESTK Console produces the following output,
before halting with the “No such element” error (see it in the status bar, at the very bottom of ESTK
window) and highlighting the offending line in orange:

To complete this brief debugging overview, I’d like to mention try/catch blocks:

1 try {
2 var doc = app.activeDocument;
3 var len = doc.artLayers.length;
4 for (var i = 0; i < len; i++) {
5 if (!doc.artLayers[i].visible) {
6 doc.artLayers[i].remove();
7 }
8 }
9 } catch(e) {
Hello World by Examples 51

10 $.writeln(e.message + "\nLine:" + e.line);


11 }

Whenever an error is thrown (happens) in between the try curly brackets, the pointer goes straight
to the catch block, and the Error object passed as a parameter – you can inspect it and extract
properties such as the message, an hopefully meaningful description of it, and line i.e., where it
exactly happened. In our case it’ll output just:

No such element
Line:5

Big news, the whole script itself won’t break: the code that follows the catch braces is then executed
normally. A general recommendation is not to overuse try/catch, because you are supposed to take
care of all the conditions that may generate errors, and directly deal with them: but if you feel lost
and/or there’s something you cannot control, wrap a chunk of code with try/catch. An example is
File reading from disk: a lot of bad things can happen (from disk failure to server disconnection, so
it’s a perfectly reasonable choice there.

The strange "\nLine:" string found in the catch block means: go to a new line (\n) and then
write Line. You don’t add a space, like "\n Line:" or the new line itself will start with a
space. Other so-called escaped characters that you will likely be using are \t (tab) and \r
(carriage return) which as a matter of fact in strings can be used as a newline.
I’ve used, but so far never explicitly talked about, comments – they work this way:

// Double forward slashes comment a single line


var vers = app.version; // They can be appended at the end of a line too
/* Forward slash plus asterisk is used for block comments
that are allowed to span through more lines.
The comment block ends with an asterisk plus forward slash */
$.writeln (vers);
/* Comment block can be used within code as well: */
$.writeln (vers /* This text has no effect */);

Now that you know about the most common ways to deal with debugging, it’s time to get back to
our layers loop and finally fix it. Ready?
What if, instead of deleting items in the layers array, you keep a reference of them somewhere, and
delete them all afterward when the looping is completed? Let’s try.
Hello World by Examples 52

1 // Still dealing with the layers above the Retouching group only
2 // We'll deal with LayerSets later on
3 var doc = app.activeDocument;
4 var len = doc.artLayers.length;
5 var layersToDelete = []; // Array that will contain the layers to delete
6 for (var i = 0; i < len; i++) {
7 if (!doc.artLayers[i].visible) {
8 layersToDelete.push(doc.artLayers[i]);
9 }
10 }
11
12 // Loop through the layersToDelete array to delete each layer
13 for (var j = 0; j < layersToDelete.length; j++) {
14 layersToDelete[j].remove();
15 }

It works! Let’s recap: a layersToDelete Array literal¹² is created (line 5), empty. The for loops
through all the available ArtLayers, but this time if a layer which visible property is false is found
(remember: the ! means “not”) its reference is stored inside the layersToDelete Array, and not
removed straight away as we’ve done so far.
What is a reference in this case, and how can you store it into an Array? It’s like if you were taking
note of what Layer you want to wipe out, looking at all of them and writing down the ones which
need to be removed, – say, on a piece of paper. In fact, “writing down” becomes in our case “storing
them inside an Array”. How? To fill an Array with elements, you use its .push() method, passing
as a parameter what you want to put in the available slot¹³.

var a = []; // an empty array


a.push(1); // filling the array with 1
a.push(2);
a.push(3); // [1, 2, 3]

Line 13, another for is looping through layersToDelete to remove() them. However, why deleting
from that Array doesn’t break the loop this time? Because the layersToDelete contains references:
links, pointers to the actual Layers. At the restaurant, the cardboard menu doesn’t contain real food,
it references the available meals. On each loop run, the reference is used to run the .remove() method
on the actual ArtLayer object it points to. After, the actual ArtLayer is gone, but not the reference
itself in the layersToDelete array: it’s still there, only it points to… nothing. Which is not a problem
at all, the loop is now on to the next iteration, and won’t use it anymore.
¹²You use the so-called literal notation when creating arrays with square brackets, e.g. var a = [1,2];. This is generally preferred over
it’s alternative, var a = new Array(1,2);
¹³push() adds elements from the Array’s tail (the end). If you want to insert items from the Array’s head, you have to use unshift().
Hello World by Examples 53

With a safer workflow tested and approved, you have to implement LayerSets cleaning. One way is
as follows:
Working code for Layers Spring Cleaning - not optimized
1 var doc = app.activeDocument;
2 var layersToDelete = [];
3
4 var outerLayers = doc.artLayers;
5 var skinLayers = doc.layerSets[0].layerSets.getByName("Skin").artLayers;
6 var bgLayers = doc.layerSets[0].layerSets.getByName("Background").artLayers;
7
8 // Outer ArtLayers
9 for (var i = 0; i < outerLayers.length; i++) {
10 if (!outerLayers[i].visible) {
11 layersToDelete.push(outerLayers[i]);
12 }
13 }
14
15 // Skin Set ArtLayers
16 for (var j = 0; j < skinLayers.length; j++) {
17 if (!skinLayers[j].visible) {
18 layersToDelete.push(skinLayers[j]);
19 }
20 }
21
Hello World by Examples 54

22 // Background Set ArtLayers


23 for (var k = 0; k < bgLayers.length; k++) {
24 if (!bgLayers[k].visible) {
25 layersToDelete.push(bgLayers[k]);
26 }
27 }
28
29 // Remove layers
30 for (var z = 0; z < layersToDelete.length; z++) {
31 layersToDelete[z].remove();
32 }

You will notice how I’ve accessed the ArtLayers collection in the nested LayerSet:

It might seem strange, but hopefully, it makes sense. You can read the illustrated code line above
backward, like: the ArtLayers collection that is inside a LayerSet which name is "Skin", which in
turn is inside the LayerSet that has the index 0 in the document’s LayerSets collection.
Reading it forward: the document has a LayerSets collection, we want its first item (the one with
index 0). In turn, such LayerSet has its LayerSets collection: this time we’re don’t want to get it
by index but by name: we’re interested in "Skin". In turn, this inner LayerSet has an ArtLayers
collection, which eventually we store into a skinLayers variable for convenience. Also, note that it
is perfectly fine to mix “by name” and “by index” syntaxes.
The loops are then constructed as we did in our first example: each for fills the layersToDelete
Array, that is eventually used as a list that contains all the ArtLayers we want to remove().

I have been using these words quite carelessly so far, but it’s time to clarify. In Photoshop
you can deal with Layers, ArtLayers, and LayerSets (mind the final “s”: if it’s plural, then
it is a collection).
A LayerSet is also known as a Layer Group, an ArtLayer is everything else (that is not
a Layer Group, e.g., a Text Layer, a Bitmap Layer, etc.). Quite logically, the LayerSets
collection contains LayerSet items, while the ArtLayers collection contains ArtLayer items.
A Layer is either a LayerSet or an ArtLayer, hence the Layers collection contains both
ArtLayer and LayerSet items¹⁴.
¹⁴If you feel completely lost, think about Apples and Oranges collections, containing Apple and Orange items. Then there’s the Fruits
collection, which contains Fruit items. Apple and Orange items are also featured in the Fruits collection as Fruit items.
Hello World by Examples 55

There is one last, small optimization that I would like to make before calling quit on this exercise.
Earlier in this Chapter, I have mentioned functions, so it may be convenient to expose those of
you who are approaching software development for the first time to this fundamental concept. A
function is a way to encapsulate a task in a reusable form: as a rule of thumb, each time you run
into the same code that does the same kind of job in different places of your program, you may want
to wrap it with a function and just call it as many times as needed.
Let’s use a dummy example first, to understand the syntax; then we’ll applicate our knowledge to
the Layers code. Say that you are debugging a Script, and you want your log messages to be both
popped up as alerts and written to the ESTK Console without bothering to write the same string
twice each time:

alert ("Script Started!");


$.writeln ("Script Started!");
// ...
alert ("About to loop the Array");
$.writeln ("About to loop the Array");
//...
alert ("Finished with the Array");
$.writeln ("Finished with the Array");

You get the idea. Instead, you would rather prefer something shorter:

logAll ("Script Started!");


// ...
logAll ("About to loop the Array");
//...
logAll ("Finished with the Array");

The problem is that logAll() is not provided by Photoshop, so you have to… build your first function!
This example is ideal, because you have a repetitive pattern, with one single variable: the string
used for logging purposes. In other words, you need to do always the same thing – alert() and
$.writeln() – each time with a different message. The code is as follows:

1 // function declaration
2 function logAll (message) {
3 alert (message);
4 $.writeln (message);
5 }
6 // function call
7 logAll ("Debugging time...");
Hello World by Examples 56

That’s it! You first write the function keyword, followed by the function name, the one you will
be calling each time you want to execute it: in this case, it’s logAll. After the name comes a set of
parenthesis () that wrap the function parameters: these are the things the function uses/operates
upon. In the example, just message: this, too, is arbitrarily named: I could have called it mess, or
myMessage, it doesn’t really matter. The parameter, which can be really anything (a String, an Object,
another Function, etc.), is passed “from outside the function” to the function itself each time you call
it; and it is used “internally” in the function body: the code that is wrapped with curly brackets {},
and where things happen.
When you write a function call in your program it’s like replacing:

logAll ("Debugging time...");

with:

var message = "Debugging time...";


alert (message);
$.writeln (message);

In fact, the "Debugging time..." string that you pass as the parameter of the function is assigned
to the internal message variable (aka local variable) and used throughout the function itself
when needed. A function can also explicitly return a value. E.g., the following function adds an
exclamation mark to the String that is passed as a parameter, and returns the string to… whatever
has called it in the first place:

1 function happify (str) {


2 var happierString = str + "!";
3 return happierString;
4 }
5 // Example of use:
6 var something = "Nice dress";
7 var somethingNicer = happify (something); // "Nice dress!"

Here the "Nice dress" string is passed as the parameter of the happify() function, assigned to the
str local variable, processed, and returned; to whom? Being the function on the right side of an
equal sign, its returned value is assigned on the fly to the somethingNicer variable. Hundreds of
pages have been written to describe in excruciating detail JavaScript functions, so please consider
this one as nothing but a very primitive introduction.
Back to our Spring Cleaning exercise, some very repetitive code is found three times in a row:
Hello World by Examples 57

1 // Outer ArtLayers
2 for (var i = 0; i < outerLayers.length; i++) {
3 if (!outerLayers[i].visible) {
4 layersToDelete.push(outerLayers[i]);
5 }
6 }
7 // Skin Set ArtLayers
8 for (var j = 0; j < skinLayers.length; j++) {
9 if (!skinLayers[j].visible) {
10 layersToDelete.push(skinLayers[j]);
11 }
12 }
13 // Background Set ArtLayers
14 for (var k = 0; k < bgLayers.length; k++) {
15 if (!bgLayers[k].visible) {
16 layersToDelete.push(bgLayers[k]);
17 }
18 }

The for loop it is not exactly cloned three times, but the structure is the very same: you have an
Array (an ArtLayers collection); you loop through it, inspecting the visible property of each one
of its ArtLayer elements; if the property is not visible, you store a reference of such ArtLayer into
the layersToDelete Array. The one and only element that actually varies in these three for loops is
the Array that is tested: the first loop inspects outerLayers, the second loop skinLayers, the third
loop bgLayers. Everything else is the same.
It is a perfect candidate for a function, which we could write this way¹⁵:

1 function pushInvisibleLayers (sourceArray) {


2 for (var i = 0; i < sourceArray.length; i++) {
3 if (!sourceArray[i].visible) {
4 layersToDelete.push(sourceArray[i]);
5 }
6 }
7 }

As you see, the parameter is the Array that needs to be looped through, sourceArray. There is no
explicit returned value here, we’re just interested in the fact that switched off ArtLayers are pushed
to the layersToDelete Array¹⁶.
The final code now becomes:
¹⁵Picking the most appropriate name is an art that I’m afraid I don’t master, so bear with me when you bump into functions I’ve questionably
named.
¹⁶A better function would have involved an extra parameter, namely the Array to push references to, so that layersToDelete is not
hardwired.
Hello World by Examples 58

Working code for Layers Spring Cleaning - refactored using functions


1 var doc = app.activeDocument;
2 var layersToDelete = [];
3
4 var outerLayers = doc.artLayers;
5 var skinLayers = doc.layerSets[0].layerSets.getByName("Skin").artLayers;
6 var bgLayers = doc.layerSets[0].layerSets.getByName("Background").artLayers;
7
8 function pushInvisibleLayers (sourceArray) {
9 for (var i = 0; i < sourceArray.length; i++) {
10 if (!sourceArray[i].visible) {
11 layersToDelete.push(sourceArray[i]);
12 }
13 }
14 }
15
16 // Outer ArtLayers
17 pushInvisibleLayers (outerLayers);
18 // Skin Set ArtLayers
19 pushInvisibleLayers(skinLayers);
20 // Background Set ArtLayers
21 pushInvisibleLayers(bgLayers);
22
23 // Remove layers
24 for (var z = 0; z < layersToDelete.length; z++) {
25 layersToDelete[z].remove();
26 }

You’ve finally reached level 1 (“it works”), congratulation! Be proud of yourself – by the end of this
book you’ll look at this code in a mix of disgust and shame, but who cares now, it does the job.
If you wonder how this can be further optimized, there are several aspects that you may consider.
For instance, as is, the script works only with the strict naming convention we’ve based it upon:
you can make it LayerSets-name independent. Also, to make it fast, I’m afraid you’d need to use
ActionManager code, that will have its coverage on Chapter 6.

3.5 Homeworks
I’ve never liked Hello World exercises myself – they’re rarely useful, but you can’t avoid them…
after all, who am I to break traditions?! This Chapter has been written to be a soft introduction to
many concepts that somebody who is approaching Scripting and programming in general needs to
be aware of.
Hello World by Examples 59

On one side, you’ve been exposed to variables, objects, dot and new operators, methods and
properties, loops, classes, instances, arrays and collections, functions. On the other side, which
is probably more important, you’ve seen how you’re supposed to tackle a problem: thinking
about a plan, splitting it into smaller tasks, testing them, being able to react appropriately when
something goes wrong, using debugging tools to understand the reason why, and effectively
browsing documentation to find a way through the forest. Not bad at all for three little Hello World!
During these thirty-something pages, the problems you’ve got to solve have been pretexts to expose
you to all of the above: from now on, I cannot be so verbose on language details. Which is not to
say that Chapter 4 will be a sudden dive in deep waters! I’ll keep punctuating the whole course with
dedicated sections when I’ll run into topics that I had trouble dealing with, back when I was in your
shoes myself. Yet, you’d better off studying some of the resources I’ve mentioned – or equivalent
ones – to keep up with the following Chapters. There are several language elements that I’ve skated
over (e.g., comparison and logical operators, while loops, closures, and so on). Take your time to
review these pages and familiarize with the language. Then, onwards!
4. Climbing the DOM

The Tree of Life – courtesy of David M. Hillis, Derrick Zwickl, and Robin Gutell, University of Texas
Climbing the DOM 61

4.1 The Tree of Scripting Life


This Chapter’s cover image is one representation of the evolutionary relationships connecting
Earth’s various life forms: what evolutionary biologists would call a big, circular phylogenetic tree¹.
For your information, we’re around 10:30 PM, yellow section, in between Mus musculus (house
mouse) and Typhlonectes natans (rubber eel).
Such a remarkable piece of art/science is there to remind you that stuff in this world is connected –
and Photoshop Scripting makes no difference. I know it’s a bold leap, bear with me.
You’ve already met in the previous Chapter Object properties, which in turn can be Object
themselves. For instance, the app.documents collection, a property of the app object that contains
several document objects. This is to say that it exists a “Tree of Photoshop Scriptable Things” – way
much simpler than the actual tree of life, so each time you’ll find yourself lost in PS code, think that
it could be worse: you could be an evolutionary biologist.
We use to refer to the description of Photoshop internal connections as the Document Object Model,
also known as the DOM. Each time you refer to a property, such as a Guide position, or a Layer’s
opacity, you’re in fact traversing the DOM. We’ve already encountered this one:

app.documents[0].layerSets.getByName("Skin").artLayers[0].opacity

Here, you’re interested (read the code backward) in the opacity of the topmost layer that you get
by Index, inside a LayerSet called “Skin”, which happens to belong to an open document of the
Photoshop application. You’re following the tree, from the main trunk (the app object), up to the
document branch, until you finally reach the opacity limb.
An utterly simplified version of the Photoshop Application Tree is as follows: there, only Collections
are shown.

Photoshop DOM – Available Collections


¹You can print and hang it on the wall, the recommended size is at least 54’’ wide; find it here (B/W).
Climbing the DOM 62

4.2 Collections, Classes and Instances


In case these concepts are still fuzzy in your mind, let’s make the distinction clear once and for all.
The following line sums up everything you need to know: guides are going to be our guinea pig.

var g = app.activeDocument.guides.add(Direction.HORIZONTAL,
new UnitValue(50, "px"));

This is a surprisingly complex statement, which purpose is to add a horizontal Guide at 50 pixels
from the top to the currently active document. How to read it?
The starting point is app, the Photoshop application. You could omit it, because the ExtendScript
parser would look up in the prototypal chain for the activeDocument property, and would find it
for the globally available Application object. I suggest you to keep writing app – in my opinion, the
code is more readable and the “less typing is better” argument never really convinced me.
Next comes activeDocument, a special property of app accessed via the dot . operator, that returns
an instance of the Document Class, which belongs to the Documents Collection: a handy way to
group homogeneous objects.

• app.documents; the Documents Collection.


• app.documents[0]; a Document Instance of the Document Class: accessed by Index from the
collection.
• app.documents.getByName("first"); a Document Instance of the Document Class: accessed
by Name from the collection.
• app.activeDocument; a Document Instance of the Document Class: the active one.

Then you access the guides Collection. Besides getting element by Index, by Name, and the currently
active one (like activeDocument, activeLayer, etc.) you’re allowed to:

• Query the collection to get the total number of items with the read-only length property.
• Query the collection to get the parent property, which refers to the object’s container, the
Document.
• Add elements to the collection via the add() method.
• When possible, empty the collection via the removeAll() method.

That’s the reason why you can add() to the guides. In Collections jargon, the add() method
corresponds to new: it returns a newly created Instance of the Class. In the case of guides, it expects
two parameters.
The first param is a Direction constant: remember, you access a constant following the ConstantType
dot CONSTANTVALUE naming convention, here Direction.HORIZONTAL. Where do you find them listed?
In the JavaScript Reference, page 202.
Climbing the DOM 63

The second param is a value plus a unit of measure (pixels, inches, and so on). You’ve already seen
that you can instantiate inline² the UnitValue Class using the new operator and passing the desired
parameters.
In the end, everything is stored in the g variable. You can treat g as a guide object (an Instance of
the Guide Class, belonging to the Guides Collection). Look up the JavaScript Reference at page 119,
and you’ll find out that Guide (the Class instance) has no methods, but properties: direction and
coordinate. So you’re allowed transform the existing guide through the g object:

// Transform the horizontal guide into a vertical guide


g.direction = Direction.VERTICAL;
// Move the existing guide
g.coordinate = new UnitValue(70, "px");

Readable and Writable properties. Not every property can be modified, some of them are
read-only: as an example parent, check out the JS Reference for a detailed list. The idea
here is that you can act upon objects in two ways: through methods (e.g., to duplicate()
an existing ArtLayer), and through writable properties, directly changing their values.
In a sense, read-only properties have just getters, while read/write ones have both getters
and setters.

Please mind the syntax. You refer to Collections using a capital letter and the plural (Guides,
Documents, ArtLayers), but in code you use camelCase (guides, documents, artLayers) with no
first capital; you refer to Classes using a capital but no plural (a Guide, a Document, an ArtLayer).
So so does the JavaScript Reference. A Class is a theoretic thing (the blueprint for a Guide), the
Instance of a class is the actual object (built following the blueprint: this guide here). Collections are
named after Classes, but only contain Instances – actual stuff, not blueprints.
Also note that when you look at the Documentation for Collections’ methods and props, they’re
usually very few (depending on the type: add(), getByName(), length, etc.). They are meaningful in
that limited context only: what do you do to manage a Collection of things? You count, pick, add,
remove them, and that’s it – it doesn’t make much difference if it’s a Collection of Layers, Guides
or postage stamps. Most of the times instead, you might be interested in Instance’s methods and
prop. In other words, you may want to know how to move, rasterize, desaturate, rotate and whatnot
an ArtLayer instance (ArtLayer, page 54 of the JavaScript Scripting Reference), and not manage the
Collection (ArtLayers, page 66). I’m stressing this concept since it’s proven to be very confusing.

4.3 Scripting practice with Algorithmic Art


I am a fanatic supporter of the hands-on approach. You can master the DOM (and then, what is
beyond it, which for your information is: a lot) only with practice; on the other hand, solving actual
²You could have written var foo = new UnitValue(50, "px") and used foo as the second parameter of the add() method. Here inline
means that you avoid creating an extra variable, putting the instance creation statement directly in place.
Climbing the DOM 64

problems is the way to build up the proper Scripting mindset, so to speak, which is as crucial as
walking the DOM like a pro.

We’re going to try to reproduce the work of Georg Nees,


who’s been a pioneer in the field of Algorithmic Art – also
known as Generative Art, something different people have
different opinions about. A known landmark in Nees’ artworks
is “Schotter” (1965), a plot of squares in a grid arrangement, that
progressively and randomly rotate and move away from their
position.
In Photoshop and in Scripting too, there are several ways to
get to the same result: I’ve decided to use stroked squares
selections on a dedicated layer and not vector shapes, which
would require ActionManager code (the post-DOM world we
still have to explore). It’s possibly not the best option here, but
for demonstration purposes, it’ll be good enough.
Before even thinking about the Schotter grid of squares, let’s
divide and conquer, coping with a single square. In our case
a Photoshop selection – and mind you, from now on I will
assume that you know how to look for information either in
the JavaScript Reference and/or the ESTK Document Object
Viewer.
If you look in the diagram at the beginning of this Chap-
ter, you’ll see that Selections Collection is missing: in fact Georg Nees’ Schotter
selection is just a property of the Document Class. How do
you create a new selection? Since the add() method is a particular feature of Collections, you won’t
find it here: I beg your pardon for the wordplay, but in order to select something, you need to call
the select() method of the selection property, passing as the argument an Array of coordinates,
something like this:

1 // select a square
2 app.activeDocument.selection.select([
3 [2,2], // top-left
4 [8,2], // top-right
5 [8,8], // bottom-right
6 [2,8] // bottom-left
7 ]);

The above is a multi-dimensional Array, i.e., an Array of Arrays – there are four of them because
we’re interested in square selections; you can use three or more for a polygonal selection, like the
Polygonal Lasso Tool). Time to review how Photoshop deals with coordinates.
Climbing the DOM 65

The [x,y] coordinate system uses the upper


left corner as the origin [0,0] and, as opposed
to what we’ve been taught at school, the y
axis positive values go downwards. In the se-
lection code of the previous page I’ve started
with [2,2] (top-left corner) and each other
Array describes the [x,y] points values clock-
wise: so [8,2] is top-right, [8,8] bottom-
right, [2,8] bottom-left. No need to specify a
closure point, the last one automatically joins
the first.
You may now wonder what we’re talking
about: pixels, inches, points? It’s not speci-
fied: both syntaxes are fine, with and without
UnitValue.

1 app.activeDocument.selection.select([
2 [new UnitValue(2, "px"),new UnitValue(2, "px")], // top-left
3 [new UnitValue(8, "px"),new UnitValue(2, "px")], // top-right
4 [new UnitValue(8, "px"),new UnitValue(8, "px")], // bottom-right
5 [new UnitValue(2, "px"),new UnitValue(8, "px")], // bottom-left
6 ]);

In the former case (no UnitValue), the ExtendScript engine assumes that you’re using the Units that
are set in the Photoshop Ruler. So [2,2] is whatever the Ruler happens to be set to, the very moment
you launch the script. Might be 2 pixels like 2 inches³: and even if you’re not NASA losing $125
million Mars orbiter because of a conversion mishap, you should check in advance anyway.

In case you really need precision, please note that Photoshop uses
the pixel’s bottom-right corner (see the image on the right) as the
reference for points. The pixel [2,2] is where the square selection
starts, but it’s not actually selected! Neither are [8,2] or [2,8], whereas
[8,8] ends up being included.

Speaking of position and [x,y] values, Color Samplers work exactly


the same, with a quirk which is as follows: if you pin them manually,
they snap to the very center of the pixel; conversely, if you add them
via Scripting, they position themselves at the bottom-right corner of
the selected pixel; but will sample the color of the one at south-east. E.g., a Color Sample at [2,2]
will show the value of pixel [3,3].
³There is a bug with percents (at least until CC 2018), so it’s not possible to select, say, new UnitValue(8, "%") – even if the Ruler is set
to Percent. It always uses Pixels instead.
Climbing the DOM 66

For simplicity’s sake, let’s try to use the shorter syntax; I’m going to use pixels as the UnitValue, so
the first thing is… to set the Ruler accordingly – which you can do: it’s a Photoshop preference, and
you access the Preferences Collection as a property of the app global object.

It’s a recommended and polite practice, when you have to change them, to store, modify
and then restore the Application Preferences, so the idea here is:

var old = app.preferences.rulerUnits; // store original Prefs


app.preferences.rulerUnits = Units.PIXELS; // modify the Prefs
// ... do your stuff here
app.preferences.rulerUnits = old; // restore original Prefs

So that the user finds the same Ruler Units s/he had before running your script.

Let’s create a brand new document as the canvas for our experiment, accessing the add() method
of the app.documents Collection:

1 var doc = app.documents.add(new UnitValue(100, "px"), // width


2 new UnitValue(100, "px"), // height
3 72, // PPI resolution
4 "Square Test", // name
5 NewDocumentMode.RGB, // document mode
6 DocumentFill.WHITE, // initial fill
7 1, // pixel ratio
8 BitsPerChannelType.EIGHT, // bit depth
9 "sRGB IEC61966-2.1"); // ICC profile

The above specifies all the (optional) parameters; using a shorter syntax, for UnitValues can be
written also as strings, you could even:

1 var doc = app.documents.add("100 px", "100 px");


2 // or just
3 var doc = app.documents.add();

The doc variable now stores the document. Let’s create a new layer via the ArtLayers collection
add() method, again storing it in a variable – we don’t want to draw on the background layer
because we’re oh so tidy:

1 var lay = doc.artLayers.add(); // create a new Layer

Having newly created objects referenced by variables is a common pattern, which utility becomes
apparent when you later want to operate on them like:
Climbing the DOM 67

1 var lay = doc.artLayers.add();


2 lay.name = "A Square"; // rename the layer
3 doc.activeLayer = lay; // redundant in this case

The name property of the Layers class (and in this case, of the lay instance) is read/write – that is
to say: it can be used to retrieve the existing layer name, and/or set a new one. Please note that the
next line (using the similarly read/write activeLayer property) sets the newly created layer as the
active one⁴.
In this very case it is redundant: lay is created and becomes active at the same time (like it would if
you were doing it manually). There are circumstances when this doesn’t happen, for instance when
you duplicate a layer:

1 // Duplicate layers are not active by default


2 var dup = lay.duplicate();
3 doc.activeLayer = dup; // dup is not active by default

So keep that in mind, and explicitly set the activeLayer when needed. Let’s now draw a selection,
with 10 pixels padding from the document bounds:

1 doc.selection.select([ [10,10], [90,10], [90,90], [10,90] ]);

In order to stroke the selection, you first need to instantiate the Solid Color class like that:

1 var col = new SolidColor();


2 col.rgb.hexValue = "000000";
3 // alternatively:
4 col.rgb.red = 0; // RGB values are in the range [0..255]
5 col.rgb.green = 0;
6 col.rgb.blue = 0;
7 // you're allowed to use also hsb, lab, cmyk, gray...

and finally you can:

1 // params are: SolidColor, stroke width, StrokeLocation


2 doc.selection.stroke( col, 4, StrokeLocation.INSIDE);
3 doc.selection.deselect();

StrokeLocation is
another example of Constants. Find the whole little script with minor rearrange-
ments as follows:
⁴I would have written “it selects the layer”, but in this example we’re also dealing with actual, marching-ants selections; hence confusion
might arise. The proper scripting terms for the action corresponding to you clicking a Layer in the Layers palette is “activate”.
Climbing the DOM 68

1 // Save and Modify Preferences


2 var old = app.preferences.rulerUnits;
3 app.preferences.rulerUnits = Units.PIXELS;
4 // Document and Layer
5 var doc = app.documents.add("100 px", "100 px", 72, "Square Test") ;
6 var lay = doc.artLayers.add();
7 // Selection
8 doc.selection.select([ [10,10], [90,10], [90,90], [10,90] ]);
9 // Solid Color
10 var col = new SolidColor();
11 col.rgb.hexValue = "000000";
12 // Stroke and Deselect
13 doc.selection.stroke( col, 4, StrokeLocation.INSIDE);
14 doc.selection.deselect();
15 // Restore Preferences
16 app.preferences.rulerUnits = old;

It works! So we’ve proved successful in drawing a square, we call quit


with our little feasibility test, time to approach the grid.
Let’s start from scratch with the actual project in a new JSX file,
defining four constants: number of rows, number of columns, margin
(the distance between the squares and the image border on the left, top,
and right; the bottom depends on the rows number), and padding (the
distance between squares), assigning some values. Also, I’ll paste part of the previous test’s code.

1 // Constants
2 const ROWS = 20,
3 COLUMNS = 10,
4 MARGIN = 100, // in pixels
5 PADDING = 10; // in pixels
6
7 // Preferences
8 var oldPrefs = app.preferences.rulerUnits;
9 app.preferences.rulerUnits = Units.PIXELS;
10
11 // Vars
12 var doc = app.documents.add("1000 px", "1800 px", 72, "Schotter") ;
13
14 // NEW CODE HERE!
15
16 // Restore Preferences
17 app.preferences.rulerUnits = old;
Climbing the DOM 69

Note that ExtendScript has the notion of const (here I’m using the full-capitals convention for them).
You can’t modify the value of a constant, nor redeclare it: in the first case, the new value doesn’t
stick, while the second throws a “Redeclared” Error.
It’s helpful to look at this illustration (the tiny dark gray squares within the cyan squares define the
top-left corner, that is the reference point for the selection) to derive the square size, i:

If you define w as the Document’s width, m the Margin, p the Padding, s the Square side, and n
the number of Columns, you’re able to write a simple equation and derive the square size from the
other parameters:

w = 2m + ns + (n − 1)p
ns = w − 2m − np + p
w 2m − np + p
s= −
n
w − 2m − p(n − 1)
s=
n

Which in ExtendScript translates into:

var side = (doc.width - 2 * MARGIN - PADDING * ( COLUMNS - 1) ) / COLUMNS;

To define all the points that you’ll be using to draw the squares, you need to nest a couple of loops,
the outer one for rows, the inner one for columns:
Climbing the DOM 70

for (var row = 0; row < ROWS; row++) {


for (var columns = 0; columns < COLUMNS; columns++) {
// ...
}
// ...
}

The problem is… what to do with those loops?

It’s quite easy, think about it like using an old typewriter: you start at the left margin position, then
you type, and the carriage moves by the letter width, plus some spacing, on and on; until you reach
the right margin and a bell rings. Then you feed a new line and return the carriage to the left margin
position.
If you have no idea what I’m talking about you’re too young, and I hate you. Have a look at this
video – yes, we used to have biceps in our fingers.
I’d say a pointer will help us keeping track of all the positions we’ll loop through. I’ll make it as an
array, containing x and y positions:

var pointer = [MARGIN, MARGIN];


// pointer[0] is the x position
// pointer[1] is the y position

The starting point is at the [x,y] position defined by the MARGIN constant. So you have fed the sheet
of paper in the typewriter, you’re at the top-left margins point, what do you do? Start typing (i.e.,
laying down squares). This is the inner loop, that is about columns, and is based on the x position.
We’ll add the square’s side plus the PADDING each time⁵ to get to the next square spot:

⁵If you’ve never encountered the += operator before, x += 2 means x = x + 2, so you’re incrementing x by 2.
Climbing the DOM 71

for (var row = 0; row < ROWS; row++) {


for (var columns = 0; columns < COLUMNS; columns++) {
// ...
pointer[0] += (side + PADDING);
}
// ...
}

When we’ve typed in all the available positions, and we’re done with the line (the inner loop), we
need to return the carriage (i.e., reset the counter for the x position) and also feed a new line (push
the y by the same side plus PADDING amount). This happens in the outer loop, the one which controls
the rows:

for (var row = 0; row < ROWS; row++) {


for (var columns = 0; columns < COLUMNS; columns++) {
// move horizontally to the next square location
pointer[0] += (side + PADDING);
}
// "Carriage Return": resetting the horizontal value
pointer[0] = MARGIN;
// "New Line feed": moving to the next vertical line
pointer[1] += (side + PADDING);
}

Hopefully, this makes sense. To test this, you could log values in the console, but it’s so boring
and visually not very helpful, so why don’t we pin Count Items in the actual image? Guess what,
there is a CountItems (capital, plural) Collection, which has an add() method accepting an array of
coordinates. So:

for (var row = 0; row < ROWS; row++) {


for (var columns = 0; columns < COLUMNS; columns++) {
// Pin a count item at the square location
doc.countItems.add(pointer);
pointer[0] += (side + PADDING);
}
pointer[0] = MARGIN;
pointer[1] += (side + PADDING);
}

The result is a nice, regularly spaced positions in a new document, that define the top-left point of
the squares we’ll eventually be drawing.
Climbing the DOM 72

We’re progressing – slowly, but it’s OK. The Schotter plot is a distribution of squares which random
offset and random rotation increase as they get created: we’ll start with no offset and rotation first.
Next step is to encapsulate the select/drawing code of our early prototype into a function, which
will come handy in our loops.

1 // Make a square selection of given size


2 // topLeftArray: Array of x,y coordinates for
3 // the top-left selection corner
4 // side: the selection's square side
5 function selectSquare(topLeftArray, side) {
6 var originX = topLeftArray[0],
7 originY = topLeftArray[1];
8 app.activeDocument.selection.select([
9 [originX, originY ],
10 [originX + side, originY ],
11 [originX + side, originY + side],
12 [originX, originY + side]
13 ]);
14 }
15 // Example use: selectSquare([10,10], 30);

The above function works even with negative values, that is, in case the origin point is outside of the
document bounds, or the side itself is negative – in which case the selection is going to be symmetric
to the origin point (like if the origin was defined bottom-right instead of top-left). We can now easily
substitute the Count Item placing with actual drawing:
Climbing the DOM 73

1 var col = new SolidColor();


2 col.rgb.hexValue = "000000";
3
4 for (var row = 0; row < ROWS; row++) {
5 for (var columns = 0; columns < COLUMNS; columns++) {
6 // draw the square
7 selectSquare(pointer, side);
8 // Adding a 4 pixels inner stroke
9 doc.selection.stroke( col, 4, StrokeLocation.INSIDE);
10 pointer[0] += (side + PADDING);
11 }
12 pointer[0] = MARGIN;
13 pointer[1] += (side + PADDING);
14 }

What about the offset and rotation now? The latter is performed inspecting the Selection Class (it’s
the DOM Chapter after all), which exposes a rotate() method, while we can directly offset the
top-left point we’ve calculated ourselves.
The problem is that rotate() doesn’t work on empty selections, nor we can afford to work on
the Background layer since squares would overlap and rotating one would rip off bits from other
squares. A solution (not one I like that much, but for this example it does the job) is to keep every
square on its own layer, stroke first and then rotate.
The randomization must increase from square to square: that is to say, you need to generate a random
number in a range of values that grows over positions. Let’s first create a counter that is increased
each time a square is drawn in the loops, like:
Climbing the DOM 74

var counter = 0;
for (var row = 0; row < ROWS; row++) {
for (var columns = 0; columns < COLUMNS; columns++) {
counter++;
// ...

Then the trick is to use the counter (itself, or multiplied by a constant) to calculate a random range
with positive and negative values, that you’ll use to shift and rotate the squares.
A random generator function, accepting the output range as the parameter is also provided⁶.

1 // Returns a random number between min (inclusive) and max (exclusive)


2 function getRandom(min, max) {
3 return Math.random() * (max - min) + min;
4 }
5
6 var counter = 0;
7 for (var row = 0; row < ROWS; row++) {
8 for (var columns = 0; columns < COLUMNS; columns++) {
9
10 doc.artLayers.add();
11
12 // shift
13 pointer[0] += getRandom(-counter, counter);
14 pointer[1] += getRandom(-counter, counter);
15
16 selectSquare(pointer, side);
17
18 // rotate
19 doc.selection.stroke( col, 4, StrokeLocation.INSIDE);
20 doc.selection.rotate(getRandom(-counter, counter),
21 AnchorPosition.MIDDLECENTER);
22
23 pointer[0] += (side + PADDING);
24 counter++;
25 }
26 pointer[0] = MARGIN;
27 pointer[1] += (side + PADDING);
28 }

⁶The function comes from the Mozilla Developer Network.


Climbing the DOM 75

The idea is that you’re building a probability


area (increasing in size on each iteration)
where the square can find its origin point –
and similarly, its rotation.
If you’re lucky, you’ll get more or less around
the square #110, and the script halts: “Could
not stroke the layer because there is nothing
to stroke.”
The randomization is too wild, and the selec-
tion goes off the canvas. As a result, it can’t be
stroked, and Photoshop complains. Moreover,
it might be that you also find half-stroked
squares.
As follow the final script, where I’ve added
multipliers to dampen the shift, and the rota-
tion is restricted only to meaningful values.

1 // Constants
2 const ROWS = 20,
3 COLUMNS = 10,
4 MARGIN = 100, // in pixels
5 PADDING = 10; // in pixels
6 const shiftDampen = 0.06,
7 rotationDampen = 0.45;
8 // Preferences
9 var oldPrefs = app.preferences.rulerUnits;
10 app.preferences.rulerUnits = Units.PIXELS;
11
12 var doc = app.documents.add("1000 px", "1800 px", 72, "Schotter") ;
13 var side = (doc.width - 2 * MARGIN - PADDING * ( COLUMNS - 1) ) / COLUMNS;
14 // Pointer, which will run into all the positions we'll use to draw a square
15 // pointer[0] is the x position; pointer[1] is the y position
16 var pointer = [MARGIN, MARGIN];
17 // Solid Color
18 var col = new SolidColor();
19 col.rgb.hexValue = "000000";
20 // Used to increase the randomness on each iteration
21 var counter = 0;
22 for (var row = 0; row < ROWS; row++) {
23 for (var columns = 0; columns < COLUMNS; columns++) {
24 doc.artLayers.add();
25 // shift
Climbing the DOM 76

26 pointer[0] += shiftDampen * getRandom(-counter, counter);


27 pointer[1] += shiftDampen * getRandom(-counter, counter);
28 // select
29 selectSquare(pointer, side);
30 // rotate
31 doc.selection.stroke(col, 4, StrokeLocation.INSIDE);
32 doc.selection.rotate(rotationDampen* getRandom(-counter, counter),
33 AnchorPosition.MIDDLECENTER);
34 pointer[0] += (side + PADDING);
35 counter++;
36 }
37 pointer[0] = MARGIN;
38 pointer[1] += (side + PADDING);
39 }
40
41 doc.selection.deselect();
42 // Restore Prefs
43 app.preferences.rulerUnits = oldPrefs;
44
45 // Functions
46 function selectSquare(topLeftArray, side) {
47 var originX = topLeftArray[0],
48 originY = topLeftArray[1];
49 app.activeDocument.selection.select([
50 [originX, originY ],
51 [originX + side, originY ],
52 [originX + side, originY + side],
53 [originX, originY + side]
54 ]);
55 }
56 // Returns a random number between min (inclusive) and max (exclusive)
57 function getRandom(min, max) {
58 return Math.random() * (max - min) + min;
59 }
Climbing the DOM 77

Variations on the Schotter theme

4.4 Layers and LayerSets: the DOM approach


No matter what you’ll use Scripting for in your career, a solid understanding of Layers is something
that you’re going need for sure. Our goal here is to understand how id, collection’s index and
itemIndex work. Now run this script:

1 var doc = app.documents.add(new UnitValue("100 px"),


2 new UnitValue("100 px"),
3 72, "Layers Test");
4 var bg = app.activeDocument.activeLayer;
5 var layer1 = doc.artLayers.add();
6 layer1.name = "Layer 1";
7 var layer2 = doc.artLayers.add();
8 layer2.name = "Layer 2";
9
10 logLayers();
11
12 $.writeln("\nBackground won't be background anymore!\n")
13 bg.isBackgroundLayer = false;
Climbing the DOM 78

14 logLayers();
15 $.writeln("Do you know bg? " + bg);
16
17 layer1.move(layer2, ElementPlacement.PLACEBEFORE);
18 $.writeln("\nMoved Layer 1 on top of Layer 2\n");
19 logLayers();
20
21 function logLayers() {
22 for (var i = 0; i < app.activeDocument.layers.length; i++) {
23 var lay = doc.activeLayer = doc.layers[i]
24 $.writeln("doc.layers[" + i + "] " + lay.name + ". id: " +
25 lay.id + ". itemIndex: " + lay.itemIndex)
26 }
27 }

It creates a three layers document, logging some information for each layer: a logLayers() function
encapsulates the needed loop. Then, it performs some changes (which I’ll review in a moment) and
logs layers info again a couple of times.
The results are summed up in the following illustration.

(A) A document is created, (B) Background Layer is unlocked, (C) Layer 1 is moved

First, being doc the app’s active document, the Layers belong to the doc.layers Collection, which
allows you to getByName() or by index, so doc.layers[0], doc.layers[1], etc. This is what
I’ve called the Collection index. A layer has (among the rest) two interesting properties: id and
itemIndex.

In the A screenshot, the layers have been created (please note: using the artLayers collection,
which is the correct one to use, not the layers Collection, which doesn’t tell the difference between
artLayers and layerSets).

In B, the Background layer has been… unlocked⁷, assigning to its isBackgroundLayer property
(which is read/write) the value false.
In C, the Layer 1 has been moved on top of Layer 2. Several things to notice here:
⁷I’m not sure if “unlocked” is the proper word: made it into a regular layer, not Background anymore, freed.
Climbing the DOM 79

• You see that the Collection index starts from 0 in the topmost position (Layer 2) and increases
top to bottom, so the Background has an index of 2. It’s not linked to any actual Layer:
layers[0] is always the topmost, even if it’s been substituted with a different one as in C,
i.e., it’s a positional attribute. Also, being zero-based, there are three layers, but of course no
one is layers[3] – something to keep in mind when looping: the Collection’s length property
is available for that purpose, see line 22.
• The id is a property of the layer, not the position. It is a unique identifier for the layer, no
duplicates are possible. Each time you create a new layer, an id is consumed and assigned to
it. Delete that layer, that id is gone forever. Create a new layer, and the next available id is
assigned. You might guess why, in B, the Background layer’s id changes: when you unlock it,
you’re deleting a Background layer while creating a new one (at the same time), so a new id
must be assigned. To the best of my knowledge, the only exception to the rule of unambiguous
ids is the case when a layer is turned to Background – then, it acquires back the 1 id, which
happens to be reserved for that.
• the itemIndex is a layers’ stack position indicator, and in this very case, it corresponds to the
Collection index in reverse, not zero-based.

Please note that (line 17) the layer’s move() method expects two parameters: the reference element,
in this case layer2, and the element placement. Among the available constants, I’ve picked
ElementPlacement.PLACEBEFORE, which confirms that, in Scripting, layers are counted top to bottom
(and not vice versa): a layer on top of the stack comes before the one below.
We’ve seen the case of a simple stack of layers; what about more elaborated scenarios where
LayerSets are involved?

This is indeed a complex stack, compared to the first one. There are two nested LayerSets, and please
note that the three screenshots show the very same file, with just differences in the way the content
of the Layer Sets is displayed in the Layers palette.
Let’s start with A, where Group 1⁸ is closed. If you inspect the Layers collection:

⁸I’m using Group and Layer Set as synonyms.


Climbing the DOM 80

var doc = app.activeDocument;


doc.layers.length; // 4

You’ll see that it contains four items only, and you don’t get the total number of Layers in the
document: this is the way it works, and you need to be aware of it. This Document object has, at the
same time, an ArtLayers Collection three items strong (Background, Layer 1, Layer 5), a LayerSets
Collection with Group 1 only, and a Layers Collection, containing all four items.
Like in the previous example, the id takes into account the creation order: I’ve started with a new
document with only the Background, then Layer 1, then Group 1. Since Layer 5 has an id of 10, you
infer that I’ve created some Group 1 content first – and you’re right.
Let’s expand the Layer Set (screenshot B). That very Group can be referenced in two equally correct
ways: either as app.layers[1], or app.layerSets[0] because it is both the second Layer from the
top, or the first (and only) LayerSet, in fact, it is the only item in the LayerSets collection. In a
Russian doll fashion, you can then access the Group’s Layers Collection:

doc.layers[1].layers.length; // 3

Which is correct: Group 1 contains three items: Layer 4, Group 2, Layer 2. They have the same
Collection index you would expect: 0, 1, 2.
A quick jump to screenshot B, where all the Layer Sets are exposed. Group 2 is the only Layer Set,
within the only top-level LayerSet (Group 1), of this document:

doc.layers[1].layers[1]; // Group 2
doc.layers[1].layerSets[0]; // this is Group 2 too
doc.layerSets[0].layers[1]; // same Group 2
doc.layerSets[0].layerSets[0]; // and again, Group 2

What about Layer 3, which is nested in Group 2?

doc.layerSets[0].layerSets[0].layers[0];

You can always get them by name, although in this case you need to use the correct Collection:
either ArtLayers or LayerSets.

doc.artLayers.getByName("Layer 5");
doc.layerSets.getByName("Group 1");

Mind you, you’re not allowed to get nested elements by name! The following, alas, won’t work:
Climbing the DOM 81

// Nested Layer Set by name:


doc.layerSets.getByName("Group 2"); // Error: No such element!
// Nested Layer by name:
doc.artLayers.getByName("Layer 2"); // Error: No such element!

Instead, you should walk the DOM properly, using the Collections:

// Nested Layer Set by name:


doc.layerSets[0].layerSets.getByName("Group 2");
// Nested Layer by name
// Get the Layer Set by index, then access the Art Layers Collection,
// then you're allowed to get the Art Layer by name:
doc.layerSets[0].artLayers.getByName("Layer 2");
// Identically, get the Layer Set by name from the Layer Sets Collection,
// then access the Art Layers Collection, and then it's as before
doc.layerSets.getByName("Group 1").artLayers.getByName("Layer 2");

So far so good! Hopefully, you might have noticed something bizarre in the screenshot C. Have a
look at it again, and then come back. So… what’s wrong, can you see it?
Speaking about ids, I’ve created the Background first (id 1), then Layer 1 (id 2); then you’ve to
jump up to Group 1 (id 3). If you remember, I told you to have stuffed some content inside that
Layer Set before getting to create Layer 5, which is, in fact, the last one (id 10). However, there’s
something missing: where are id 4 and 7?
Well, since ids are disposable, I might have created some layers, deleting them * afterward*. It would
explain the missing ids – and if this is your answer I congratulate with you, nice catch. Now have
a look at the itemIndexes: they’re position based and not bound to the Layers any way; well, there
are a couple of desaparecidos in that family too.

What the heck is going on?


I’ll tell you: when creating a Layer Set, Photoshop internally
stores information for both the Group’s start-point and end-
point. This eats one extra id (and as a consequence, apparently,
an itemIndex as well). Of course, the Layers palette does not
expose this “closing door” to you, but the Layer’s properties
we’ve looked at in the last few pages are hints of what happens
behind the scenes – it must be a convenient way to store the
layers stack description.
When dealing with Layers ActionManager code, we’ll meet
these hidden items again.
Now, the DOM way ArtLayers and LayerSets work requires
you to walk in and out each Group, and keep track of all the
Climbing the DOM 82

layers “dependencies”, i.e., the parent-child hierarchy. In the Layer Spring Cleaning example we did
precisely that: back then, we were helped by the rigid scheme of named Groups. It’s time to try
writing some general purpose code that walks the Layers DOM no matter their configuration.

If you have a sharp eye, you might have noticed that itemIndex provides you with a flat
representation of the Layers and Layer Sets stack. It would be much easier to loop through a
such a Collection by itemIndex – careless of nested Groups. Alas, this is not possible strictly
within the DOM, ActionManager is to the rescue.

The following script builds a JSON-like representation of the Document’s Layers stack, and it does
so using recursion:

1 var layersJSON = logLayer(app.activeDocument.layers);


2 $.writeln(layersJSON.toSource());
3
4 function logLayer(collection) {
5 var arr = [],
6 len = collection.length;
7 if (!len) { return }
8 for (var i = 0; i < len; i++) {
9 arr.push(buildObj(collection[i]));
10 }
11 return arr;
12 }
13
14 function buildObj(lay) {
15 var obj = {
16 "name": lay.name,
17 "type": lay.typename
18 // you can add whatever you want here, e.g.
19 // "blend mode": lay.blendMode
20 // "visible": lay.visible
21 // etc. etc.
22 }
23 if (lay.isBackgroundLayer) {
24 obj.isBackground = true;
25 } else { // No need to have opacity for Background layer
26 obj.opacity = lay.opacity;
27 }
28 // Recursion!
29 if (lay.typename == "LayerSet") {
30 obj.content = logLayer(lay.layers);
Climbing the DOM 83

31 }
32 return obj;
33 }

The function logLayer() on line 4 returns an array of objects, one for each Layer in the Collection.
That object is built in a separate buildObj() function, which collects some properties: you can add
basically what you want there, I’ve used just a few of them for demonstration purposes). The object
is then returned.
The key point is that buildObj() tests whether the Layer is either an ArtLayer or a LayerSet using
the typename property: in case it’s a Group we need to dig deeper, so the layer’s own layers collection
is recursively passed to the buildObj() function (line 30). Recursion is a powerful technique that
comes handy in this case: in essence, it means that a function, here buildObj(), can call itself. Have
a look at the output for the test file we’ve been using so far:

[{
name: "Layer 5",
type: "ArtLayer",
opacity: 100
}, {
name: "Group 1",
type: "LayerSet",
opacity: 100,
content: [{
name: "Layer 4",
type: "ArtLayer",
opacity: 100
}, {
name: "Group 2",
type: "LayerSet",
opacity: 100,
content: [{
name: "Layer 3",
type: "ArtLayer",
opacity: 100
}]
}, {
name: "Layer 2",
type: "ArtLayer",
opacity: 100
}]
}, {
name: "Layer 1",
Climbing the DOM 84

type: "ArtLayer",
opacity: 100
}, {
name: "Background",
type: "ArtLayer",
isBackground: true
}]

If you have troubles wrapping your head around recursion, let me track the program flow for you.
The logLayers() function is called, passing the app.activeDocument.layers Collection as the input
parameter, and starts with layers[0] (aka Layer 5). When it’s time to call buildObj(), logLayers()
pauses and waits for the buildObj() function to process Layer 5. When buildObj() is done collecting
the name, type and opacity props, it returns the object so that logLayers() is able to go on with its
own business: it puts the object in the array, and it processes the next item in the loop.
Which is Group 1. Like before, logLayers() passes it to buildObj(), then pauses, and waits for the
returned object. Now buildObj() faces a LayerSet: it collects the props, then (recursion) pauses and
calls itself, passing Group 1’s own Layers Collection as the input parameter. This second instance of
buildObj() doesn’t know, nor care, about the first buildObj() instance. It does its job, collects stuff,
and then pauses and calls a third instance of buildObj() to reach the Group 2 content.
When the Group 2 object is collected, buildObj() #3 returns it to buildObj() #2; which un-pauses,
and in turn returns its own object to buildObj() #1; which un-pauses, and in turn returns its object
to logLayers() – which wakes up from the long nap and is quite happy to finally have something
to feed the array with. That’s recursion.
Few things to mention. First, the function that is the subject of the recursion must have an exit point
(in our case, the returned object – in other circumstances, a return statement when there’s nothing
more to do), otherwise it keeps running endlessly. Second, recursion may have performance issues
if the stack you’re processing is awfully deep/nested; for that, ActionManager is a better choice –
yet, it requires… ActionManager knowledge, which isn’t trivial. Check out this section of Chapter 6,
where I’ll show you ways to traverse Layers ActionManager style. Lastly, the .toSource() method
(line 2), is one of the many ExtendScript exclusive features. It returns a literal version of the object:
particularly useful when logging messages in the Console.

4.5 Preferences
Photoshop, as you know, allows you a great deal of customization. Some of the Preferences dialog’s
content is available to you via Scripting as well.
Climbing the DOM 85

Since Preferences is one of the app’s Collection, you can directly modify its read/write properties,
like:

app.preferences.rulerUnits = Units.PIXELS;
app.preferences.showToolTips = true;
app.preferences.exportClipboard = false;
// etc.

As I wrote before, it’s a good practice, when you need to modify the user’s Preferences, to restore
the original ones when you’re done. If you feel like there’s something that can go really wrong, use
a try/catch block.

try {
var oldHistoryPref = app.preferences.nonLinearHistory;
app.preferences.nonLinearHistory = true;
// ... and everything else you need your script to do
} catch(e) {
// If something goes wrong, automatically open the browser
// and ask StackOverflow :-)
var url = "http://stackoverflow.com/search?q=" + e.message;
var shortcut = new File(Folder.temp + "/shortcut.url");
shortcut.open('w');
shortcut.writeln('[InternetShortcut]');
shortcut.writeln('URL=' + encodeURI(url));
Climbing the DOM 86

shortcut.writeln();
shortcut.close();
shortcut.execute();
$.sleep(4000); // let's not remove the file too early
shortcut.remove();
} finally {
// The storm is over: restore the Preferences
app.preferences.nonLinearHistory = oldHistoryPref;
}

In the above example, the finally block is executed no matter whether an error is thrown in the
try block or not, so it’s a good place for the preferences restore to be. Even if the catch block is
semi-serious (the error message is used to query Stack Overflow automatically⁹), it shows an actual
way to open a website in the default browser via Scripting: it’s a matter of temporarily create a new
file with the .url extension and execute it.
The JavaScript Reference has extensive documentation on the Preferences collection, even though
not everything can be gotten or set via DOM. It starts sounding like an old refrain, but when the
DOM isn’t enough, ActionManager is the way to go, even with Preferences.
⁹If you’ve never heard about Stack Overflow, it’s a massive community where programmers help each other and enjoy being as a pundit
as they can – for a good cause.
5. The ExtendScript Domain
5.1 Old but Gold
It’s worth quoting one more time the official words by Adobe on his Scripting language:

”[…] an extended form of JavaScript used by several Adobe applications, including


Photoshop, Illustrator, and InDesign. ExtendScript implements the JavaScript language
according to the ECMA-262 specification. [It] supports the 3rd Edition of the ECMA-262
Standard, including its notational and lexical conventions, types, objects, expressions, and
statements. ExtendScript also implements the E4X ECMA-357 specification, which defines
access to data in XML format.”

To your programmer’s ears the above means that ExtendScript complies to the same standard than
JavaScript does, but a pretty old one: we’re still in 2009 here, so the JS Precambrian era¹. As I’ve
mentioned in previous Chapters, you can’t use ES5 features such as Array.indexOf(), no JSON object,
not to mention fancier ES6 or newer features and syntax - forget about them.

The developers community has been waiting years for


an ExtendScript engine upgrade, so much so that we
basically gave up to the idea of a language revamp.
Catching us by surprise, in 2018 Adobe announced a
brand new development platform for Adobe XD plugins
called UXP (Unified Extensibility Platform), based on
a Modern JavaScript virtual machine among the rest.
Reading between the lines, the roadmap suggests that,
in the long run, UXP may land in major applications as
well.

Please note that the old JS version ExtendScript complies to, also reflects in its general
reluctance on being minified, compressed, or obfuscated with traditional JavaScript tools. A
few years ago I did test some JS libraries I needed, such as ES5-Shim and CryptoJS, and I got
discouraging results.
First, some of the minified versions provided by the authors didn’t work at all out of the box.
Second, the available tools (back then I tested Google Closure and UglifyJS) did produce
mixed results depending on their settings: sometimes the code was parsed just fine but was
functionally flawed, sometimes it couldn’t be parsed at all, and fired obscure errors.
¹Now we’re used to much more frustrating scenarios.
The ExtendScript Domain 88

Strictly speaking, ExtendScript is not a JavaScript evolution, the same way humans are not evolved
chimpanzees: science tells us that both we and the chimps share a common ancestor, which was
probably equally different from either your next-door neighbors and Bonobos. JavaScript and
ExtendScript share the same (old, v3) ECMA-262 Standard: from which both have evolved, JS
with recent ES6/ES7 features and JSX (incorporating new Standards such as E4X ECMA-357 and
proprietary features).
This is precisely what makes ExtendScript remarkably versatile – and please note I’m talking about
the core language, which is shared (with different implementations) among the scriptable Adobe
applications. This Chapter covers all the peculiar JSX main features: I won’t list every single detail
of them all, but you can look at the “JavaScript Tools Guide” (JS Tools Guide) as a reference. Let me
point out the ones I find worth mentioning, with practical examples of use.

5.2 The Dollar object


As opposed to diamonds for girls, as someone once sung, the dollar is going to be your best friend.
It’s globally available (puns could go on endlessly, but they won’t) and provides you with many
crucial properties and methods – see also the JS Tools Guide p. 216-220.

//
// === PROPERTIES ===
//

// Debug level (read/write)


// 0: No debugging
// 1: Break on runtime errors
// 2: Full debug mode
$.level;
// String. The current File Name: works only if it's been saved once
// Won't work in the context of HTML Panels
$.fileName;
// Object. The Global namespace.
$.global;
// String. The current locale (e.g. "en_US"). It's Read-Write, so you can
// change the locale of your scripts on the fly to test different languages
$.locale;
// String. In my machine, " Macintosh OS 10.12.0/64"
$.os;
// The current Stack trace
$.stack;
// The line where the $.line itself is found
$.line;
The ExtendScript Domain 89

//
// === METHODS ===
//

// Write a String, without or with a linefeed


$.write("Hello World!"); // no LF
$.writeln("Ciao Mondo!"); // with LF

// Evaluates the ExtendScript code from a file that loads from a path,
// provided as a string, and returns the result of the evaluation
$.evalFile("~/Desktop/TEMP/foobarbazprr.jsx");

// Pauses for n milliseconds. Mind you: not equivalent to JavaScript setTimeout()


$.sleep(4000);

// Runs the garbage collection


$.gc();

// Set and Get Environment Vars


// Useful when you need, say, PS and Bridge to share variables
$.setenv("HotFolder", "~/Desktop/TEMP");
$.getenv("HotFolder"); // "~/Desktop/TEMP"

// Set a breakpoint
$.bp();
debugger; // can be used as well within the code

Please have a look at the documentation for extensive coverage of all the properties and methods
available.

5.3 The Reflection Interface


Right after the dollar object, in my ranking, comes the fact that each ExtendScript object has a
reflect property, which returns a reflection object. Which in turn has two important properties:
properties and methods (both ReflectionInfo objects), eventually listing, mirror mirror on the wall,
all properties and methods of the original object.
Lost? Very likely, and understandably; see also JS Tools Guide p. 221-223, and the following example.
The ExtendScript Domain 90

var lay = app.activeDocument.activeLayer;


var mirror = lay.reflect; // a Reflection object
var props = mirror.properties; // a ReflectionInfo object
var meths = mirror.methods; // a ReflectionInfo object
props.length; // 28
meths.length; // 70

// Properties: I'm showing the first one, as an example


props[0]; // fillOpacity
props['fillOpacity'] // very same thing
props[0].name // fillOpacity
props[0].type // readwrite
props[0].dataType // number

// In theory, these properties are also available, but in Photoshop they seem
// to be all undefined; might work in InDesign or Illustrator, though
// props[0].max
// props[0].min
// props[0].description
// props[0].help
// props[0].defaultValue

// Methods: I'm showing the first one, as an example


meths[0].name // applyStyle
meths[0].type // method
meths[0].dataType // any
meths.arguments // any

Looking for a single property or method in the ReflectionInfo object doesn’t make very much sense –
but if you encapsulate the reflection interface in helper functions, the utility is immediately evident.

1 function reflectProps(obj) {
2 var props = obj.reflect.properties;
3 for (var i = 0, len = props.length; i < len; i++) {
4 try {
5 $.writeln(props[i].name + ' = ' + obj[props[i].name]);
6 } catch (e) {}
7 }
8 }
9
10 function reflectMeths(obj) {
11 var meths = obj.reflect.methods;
12 for (var i = 0, len = meths.length; i < len; i++) {
The ExtendScript Domain 91

13 try {
14 $.writeln(meths[i].name + '();');
15 } catch (e) {}
16 }
17 }

That you may use this way:

var lay = app.activeDocument.activeLayer;


reflectProps(lay);

// fillOpacity = 80
// layerMaskDensity = 100
// layerMaskFeather = 0
// vectorMaskDensity = 100
// vectorMaskFeather = 0
// filterMaskDensity = 100
// filterMaskFeather = 0
// grouped = false
// isBackgroundLayer = false
// pixelsLocked = false
// positionLocked = false
// transparentPixelsLocked = false
// kind = LayerKind.NORMAL
// typename = ArtLayer
// name = Layer 1
// allLocked = false
// blendMode = BlendMode.NORMAL
// linkedLayers =
// opacity = 100
// visible = true
// id =3
// itemIndex =2
// bounds = 0 px,0 px,562 px,225 px
// boundsNoEffects = 0 px,0 px,562 px,225 px
// xmpMetadata = [XMP Metadata]

When you’re bored, try running the reflection functions on random Photoshop stuff, and you might
find undocumented props and meths: for instance, the itemIndex I’ve extensively covered in Chapter
4 isn’t found in the JavaScript reference.
Hint: look inside the app and $ object.
The ExtendScript Domain 92

5.4 Preprocessor Directives


In the JavaScript world, there’s much interest upon module loaders, like RequireJS or AMD. I’m
not sure whether Preprocessor Directives would fit as loaders, yet they let you include code from
separate files, allowing a great deal of modularization and separation of concerns (JS Tools Guide p.
233-235).

Let’s assume you have a folder structure like this one, where main.jsx might
need to access code from other sources. Please note that some files have a
.jsxinc extension, which is traditionally recommended for included files
– not at all mandatory, it’s just a convention: their content is plain and
standard ExtendScript.
In case you need just globals.jsx you can put the following directive at the
beginning of your code (please note the number sign/hash):

#include "globals.jsx"

The quotes are optional but required when there are spaces in the filename or path. Speaking of the
path, the above syntax assumes that the file is in the same folder as main.jsx, while the following
lines deal with libs/PSUtils.jsxinc too.

#include "globals.jsx"
#include "libs/PSUtils.jsxinc"

You can also specify paths, to be used by subsequent include directives to look for files.

#includepath "GUI"
#include "assets.jsxinc"
#include "dialog.jsx"

A couple of things to notice. First, paths that start with / (forward slash) are considered absolute.
Second, developers mainly use include directives at the very beginning of the file, even if they seem
to work anywhere. For conditional content loading, $.evalFile() might be an easier alternative.
Third, due to a Photoshop… “characteristic”, #include in the context of HTML Panels’ JSX files
won’t work. Lastly, if you happen to run into a slightly different syntax such as this one:

//@include "globals.jsx"
//@include "libs/PSUtils.jsxinc"

It’s there for backward compatibility (CS or CS2), and/or to escape code linting errors.
In addition to code inclusion, you can use directives to specify the script’s target, e.g. the app that
should run it (if the user double clicks it and ESTK is the predefined app that deals with .jsx files,
or via the File.execute() method).
The ExtendScript Domain 93

#target photoshop
#target photoshop-110
#target photoshop-110.032

The first defaults to the latest installed version, while you can target a specific version (see following
lines²). The available versions can be found in the ESTK dropdown menu, or running this snippet
that lists all the installed ones:

1 var apps = BridgeTalk.getTargets("");


2 var ps = [];
3 for (var i = 0, len = apps.length; i < len; i++) {
4 if (apps[i].indexOf('photoshop') == 0) { ps.push(apps[i]) }
5 }
6 $.writeln(ps.join('\n'));
7
8 // photoshop-100.032
9 // photoshop-110.032
10 // photoshop-60.032
11 // photoshop-70.032
12 // photoshop-80.032
13 // photoshop-90.032

5.5 Filesystem
Way before Node.js was even given a name, ExtendScript could open, read, write and delete files
like a boss³. In fact, given the right user’s permissions, nothing prevents a malicious developer to
write JSX code that does evil on somebody’s machine (like blowing up a Folder, as an example of
something you shouldn’t try). For a complete reference, see JS Tools Guide p. 39-61.

Please note that all the Filesystem methods that I’ll be describing are not undoable. If you’ve
trashed a client’s folder via scripting, it’s gone forever: no way to get it back from the Trash.

The way Scripting deals with File and Folder objects is the same no matter the platform (Windows
or macOS). Both require a path, that you’ll use to create or reference them, and that can be formed
using the forward slash /.

²The .032 (on Mac) should be .064 and it’s a relic from 32 bits architecture.
³Fun and true fact: when I first heard of Node.js I genuinely (and naively) thought: “So what? We had JS outside of the Browser since the
Middle Ages here.”
The ExtendScript Domain 94

// Valid Mac paths


'~/Desktop/TEMP/1.jsx'
'/Users/davidebarranca/Desktop/TEMP/1.jsx'
'~/Desktop/TEMP/A spaced filename.jsx'
// External drive
'/Volumes/PassportHD/BLABLA/data.json'

// Valid Windows paths


'/c/Users/davidebarranca/Desktop/TEMP/1.jsx'
'/c/Users/davidebarranca/Desktop/TEMP/A spaced filename.jsx'
// External drive
'/t/BLABLA/data.json'

ExtendScript follows the Uniform Resource Identifier (URI) scheme – some characters can be
replaced with escape sequences representing their UTF-8 encoding.
The globally available encodeURI() and decodeURI() functions let you transform strings:

encodeURI('~/Desktop/A spaced filename.jsx');


// ~/Desktop/A%20spaced%20filename.jsx
decodeURI('~/Desktop/A%20spaced%20filename.jsx');
// ~/Desktop/A spaced filename.jsx

Both File and Folder have a distinct set of Class properties and functions, and Instance properties
and functions (if you need a reminder on class versus instance, see here).

Folders

You create a Folder instance with the new operator and passing a valid path.

1 var fol = new Folder('~/Desktop/MY FOLDER');

Mind you: the '∼/Desktop/MY FOLDER' folder has not really been created in the FileSystem: the
variable holds a Folder object with a valid reference to a Filesystem folder, that may exist or not.
As a Folder Instance, fol has interesting properties and methods – like before, I’m listing here the
most relevant in my opinion, have a look at the JS Reference for extra information.
The ExtendScript Domain 95

//
// === INSTANCE PROPERTIES ===
//

// Does the Folder exist?


fol.exists; // true or false

// When it comes to the name, there are several variations at your disposal
// Name, URI encoded
fol.name; // MY%20FOLDER
// platform-specific name as a full path name
fol.fsName; // /Users/davidebarranca/Desktop/MY FOLDER
// The full path name, URI decoded
fol.fullName; // ~/Desktop/MY FOLDER
// Localized name of the referenced folder, without the path.
fol.displayName; // MY FOLDER
// The full path name, URI encoded
fol.absoluteURI; // ~/Desktop/MY%20FOLDER
// The path name relative to current folder, URI encoded
fol.relativeURI; // /Users/davidebarranca/Desktop/MY%20FOLDER

//
// === INSTANCE METHODS ===
//

// Actually creates the Folder


fol.create(); // returns true if creation is successful

// Deletes the Folder *only* if it's empty


fol.remove(); // returns true if it is successful

// Renames the Folder name (doesn't change the path)


fol.rename('MY NEW FOLDER');

// Changes the path of the referenced folder


fol.changePath('~'); // the fol variable now references a different path

// Opens the System's Folder Selection dialog, preselecting the `fol` folder
var userSelectedFolder = fol.selectDlg(); // returns a Folder object

// Get Files and Folders from the folder


fol.getFiles(); // returns an Array of File and Folder objects
The ExtendScript Domain 96

The latest Instance method requires some extra coverage because it’s extensively used when batch
processing. As it’s been written, it returns no matter what fol contains – all kind of Files, and, if
present, child Folders too. It accepts a parameter that can be either a String:

// Wildcards can be used


// * matches zero or more characters
// ? matches exactly one character
fol.getFiles('*.jpg'); // Restricts the output to JPG files
fol.getFiles('???.*'); // Files 3 chars long, no matter what extension

… or a Regular Expression:

fol.getFiles(/\.(jpg|tif|psd)$/i); // Returns just files with those extensions

… or a function:

// Selects only the Folders


fol.getFiles(function(stuff){
return stuff instanceof Folder;
});

// Selects only the Files


fol.getFiles(function(stuff){
return stuff instanceof File;
});

// An example of a function excluding Mac's '.DS_Store' files


fol.getFiles(function(stuff){
if(stuff instanceof File) {
if(stuff.name === '.DS_Store') { return false; }
return true;
};
});

So far you’ve seen Folder Instance’s props and meths: the Folder Class too, has properties which are
mostly tokens to system locations (in both platforms, Mac and Win) that you may want to access
in your scripts. For instance, the Desktop folder would be hard to find if you don’t know the user’s
name, and even then, it’ll be different depending on the OS and OS version.
I’ve compiled a table with the corresponding output for macOS Sierra and Windows 10 on my
computer.
The ExtendScript Domain 97

Folder tokens
Folder.appData
Mac /Library/Application Support
Win C:\ProgramData
Folder.appPackage
Mac /Applications/Adobe Photoshop CC 2015.5/Adobe Photoshop CC
2015.5.app
Win C:\Program Files\Adobe\Adobe Photoshop CC 2015.5
Folder.commonFiles
Mac /Library/Application Support
Win C:\Program Files\Common Files
Folder.desktop
Mac /Users/davidebarranca/Desktop
Win C:\Users\davidebarranca\Desktop
Folder.myDocuments
Mac /Users/davidebarranca/Documents
Win C:\Users\davidebarranca\Documents
Folder.startup
Mac /Applications/Adobe Photoshop CC 2015.5/Adobe Photoshop CC
2015.5.app/Contents/MacOS
Win C:\Program Files\Adobe\Adobe Photoshop CC 2015.5
Folder.system
Mac /System
Win C:\WINDOWS\SYSTEM32
Folder.temp
Mac /private/var/folders/mm/vhjy49t53tjfs9th4221kkcc0000gn/T/TemporaryItems
Win C:\Users\DAVIDE∼1\AppData\Local\Temp
Folder.trash
Mac /Users/davidebarranca/.Trash
Win fires an error, because Recycle Bin is a DB
Folder.userData
Mac /Users/davidebarranca/Library/Application Support
Win C:\Users\davidebarranca\AppData\Roaming

Last useful Folder Class property is Folder.fs (filesystem), which returns either "Macintosh" or
"Windows".

When it comes to Class methods, both Folder.encode() and Folder.decode() are similar to the
globally available encodeURI() and decodeURI(). Another quasi-clone is Folder.selectDialog(),
that opens the same system dialog you’ve already seen, but doesn’t preselect the folder.
The ExtendScript Domain 98

// Class method:
var selected = Folder.selectDialog(); // no pre-selected Folder
// Instance method:
var fol = new Folder('~/Desktop/MY FOLDER');
var selected = fol.selectDlg(); // pre-selects '~/Desktop/MY FOLDER'

Files

Similarly to Folders, File objects are built with the new operator and passing a valid path.

var f = new File('~/Desktop/MY FOLDER/temp.jpg');

Again: f doesn’t care whether the 'temp.jpg' file exists or not in the Filesystem: you have created
a valid ExtendScript File object, that references (points to) a file. Whether the slot where the file
should be is empty or not, is an entirely different question.

You may deal with Files when scripting a batch process (e.g. save different versions for each
JPG within a folder): in this case the File is just the kind of image documents that you would
open or save in Photoshop yourself; but that’s the tip of the iceberg.
Filesystem access lets you create, read and write all kind of text files, and binary too: you
might want to read remote JSX code, or XML data, store JSON presets locally, or initialization
values, write Action .atn files, whatever you need.

The File Class list of props and methods is quite short:

// Class Property
File.fs; // read only, either "Macintosh"` or `"Windows"
// Class Methods for URI strings
File.encode(); // very much like Folder.encode();
File.decode(); // very much like Folder.decode();

Two particular Class methods that you’ll use perhaps more frequently are:
The ExtendScript Domain 99

/**
* Opens the System dialog to select one or more Files
* @param {String} prompt A short text that is displayed in the
* dialog [optional]
* @param {Multiple} filter A filter to limit the displayed extensions
* Win: "*.jpg", "*.*", or ["*.jpg", "*.tif"]
* Mac: a function taking a File object as a
* param and returns either true or false
* [optional]
* @param {Boolean} multiSel Allow the selection of more than one file
* [optional]
* @return {File} A File object, or an Array of File objects
* (or null if the user cancels)
*/
File.openDialog(prompt, filter, multiSelect);

// Win
File.openDialog("Select your input", "*.jpg", true);
// Mac
File.openDialog("Select your input", function(f){
return f.fsName.match(/\.(jpg|tif|psd)$/i);
}, true);

/**
* Opens the System dialog to save a File
* @param {String} prompt A short text that is displayed in the
* dialog [optional]
* @param {String} filter A filter to limit the displayed extensions:
* "*.jpg", "*.*", or ["*.jpg", "*.tif"]
* [optional, Windows only]
* @return {File} A File object for the selected location
* (or null if the user cancels)
*/
File.saveDialog(prompt, filter, multiSelect);

// Win
File.saveDialog("Select your output", "*.jpg");
// Mac
File.saveDialog("Select your output");

In both cases, a File object is the returned value (or, if openDialog() multiSelect is true, an Array of
File objects). Please note that both openDialog() and saveDialog() won’t open in Photoshop, nor
save to disk, any File. The dialogs’ goal is to let the user select File locations and hand them to you,
The ExtendScript Domain 100

so that the script will be able to open from, and save stuff to, disk. But eventually it’ll be your duty
to write the related code to perform those tasks.
File Instance properties and methods are more numerous; below a small selection of them based on
the likelihood of use, in my opinion.

var f = new File('~/Desktop/temp.txt');

//
// === INSTANCE PROPERTIES ===
//

f.exists(); // whether the File actually exists on disk or not (true, false)

// When it comes to the name, there are several variations at your disposal
// Name, URI encoded
f.name; // temp.txt
// platform-specific name as a full path name
f.fsName; // /Users/davidebarranca/Desktop/temp.txt
// The full path name, URI decoded
f.fullName; // ~/Desktop/temp.txt
// Localized name of the referenced filder, without the path.
f.displayName; // temp.txt
// The full path name, URI encoded
f.absoluteURI; // ~/Desktop/temp.txt
// The path name relative to current filder, URI encoded
f.relativeURI; // /Users/davidebarranca/Desktop/temp.txt

// The parent Folder (mind you: the returned value is a Folder object)
f.parent; // ~/Desktop
// The file Path without the File name (mind you: it's just a String)
f.path; // ~/Desktop

// Read-Write: get or set the hidden property (can actually hide the file)
f.hidden; // false
// Read-Write: get or set the readonly property
f.readonly; // false
// Creation date (or null, if the File doesn't exist)
f.created; // Sat Oct 15 2016 21:49:40 GMT+0200
// Last modification date (or null)
f.modified; // Sun Oct 16 2016 14:08:46 GMT+0200

// Mostly related to read/write operations on Files:


The ExtendScript Domain 101

// Filesize in bytes
f.length; // 4880
// Linefeed, either "macintosh", "windows", "unix"
f.lineFeed = ($.os.match(/macintosh/i)) ? "macintosh" : "windows";
// Read-Write, get or sets the file encoding among the available ones
// commonly "UTF8", "UTF16", "BINARY"
f.encoding = "UTF8";
// true if the file is either not open, or when
// a read attempt pushed the position at the EOF (end of file)
f.eof; // true (because the file's not currently open!)
// Last filesystem error, if present (e.g. "Read error", "Permission denied")
f.error; // ""

File Instance methods are the core of Filesystem operations. Let’s divide a selection of them (as usual,
everything’s in the JS Reference) into functional groups – starting with dialogs.

//
// === INSTANCE METHODS ===
//

// Clone of the Class Method .openDialog(), but with folder pre-selection


// (the dialog opens in the file parent's Folder)
// Win
f.openDialog("Select your input", "*.jpg", true);
// Mac
f.openDialog("Select your input", function(f){
return f.fsName.match(/\.(jpg|tif|psd)$/i);
}, true);

// Clone of the Class Method .saveDialog(), but with folder pre-selection


// (the dialog opens in the file parent's Folder)
// Win
var output = f.saveDialog("Select your output", "*.jpg");
// Mac
var output = f.saveDialog("Select your output");

Three methods deal with the kind of File operation you’d do yourself in Finder or File Explorer, like
create an alias, duplicate, rename, delete:
The ExtendScript Domain 102

// All methods return true if successful


// Create an Alias of the file
f.createAlias('~/Documents/aliases/'); // accepts a valid path string
// Copies (duplicate) the file into a different location (Folder)
f.copy('~/Desktop/temp2/file.txt'); // feed it with a valid path
// for your platform
// Renames the file
f.rename('temporary.txt'); // needs the filename only, no path
// Obliterate the file from the filesystem
f.remove(); // no way to get it back, so be careful!

One method opens the file with the system’s default application: for instance, it will use Preview or
Adobe Acrobat Reader to open a .pdf, iTunes or your media player to open a .mp3, the Terminal or
Command Prompt to run a shell script, etc.

f.execute(); // returns true after the application's been launched

And finally here are the methods that you’re going to use to read and write files. First thing ever
that you need to do (even, or better especially when, the file doesn’t exist), is to open it.

// Open the file for reading, writing, editing, appending. Mac only: it accepts
// two optional params as 4 chars strings: type (e.g. "TEXT"), and creator.
f.open('r'); // reading
f.open('w'); // writing
f.open('e'); // editing
f.open('a'); // appending

When the file is open (and only then), you can perform actions, such as reading its content.

// Reads the content of the file: if no parameter is provided, it reads it all


// otherwise it reads only the specified number of chars. Returns the string.
var content = f.read();
var content = f.read(10);
// Reads a single character. Special ones are 'CR' (carriage return), 'LF'
// (line feed) and pairs such as 'CRLF' and 'LFCR'. If the file is encoded,
// multiple chars must be read and then combined to form Unicode characters.
// Returns the 1 char string.
var content = f.readch();
// Reads an entire line. 'CR', 'LF', 'CRLF', 'LFCR' are recognized.
// Returns the string.
var content = f.readln();

Similarly, you have methods for writing content to the file.


The ExtendScript Domain 103

// Write content
f.write("Some text", ", and optionally some more.");
// Same as above, but adding a linefeed at the end
f.writeln("Some text");

// Utility functions:
// Get the current position as a byte offset from the start of the file
f.tell();
// Move to a new position as a byte offset from the start of the file
// The second parameter is:
// 0 (absolute position, where first position has index = 0)
// 1 (relative to the current position)
// 2 (relative to the end of the file, backwards)
f.seek(10, 1);

Time for some examples on File I/O, otherwise all the above will flush out from your brain next
time you blow your nose.
Let’s pretend that your script needs to write trial information on a hidden file, like a counter
(a number, say you’re allowing 10 runs), and a user name: it’s a plausible scenario, even if the
implementation is really simplified for demonstration purposes. So the text’s going to be like:

10
Davide Barranca

In the first script run there’s no hidden file: you need to create it.

1 // First you need to create a folder, in this case a subfolder:


2 var fol = new Folder(Folder.userData + '/secret');
3 fol.exists; // false
4 fol.create();
5 fol.exists; // true
6 // Now you need to create a file:
7 var f = new File(Folder.userData + "/secret/invisible.ini");
8 f.exists; // false
9 f.open('w');
10 // Now the file exists!
11 f.exists; // true
12 f.writeln("10"); // in our example, the available script runs
13 f.writeln("Davide Barranca"); // in our example, the user name
14 f.close();
15 // Hide it
16 f.hidden = true;
The ExtendScript Domain 104

And that’s it: please note that you can create a Folder with its create() instance method, but there’s
nothing equivalent for Files, so the trick is to open it and write something. Always remember to
close it when you’re done.

In case you’re a bit confused: yes, you’re opening a file that doesn’t exist yet. In the Scripting
context, “to open” has a different meaning compared to an actual application, such as
Photoshop. What you’ve created first (the f File) is a valid instance of the File Class, which
doesn’t happen to point to an actual existing file, yet. By the time you f.open('w'), the file
is created on disk.

The next time the user runs the script, the script itself needs to check the hidden .ini file and see
whether it’s allowed to run – and subtract 1 to the counter.

1 var f = new File(Folder.userData + "/secret/invisible.ini");


2 f.open('e');
3 f.length; // 19. Don't believe it? Count it yourself:
4
5 // (1)(0)(LF)
6 // (D)(a)(v)(i)(d)(e)( )(B)(a)(r)(r)(a)(n)(c)(a)(LF)
7
8 f.tell(); // 0 (the current pointer position, ready to read char 1)
9 var trialRuns = f.readln(); // read the first line
10 // the pointer is now at a different position
11 f.tell(); // 4
12 var username = f.readln();
13 f.tell(); // 19
14 f.eof; // true, we've reached the end of file
15 var whatelse = f.readln(); // undefined, can't read anymore
16 f.error // Read past EOF, the last recorded I/O Error

We’ve opened the file with the 'e' flag (which stands for edit) because we need to read and write
it.

Be careful: by the time you’ve called it, open('w') has already overwritten the file content,
i.e. wiped out everything. If you need to read a file, use the 'r' flag; conversely, if you need
to append data, use 'a'.

The file length is 19, for you need to take into account also linefeeds: at the file creation, we’ve used
writeln(), which always appends them. To check it, move the pointer to position 2 (right after the 0),
and read a single character using the readch() method, in conjunction with the global encodeURI()
function. The result is the corresponding to LF (linefeed). An encoding reference can be found here.
The ExtendScript Domain 105

f.seek(2);
encodeURI(f.readch()) // %0A

The following illustration shows char and pointer positions throughout the file content.

The trialRuns variable is going to contain the first read line (the string 10). Now the pointer is at
position 4, ready to read another line, and save the returned value into the username var. The pointer
has now reached position 19, which is the End Of File: the eof property is true.
If you read beyond the available file content, readln() returns undefined – and this also stores into
the error property the "Read past EOF" message (the last I/O Error occurred).
Now that the user and counter are known, you can do whatever you need with this information:
likely, alert the user that s/he has 10 remaining script runs in the trial, and more importantly lower
the count and write that to the file:

17 trialRuns = Number(trialRuns); // casting trialRuns as a Number


18 if (trialRuns > 0) {
19 alert("Trial mode\nOnly " + trialRuns + " runs left.");
20 trialRuns--; // subtract 1 to counter
21 f.seek(0); // move the pointer back to 0
22 f.writeln(trialRuns + " "); // adding a white space!
23 } else {
24 alert("Trial expired!\nPlease buy the licensed version");
25 }
26 f.close();

Please note that the pointer is reset to the position 0, and (line 21) a whitespace has been added to fill
the slots that were occupied by the 10 string – otherwise the file would have contained 9 with two
linefeed chars. Usually it’s more straightforward, and possibly safer, to rewrite the whole content of
the file from scratch; yet I wanted to draw your attention to the way file writing works.
Mind the file encoding, though! If you need to use Unicode chars, set "UTF8".
The ExtendScript Domain 106

//...
f.encoding = "UTF8";
f.open('w');
f.writeln("Tschüß"); // Tschüß!

We’ll use File I/O throughout the whole course, so don’t worry if you still have open questions.

5.6 Notification Dialogs


ExtendScript has its GUI – Graphical User Interface – system, called ScriptUI (see Chapter 7), yet
you can easily launch three different modal popup dialogs to communicate with the user and/or ask
for user’s input (JS Tools Guide p. 227-229).

They are triggered using methods of the application object; so they’re globally available, and
usually called without prefixing app. These dialogs are modal, that is to say: they steal the
application focus, and you’re not allowed to interact with the rest of the Photoshop interface
(if you try, it beeps). Conversely, HTML Panels are usually modeless: you can keep them
open while doing your business with other panels, tools and menus.
The aspect, and sometimes minor features, of these dialogs might be different from Mac to
Windows, so I’ll report both syntaxes. Usually, developers don’t bother.

The first and simplest dialog is the Alert, which pops up a window with a string. The text can be
multiline inserting \n, and the title is set differently on both platforms; the icon can be chosen among
the default Alert and default Error (which in Windows results in two different jingles as well, how
nice). The return value is undefined.

// Mac
// The title should be the second param, but won't work, instead it's everything
// before the first newline. If you omit the second param, though, the
// icon will never change: default is false (Alert icon)
alert("Nice title here\nIt was a dark and stormy night...\nWith an Alert icon",
"", false);

// Windows
// Title param works here
alert("It was a dark and stormy night...\nWith an Alert icon",
"Nice title here", false);

// 99.9% of the times you'll just:


alert("Ugh!\nSomething went wrong, either pray or drink.");
The ExtendScript Domain 107

Alert popups

Next, there’s a Confirmation popup, which allows the user to choose between two options: Yes or
No (which returns respectively true or false). The title works the same as with Alerts (platform
differences), and you’re allowed preselect the choice with the second param⁴.

// Mac, second param when false preselects Yes (default)


var really = confirm("Think about it\nWill you marry me?", false);

// Windows, Title works


var really = confirm("Will you marry me?", false, "Think about it");

Be aware that there’s a Mac-only bug: if you preselect “No” (param is true), and the user
doesn’t click any button, but escapes with the ESC key, the returned value is “Yes”; whereas
it should always return false, as it happens on Windows.

Confirm popups

Lastly, there’s the Prompt dialog, that lets the user inputs a string, that is the returned value of the
dialog. The title is broken on Mac as usual, but you can insert a placeholder text.
⁴One of those cases where the design is dubious: the param is called noAsDefault, which means that if you set it true, it preselects No.
Conversely, if it’s false, it preselects Yes. Counter-intuitive to say the least.
The ExtendScript Domain 108

// Mac
var really = prompt("What's the meaning of life?\n" +
"Feel free to think as long as you need to...", 42);

// Windows
var really = prompt("Feel free to think as long as you need to...", 42,
"What's the meaning of life?");

Prompt popups

5.7 XML
In ExtendScript, as opposed to JavaScript, XML is a first-class citizen. Instead of listing all properties
and methods (JS Tools Guide p. 236-256) let’s use the following, simple XML as our playground; let’s
pretend it is data for a Preset Management system – which it is, as you’ll see in Chapter 8.

<presets>
<preset default="true">
<name>Default</name>
<value>100</value>
</preset>
<preset default="true">
<name>Low</name>
<value>10</value>
</preset>
<preset default="false">
<name>High</name>
<value>400</value>
</preset>
</presets>

To acquire this .xml file and transform it into a proper object, you need to read it and use the XML
object constructor. Let’s assume the two files (.xml and .jsx) are in the same folder: you can get the
folder using $.fileName.
The ExtendScript Domain 109

1 // currentFolder is the Folder's path this very file belongs to


2 var currentFolder = new File($.fileName).path;
3 var xmlFile = new File(currentFolder + '/presets.xml');
4 xmlFile.open('r');
5 var xmlContent = xmlFile.read();
6 xmlFile.close();
7 var xmlObj = new XML(xmlContent); // XML Object

If you need, you can pass the XML as a string, like:

// mind the quotes


var xmlString = "<presets><preset default='true'> /* etc. etc */";
var xmlObj = new XML(xmlContent);

As a third option, an XML object is going to be created if you feed the XML directly to the variable:

var xmlObj = <presets>


<preset default="true">
<name>Default</name>
<value>100</value>
</preset>
// etc. etc.
// you're allowed to start new lines
typeof xmlObj // xml

When the xmlObj has been built, it represents the root element (here, presets). You can then access
child elements, tags and attributes.

// children() and length() methods are documented you know where


xmlObj.children().length(); // 3
// find by index
xmlObj.preset[0]; // returns the xml
// <preset default="true"><name>Default</name> etc.
// tags use the dot operator
xmlObj.preset[0].name; // Default
xmlObj.preset[0].value; // 100
// attributes are accessed with .@
xmlObj.preset[0].@default; // true

You can also list tags, e.g. all the names or all the default arguments, this way:
The ExtendScript Domain 110

xmlObj.preset.name;
// <name>Default</name>
// <name>Low</name>
// <name>High</name>
xmlObj.preset.@default;
// truetruefalse [as xml]

// You can access them by index


xmlObj.preset.name[1]; // same as xmlObj.preset[1].name, i.e. Low

Editing is straightforward too:

// Assign new values


xmlObj.preset[0].name = "New Default";
xmlObj.preset[0].@default = false;
// delete
delete xmlObj.preset[0].@default;
delete xmlObj.preset[1];

// build a new child element and append it


var newChild = <preset default="false">
<name>New child</name>
<value>900</value>
</preset>
// append the new xml object as a child
xmlObj.appendChild(newChild);

// alternatively, you can decide where to put it


// first param is the reference insertion point
xmlObj.insertChildAfter(xmlObj.preset[0], newChild);
// same idea, but before the selected element
xmlObj.insertChildBefore(xmlObj.preset[2], newChild);

There are more XML instance methods available. If you need to operate on more advanced features,
say custom namespaces, have a look at the documentation and you’ll find there all the information
you need. I want to mention here two more things: first, String methods.

xmlObj.preset[0].name.toString(); // Low
xmlObj.preset[1].name.toXMLString(); // <name>Low</name>

And second, how to dynamically build XML objects with JS Objects – short answer: curly braces.
The ExtendScript Domain 111

var newValue = 900/2,


newName = "Better Default"
var newChild = <preset default="false">
<names>{newName}</names>
<value>{newValue}</value>
</preset>
newChild.toString();
// <preset default="false">
// <names>Better Default</names>
// <value>450</value>
// </preset>

5.8 Sockets
Even if with HTML Panels (available in Photoshop since CC) communicating with remote servers
is much easier, ExtendScript implements the Socket object to connect via TCP/IP. It’s a * low-level*,
bidirectional system – that is to say, you can send GET, POST and HEAD requests, or set up a web server
yourself, listening for incoming connections. The documentation (JS Tools Guide p. 193-199) even
shows a rudimental Chat Server example.
Coupled with Filesystem management, Sockets make ExtendScript quite a dangerous language:
nothing prevents you from doing evil… for instance, in the past, I’ve encountered a demo JSX which
purpose was to steal sensitive data from the user disk, over a socket connection. Of course this is not
what ExtendScript should be for.
More frequently, in my own experience, you use Sockets for simpler tasks, such as to communicate
with Analytics services, or get a reliable timestamp in case you need to validate a trial. The following
code gets the number of minutes since Unix epoch (January 1, 1970 00:00:00 UTC) from the free
service currentmillis.

1 var reply = "";


2 // The HTTP Request string
3 var req = "GET http://currentmillis.com/time/minutes-since-unix-epoch.php " +
4 "HTTP/1.1\r\nHost: currentmillis.com\r\nConnection: close\r\n\r\n";
5 conn = new Socket();
6 if (conn.open("www.currentmillis.com:80", "binary")) { // 188.121.45.1
7 conn.write(req);
8 reply = conn.read(999999);
9 $.writeln("Full Reply ---------------\n" + reply);
10 conn.close();
11 $.writeln("Minutes since Unix epoch: " +
12 reply.substr(this.reply.indexOf("\r\n\r\n") + 7, 10));
The ExtendScript Domain 112

13 }
14 /*
15 Full Reply
16 HTTP/1.1 200 OK
17 Date: Thu, 20 Oct 2016 23:05:47 GMT
18 Content-Type: text/html
19 Transfer-Encoding: chunked
20 Connection: close
21 Set-Cookie: cfduid=d572f5f6bb0f56332576e90f35c27d3fe1477004747; expires=Fri, 20-Oc\
22 t-17 23:05:47 GMT; path=/; domain=.currentmillis.com; HttpOnly
23 Access-Control-Allow-Origin: *
24 Cache-Control: public, max-age=1800
25 Vary: Accept-Encoding
26 CF-Cache-Status: EXPIRED
27 Expires: Thu, 20 Oct 2016 23:35:47 GMT
28 Server: cloudflare-nginx
29 CF-RAY: 2f501dd570500e36-MXP
30
31 8
32 24616746
33 0
34 Minutes since Unix epoch: 24616746
35 */

When you deal with sockets you must know your HTTP requests – I’ve found this page incredibly
helpful to understand how to write them properly. Alternatively, you can fetch whatever file on a
server, and inspect the response Header – i.e. send a HEAD request:

1 var reply = "";


2 // The HTTP Request string, use any asset you want (here's a GIF)
3 var req = "HEAD https://www.jmarshall.com/images/blueribbon.gif " +
4 "HTTP/1.1\r\nHost: jmarshall.com\r\nConnection: close\r\n\r\n";
5 conn = new Socket();
6 if (conn.open("www.jmarshall.com:80", "binary")) {
7 conn.write(req);
8 reply = conn.read(999999);
9 $.writeln("Full Reply ---------------\n" + reply);
10 conn.close();
11 $.writeln(reply.substr(this.reply.indexOf("Date:") +6, 29));
12 }
13 /*
14 Full Reply
15 HTTP/1.1 200 OK
The ExtendScript Domain 113

16 Server: nginx/1.10.1
17 Date: Thu, 20 Oct 2016 23:45:56 GMT
18 Content-Type: image/gif
19 Content-Length: 221
20 Connection: close
21 Last-Modified: Wed, 14 May 1997 18:25:17 GMT
22 Accept-Ranges: bytes
23 Vary: Accept-Encoding
24
25
26 Thu, 20 Oct 2016 23:45:56 GMT */

In the JSX samples that come along with ESTK, you can find an EmailWithAttachment.jsx, which
uses Sockets to send an email – have a look at that too.

5.9 Graphic User Interfaces

ExtendScript can display complex GUIs using


its peculiar ScriptUI Class (JS Tools Guide p.
61-165), which is extensively used by Adobe
engineers themselves in many of the scripts
bundled with Photoshop, such as “Contact
Sheet II”. ScriptUI would deserve a book on
its own, and in fact InDesign developer Peter
Kahrel has written ScriptUI for Dummies,
which I strongly recommend you to read⁵.
Yet, ExtendScript GUIs are one of those areas
where the difference between Adobe products
implementations differ the most: InDesign
does dialogs differently from Photoshop; early
versions of Photoshop are different compared
to newer versions, and sometimes the same
Photoshop version has different behaviors de-
pending on the platform (Mac or Windows).
Please refer to Chapter 7 to read my take on
this topic.

5.10 External Object


Quoting the official documentation (JS Tools Guide p. 199-215) directly:
⁵If you make consistent use of ScriptUI for Dummies, please consider to donate to the author and support future updates.
The ExtendScript Domain 114

You can extend the JavaScript DOM for an application by writing a C or C++ shared
library, compiling it for the platform you are using, and loading it into JavaScript as an
ExternalObject object. A shared library is implemented by a DLL in Windows, a bundle
or framework in Mac OS, or a SharedObject in UNIX.

I’m afraid I’m no C developer, so I cannot but inform you about this feature. If you’re inclined, you
can try compiling in XCode or Visual Studio a demonstration library called “BasicExternalObject”
that is found in the ESTK Samples/cpp folder⁶.
The big deal of External Objects is enabling you to call C/C++ functions in the ExtendScript
context. If you have prior experience of HTML Panels, you might have met External Objects when
dispatching custom ExtendScript Events in your JSX (to be listened by the Panel’s JS). The code
looks like this:

1 try {
2 var xLib = new ExternalObject("lib:\PlugPlugExternalObject");
3 } catch (e) { alert(e) }
4 if (xLib) {
5 var eventObj = new CSXSEvent();
6 eventObj.type = "com.davide.customEvent";
7 eventObj.data = "some payload data...";
8 eventObj.dispatch();
9 }

Another use case of ExternalObject is to load the XMP Scripting API, which happens to be the
subject of the next section.

5.11 XMP
This is another essential topic that deserves a section of its own (see Chapter 8), but I’m listing it
here as well since it’s one of the peculiar features that make ExtendScript the remarkable language
it is (JS Tools Guide p. 256-293). XMP stands for eXtensible Metadata Platform: a technology first
created by Adobe, then standardized (it’s under the wings of ISO since 2012) that deals with the
creation, processing and exchange of metadata information in digital media.
XMP is used, for instance, by photographers for copyright information, or by cameras themselves
when creating the file to, say, geo-tag pictures with GPS data. In the context of more complex
(and very likely: automated) workflows, metadata is crucial to keep track of the various steps a file
undergoes – think about gaming or 3D pipelines, or DAM (Digital Assets Management).
Since 2008, in addition to metadata on a file basis, Photoshop also supports per layer metadata, a
fascinating and perhaps little-known fact. Refer to Chapter 8 for more extensive coverage of the
topic.
⁶Out of curiosity I did try to build it, but I run into compiling errors, and I quickly gave up.
The ExtendScript Domain 115

5.12 Localization
Photoshop comes into a variety of different languages: scripts, too, can adapt to the user’s locale –
i.e. the language – using strings that match his/her settings (JS Tools Guide p. 224-226).
As an example, let’s suppose that you’re building a script where at some point the user must be
presented with a choice.

1 if (confirm("Save also a backup copy?")) {


2 // do the backup step
3 } else {
4 // go ahead with the script
5 }

By default you’d write the message in English, especially if the script has an international audience.
Localization lets you display that string, say, in French, and Portuguese too – as long as you can
provide the translation. Please note: it’s not an automatic translation service!
The feature is based on string objects that link a two chars code (such as "en", "fr", "pt") to the
actual translation.

var confirmStrings = {
en: "Save also a backup copy?",
it: "Salvare una copia di backup?",
pt: "Salvar também uma cópia de backup?"
}

Actually, besides the above language code (aka ISO 639-1, list here), you can optionally specify
Region codes as well (aka ISO 3166-1 alpha 2, list here), e.g. "en_US", "en_CA", etc.
The localized strings are displayed using the global localize() function, which automatically
fetches the user’s setting and picks the right match.

confirm(localize(confirmStrings));

You’re able to get the current language with the locale property of the dollar object. Which happens
to be read-write: as a result, it’s possible to switch to a different locale to test your translations
temporarily.
The ExtendScript Domain 116

$.locale; // en
$.locale = "pt";
confirm(localize(confirmStrings)); // Salvar também uma cópia de backup?
$.locale = "it";
confirm(localize(confirmStrings)); // Salvare una copia di backup?

In case every element that your script exposes to the users are localized, you can get rid of the
localize() and pass the localization object directly, provided you’ve set the $.localize property
to true.

$.localize = true;
confirm(confirmString);

Since the three syntaxes are very similar, let’s recap below to avoid confusion.

$.locale; // get/set the current locale (string)


$.localize; // get/set the auto-localization (boolean)
localize(); // function, returns the localized string,
// accepts a localization object as the parameter

There are extra, interesting features that you should know about when it comes to ExtendScript
localization.
First, not always a flat, translated string is everything you need. Say that you have to provide a
localized version of the following alert:

var d = app.documents.length;
if (d > 1) {
alert("You have " + d + " documents open.\nKeep only one of them!");
}

Composing localized substrings is a majestic annoyance. Instead, while building the localization
object, you’re allowed to use template strings, which work as follow:
The ExtendScript Domain 117

var alertString = {
en: "You have %1 documents open.\nKeep only one of them!",
it: "Ci sono %1 documenti aperti.\nNe occorre uno solo!"
}
var d = app.documents.length;
if (d > 1) {
alert(localize(alertString, d));
}

Templates use the %n syntax (e.g. %1, %2, etc.): these parameters are passed to the localize() function,
and are then inserted – respecting their order.

var alertString = {
en: "Free updates until %1/%2/%3",
fr: "Mise à jour gratuite jusqu'au %1/%2/%3"
}
alert(localize(alertString, 12, 31, 2016));
// Free updates until 12/31/2016
// Mise à jour gratuite jusqu'au 31/12/2016

Second, you can also localize based on platform (Mac or Windows) appending the "_Mac" and "_Win"
suffixes to either the language (e.g. "en_Mac") or the region (e.g. "en_US_Win").

var alertString = {
en_Mac: "Command + click on the Process button to display more options",
en_Win: "CTRL + click on the Process button to display more options",
// etc.
}

Third, if you look at the code of Adobe’s scripts, very likely you’ll run into strange localization
strings (also known as zstrings) containing the triple dollar $$$ sequence.

localize("$$$/JavaScripts/ArtboardsToFiles/Title=Artboards To Files");

These are for Adobe’s internal use, in their dialogs. Nothing prevents you, though, to borrow some
of them if you’re in need (they’re surely better than my Google Translate strings, and cover many
more language translations); their original purpose is of no interest to you, as long as they provide
the translation of the right words.
The ExtendScript Domain 118

"$$$/Webdav/Progress/Download=Downloading file"
"$$$/TransferMode/Desaturate=Desaturate"
"$$$/Comps/EasterMonkey/PanelName=Layer Monkey!" // WTF?!

Where did I get them? Fair question. I’ve been advised to use the string command in the Terminal,
targeting the Photoshop executable (the one within the Adobe Photoshop CC 2017.app file):

strings '/Applications/Adobe Photoshop CC 2017/Adobe Photoshop CC 2017.app/Contents/\


MacOS/Adobe Photoshop CC 2017' | grep '\$\$\$' > ~/Desktop/zstrings.txt

The above strips all the occurrences of the "$$$" string from the Photoshop exec, and writes them
into a file on your desktop (you can find it in the code zip as well).
Please note that with zstrings it’s possible for you to compose localized versions of the absolute path
of interesting locations – for instance the Photoshop /Preset/Scripts folder.

// Scripts path
var sp = app.path + "/" +
localize("$$$/ScriptingSupport/InstalledScripts=Presets/Scripts");
// Plugins path
var pp = app.path + "/" +
localize("$$$/private/Plugins/DefaultPluginFolder=Plug-Ins");
// Actions path
var ap = app.path + "/" +
localize("$$$/ApplicationPresetsFolder/Presets=Presets") + "/" +
localize("$$$/private/Photoshop/FilePreferences/DefaultActionsDir=Actions");
// etc.

5.13 Operator overloading, Constants and UnitValue


To conclude the list of the ExtendScript language peculiar characteristics, there are three features to
mention.
Operator overloading in Classes is about extending and/or overriding the default behavior of
mathematics or boolean operators – in the context of a Class (JS Tools Guide p. 235-236). Say for
instance that you need a Rect class: you’re allowed to define the behavior of the + operator so that
you can actually add two Rect instances and have their composite area as the returned value.
The ExtendScript Domain 119

1 // Rect "class"
2 function Rect(w, h) {
3 this.width = w;
4 this.height = h;
5 }
6 // Overriding the "+" operator
7 Rect.prototype["+"] = function(r) {
8 return (this.width * this.height) + (r.width * r.height)
9 }
10 // Instantiating to Rects
11 var r1 = new Rect(50, 100),
12 r2 = new Rect(20, 20);
13 // Adding two Rects
14 r1 + r2; // 5400

Please refer to the JavaScript Tools Guide for details upon the unary and binary operators available
for overloading. If you’re interested in DOM extension, please jump to this section of the book.
Constants have been added to the ECMAScript standard quite recently, but we could use const
from the very beginning (what an act of revenge…). As opposed to what you’d expect, assigning a
new value to a constant doesn’t return an error, while an error is thrown when you try to redeclare
the constant:

const AUTHOR = "Davide Barranca";


AUTHOR = "Barranca Davide"; // no Error here, but the new value won't stick
$.writeln(AUTHOR); // Davide Barranca
const AUTHOR = "Anonymous"; // Error: AUTHOR redeclared

We’ve already met the UnitValue (JS Tools Guide p. 230-233), representing measurements (a value
bundled with its unit of measurement) which can be expressed with more or less verbosity.

var w1 = new UnitValue(1280, "px"); // value, "unit"


var w2 = new UnitValue("1280 px"); // "value, unit" var
w3 = UnitValue(1280, "px"); // without "new"
var w4 = UnitValue(1280, "pixel"); // with not abbreviated, singular unit
var w5 = UnitValue(1280, "pixels"); // with not abbreviated, plural unit
// or a combination of the above

You can query the UnitValue instance for its value or type:
The ExtendScript Domain 120

w1.type; // "px"
w1.value; // 1280

Instances have two similar methods, as() and convert().

var v = new UnitValue(1, "inch");


// Returns the value of the converted UnitValue
v.as("cm"); // 2.54 (number)
// Convert the UnitValue into the specified new unit of measurement
v.convert("cm"); // returns true if successful
v instanceof UnitValue; // true: v is equal to UnitValue(2.54, "cm")

Lastly, there are several methods to compare and operate on UnitValue. I leave the full discovery
to you as an exercise – a single example closes this extended look at the ExtendScript exclusive
features.

var inch1 = new UnitValue(1, "inches");


var inch2 = new UnitValue(2, "inches");
inch1 < inch2; // true

These pages have covered the peculiar characteristics that make ExtendScript a compelling program-
ming language: hopefully, they are enough to let you forget about what is, and possibly will be in
the future as well, missing.
The next Chapter is going to be long, and dense – the topic has a reputation that commands respect!
I’m talking about another ExtendScript unique feature, this time exclusive to Photoshop only, and
as powerful as it gets.
6. Action Manager
6.1 Action Manager is not the DOM
Sooner or later, while working on your scripts, you will inevitably step into DOM gaps. Say that you
need to apply a Hue/Saturation adjustment, uncheck the Move Tool’s Auto-Select option, or simply
select the Move Tool itself – the list could be extensive.
How do you do that? There’s no app.selectTool() function described anywhere in the documen-
tation; impossible you say? Hard to believe. Ask that in the Forums, and a fellow developer will
eventually come up with the following answer:

1 var desc = new ActionDescriptor();


2 var ref = new ActionReference();
3 ref.putClass( app.stringIDToTypeID('moveTool') );
4 desc.putReference( app.charIDToTypeID('null'), ref );
5 executeAction( app.charIDToTypeID('slct'), desc, DialogModes.NO );

Now, chances are that you’re either green on Scripting and I can read your mind (it contains
a localized version of the WTF is that?! exclamation), or you’re more experienced yet you can
remember very clearly that WTF-moment back in the day when you first ran into Action Manager
code.
To say that Action Manager (aka AM) is an entangled topic is the understatement of the decade; I
advocate a staged, Q&A approach, which I’ll adopt throughout this whole Chapter.

• Is DOM the only Photoshop Scripting domain? According to the German polymath Gottfried
Leibniz, “we live in the best of all possible worlds”: such a statement could stand criticism
back in 1710, yet it’s easily dismissed by today’s Photoshop incomplete DOM coverage, global
warming statistics, and my wife’s remarks on married life. No, you’re not restricted to DOM
Scripting only, you’d be unbearably limited; there is more.
• How did you, Davide, learn Action Manager? Have you ever seen one of those documen-
taries where paleontologists in the field dig a multitude of bone flakes, then they try to make
sense of all of them back in the lab reconstructing an entire bone, if not a complete skeleton?
Mine has been, and still is, a very similar learning process. Forums, existing bits of code, old
documents, chats with other developers, seasoned with an insane amount of time spent in
trying, and mostly failing.
Action Manager 122

Generally speaking, Creative Cloud applications expose to the Scripting layer Classes and Collec-
tions, methods and properties, constants, etc. hierarchically ordered in the Document Object Model
(discussed in Chapter 4). And… that’s what Scripting is all about. DOM coverage can be as broad
as InDesign’s – where the Scripting API is highly granular – or quite lousy; I’ve been told that
Illustrator is a good fit for the low end of the spectrum. Typically, if the Scripting interface doesn’t
expose, say, Tools selection… well, Tools selection isn’t available to you as a scripts developer, period.
Except for Photoshop, which is different. Action Manager is often considered the holy grail of
Photoshop Scripting, for it goes beyond the DOM, and can succeed in tasks otherwise plain
impossible.

• How’s that I’ve never heard about¹ Action Manager before? AM is featured in the
Photoshop Scripting Guide for a total of about six pages, general concepts and code samples
included. The JavaScript Scripting Reference features more content, but in all honesty, it is
useless if you haven’t got a clue about the big picture.

If you don’t know AM, you’ve dismissed it as unimportant, or too cryptic to be worth the effort, I
don’t blame you. Learning resources are, as a matter of fact, non-existent; or scattered and buried in
Forums’ topics which looks like being written by members of some secret society, rather than fellow
developers.
My goal is to shed some light on the subject and make you acquainted with it; the next step is to
let you understand, use, and tweak pre-built AM snippets in your code; and finally, you’ll be able
to probe Photoshop and generate the AM code that precisely targets your needs. Read along – and
resist the temptation to jump to the section that allures you the most: to digest AM properly, all the
pills should be taken in order.

6.2 Action, Events, and historical perspective


Everything started when Actions first appeared in Photoshop 4 – back in 1996, some glorious twenty-
something years ago. As we all know, Actions are a fundamental building block of automated
workflows, allowing you to record, save and replay bits of your own (or others’) Photoshop activity.
Despite our familiarity with them, you might not know or haven’t realized, that Actions and Action
Manager are somehow linked. Quoting the Photoshop SDK documentation:

Internally, Actions consist of Photoshop events, and targets of those events. An action can
be a single event or a sequence of events. Events themselves are individual Photoshop
commands or instructions that act on elements or objects. Every Photoshop event has a
data structure associated with it that the automation plug-in uses to manipulate the event
and target selection.
¹This reminds me a very talented photographer and friend of mine, who in the late nineties happened to be hired for a shooting job by a
renowned publisher’s staff member. The company owner, reviewing the final prints, fell in love with his style so crazily, that he asked to meet
this new contractor as soon as possible: when they finally came face to face, he screamed: “Mr. Bigano, how’s that I’ve never heard about you
before?!”
Action Manager 123

In other words, what you see in the Actions palette has been implemented under the hood as an
Event system, based upon an extensive database of Photoshop commands, filters, and elements.
You select a rectangular portion of the image: that’s an event, it has an associated data structure
which defines the selection bounds, and acts on a target – the active Layer. Then you apply the
UnSharp Mask filter: that’s another event, its associated data structure defines the filter’s Amount,
Radius, and Threshold, and its target is the current selection of the active Layer. And so forth.
Thus, recording an Action means keeping track of the events sequence, complete with descriptor
blocks (i.e., related data and event targets).

One key aspect that you might have overlooked in the Photo-
shop SDK excerpt is the reference to Automation Plug-ins. In
case you’ve never heard about them, they’re a pretty neat way
to wrap Photoshop operations into a binary package. Forgive
me for the oversimplification, but they’re sort of “plugin-ized”
Actions, plus extra logic, and a GUI², that you write in C/C++.
Why am I bringing this to your attention? Well, the original
quote comes from a 1999 paper called “Adobe® Photoshop®
5.5 Actions Event Guide”³, which is ultimately about building
Automation Plug-ins. That document represents a surprisingly
interesting source of tangentially AM-related insights because
it describes how two fundamental elements of the Photoshop
Object Model – Containment Structures and Inheritance Hier-
archies – are associated to the Event framework which is at the
base of the ExtendScript’s Action Manager code.

• What are “Containment Structures” and “Inheritance Hierarchies”? The Object Model
codifies the relation between Photoshop’s elements (Documents, History states, etc.), and
their available methods and/or properties. Containment Structures define the parent-child
bond (what element holds another element: a Document contains LayerSets, LayerSets contain
ArtLayers, etc.), while Inheritance Hierarchies specify what methods or properties are available
to the objects held in Containers (e.g., both Curves or Levels adjustment layers inherit props
and meths from the Adjustment Layer class).

Now, to assist C/C++ programmers in building Automation Plug-ins, Adobe made (and still makes)
available the source code of a helper plug-in called “Listener”. It’s a neat piece of software since it
keeps listening for all the events fired as a consequence of you, using Photoshop; and it logs them,
events and their descriptor blocks, in a plain text file on your Desktop. Remarkably, the log consists of
full-working C code that programmers can borrow and use within their own Automation Plug-ins.
²Thanks to the advent of HTML Panels, they’re perhaps less common nowadays; as an example, Pixel Genius’ products have been built
as Automation Plug-ins.
³Find it in the Photoshop SDK.
Action Manager 124

So to speak, the Listener is very much like having an Action recording everything you do in
Photoshop; except that you’re not saving steps into a .atn file, but rather C code in a .log file.
If you’re curious, this is the result of logging the UnSharp Mask filter:

1 SPErr PlayeventUnsharpMask(/*your parameters go here*/void)


2 {
3 PIActionDescriptor result = NULL;
4 DescriptorTypeID runtimeKeyID;
5 DescriptorTypeID runtimeTypeID;
6 DescriptorTypeID runtimeObjID;
7 DescriptorTypeID runtimeEnumID;
8 DescriptorTypeID runtimeClassID;
9 DescriptorTypeID runtimePropID;
10 DescriptorTypeID runtimeUnitID;
11 SPErr error = kSPNoError;
12 // Move this to the top of the routine!
13 PIActionDescriptor desc0000000000000888 = NULL;
14 error = sPSActionDescriptor->Make(&desc0000000000000888);
15 if (error) goto returnError;
16 error = sPSActionDescriptor->PutUnitFloat(desc0000000000000888,
17 keyAmount, unitPercent, 300);
18 if (error) goto returnError;
19 error = sPSActionDescriptor->PutUnitFloat(desc0000000000000888,
20 keyRadius, unitPixels, 1);
21 if (error) goto returnError;
22 error = sPSActionDescriptor->PutInteger(desc0000000000000888,
23 keyThreshold, 5);
24 if (error) goto returnError;
25 error = sPSActionControl->Play(&result, eventUnsharpMask,
26 desc0x888, plugInDialogSilent);
27 if (error) goto returnError;
28 returnError:
29 if (result != NULL) sPSActionDescriptor->Free(result);
30 if (desc0000000000000888 != NULL) sPSActionDescriptor->Free(desc0000000000000888);
31 return error;
32 }

Even if you don’t know any C language (neither do I), some terms can be identified with confidence.
The event correlated to the UnSharp Mask filter is very likely eventUnsharpMask; its descriptor must
hold the event’s relevant data as key/values pairs, and in fact there are keyAmount, keyRadius and
keyThreshold, associated to the values that I’ve actually used (300 for the Amount, 1 for the Radius,
5 for the Threshold).
Action Manager 125

It’s not really important whether we understand the above code or not; I’ve added it here for a couple
of reasons.

• It shows what happens under Photoshop’s hood in terms of the Action Event system code.
• Having seen the C counterpart, you’re going to find the ExtendScript Action Manager much
more accessible.

We haven’t but nearly scratched the surface of AM, still, in my opinion, this introduction is
fundamental to grasp the basics: which are going to follow smoothly in the next section.

6.3 ScriptListener

Thomas Ruark, a long time Photoshop Senior Computer Scientist who’s currently
in charge, among the rest, of the PS API and SDK (also known as the Patron
Saint of PS developers, tiny holy picture on the right) wrote the “ScriptListener”
plugin (aka SL), which as a matter of fact is the scripting version of the original
“Listener” plugin. As you might guess, instead of logging C code, it writes down
valid ExtendScript.
You can find the download link in the Photoshop Scripting page; to install it, move the plugin file
(a .8bf for Windows, .plugin for macOS) in the Adobe Photoshop CC 2019/Plug-ins/ folder and
restart the application. From that point onwards, almost each and every action that you perform
in Photoshop is going to be transcribed as ExtendScript code in a ScriptingListenerJS.log file⁴,
created on your Desktop.
Since the logging is non-stop, the file can get quite big very quickly: you’re allowed to trash it
anytime, even when the application is running. It’ll be instantly recreated as soon as you perform
some Photoshop action⁵ – e.g., open a file, duplicate a layer, add a guide, almost everything.
Continuous logging and disc access may also affect the program performance, and slow down
Photoshop. If you don’t need ScriptListener, you might be better off disabling it. To stop the logging,
you can either:

• Move the ScriptListener plug-in away from the /Plug-ins/ folder, and restart Photoshop.
• Rename the plugin, prepending a ∼ (tilde), e.g., ∼ScriptingListener.plugin, and restart the
application.
• Run the following code to live-toggle the logging ON/OFF.

⁴Please note that the plugin is called “ScriptListener”, while the log file is “ScriptingListener”.
⁵In this case with “action” I mean “activity”, e.g., the user pushing buttons in the PS interface, calling menu commands, etc. I’ll write
“Actions” (capital “A”) when referring to macros, the kind of which belongs to the Actions palette.
Action Manager 126

1 /* Switch logging OFF */


2 var listenerID = stringIDToTypeID("AdobeScriptListener ScriptListener");
3 var keyLogID = charIDToTypeID('Log ');
4 var d = new ActionDescriptor;
5 d.putBoolean(keyLogID, false);
6 executeAction(listenerID, d, DialogModes.NO);
7
8 /* Switch logging ON */
9 var listenerID = stringIDToTypeID("AdobeScriptListener ScriptListener");
10 var keyLogID = charIDToTypeID('Log ');
11 var d = new ActionDescriptor;
12 d.putBoolean(keyLogID, true);
13 executeAction(listenerID, d, DialogModes.NO);

Please note that the code to disable⁶ ActionManager logging is Action Manager itself — very meta.

The log file contains all the commands that you’ve performed in Photoshop, separated with a
comment line so that it’s easier for you to understand when one ends, and the following starts.
If you copy and paste that logged code in ESTK, target Photoshop, and run it… it’ll be like playing
a recorded Action of those recorded events – of course the code must be in the right context (e.g., if
you’re playing AM for the UnSharp Mask filter, a layer must be selected beforehand).
⁶I’ve not been able to write code that checks the current logging status, though.
Action Manager 127

In your exploration of the Action Manager underworld, you might find that, in rare cases,
some actions don’t get transcribed in a useful manner. There might be a slight difference in
logged code among versions, so if you happen to have a very old Photoshop (e.g., CS3), and
your hard drive doesn’t run out of space because of it, it might be a good idea to keep and
install the ScriptListener on it too. Just in case.
Another mild, related issue with newer versions of Photoshop, is that the ScriptListener’s
signal/noise ratio in the logged code is getting worse: the chances are that it contains plenty
of "modalStateChanged" or "modalHTMLPending" references that are – to your purposes –
pointless. They take into account things such as the status of proprietary HTML Panels used
in the Photoshop GUI (e.g., the Recent Files and New File dialogs); you don’t really care
about that.

6.4 charIDs and stringIDs


As our guinea pig, let’s use the Action Manager equivalent of the C code you’ve already seen,
associated with the UnSharp Mask event, which is as follows.

1 var idUnsM = charIDToTypeID( "UnsM" );


2 var desc3 = new ActionDescriptor();
3 var idAmnt = charIDToTypeID( "Amnt" );
4 var idPrc = charIDToTypeID( "#Prc" );
5 desc3.putUnitDouble( idAmnt, idPrc, 300.000000 );
6 var idRds = charIDToTypeID( "Rds " );
7 var idPxl = charIDToTypeID( "#Pxl" );
8 desc3.putUnitDouble( idRds, idPxl, 1.000000 );
9 var idThsh = charIDToTypeID( "Thsh" );
10 desc3.putInteger( idThsh, 5 );
11 executeAction( idUnsM, desc3, DialogModes.NO );

In case you still have doubts about the way to produce it, install the ScriptListener plugin, restart
Photoshop, open a file and apply the filter – you’ll find the above snippet logged among much other
stuff in the ScriptingListenerJS.log file on your desktop.
The very first thing that I’d like you to notice is the ubiquitous use of the charIDToTypeID() function.
Each and every Action Manager snippet that is logged contains it, or its cousin stringIDToTypeID()
– which by the way are both globally available since they are methods of the app object (hence app.
can be omitted). What is that all about?
Back in Photoshop 4, when the whole business of Actions and related underlying events started,
every programmable event was given a unique identifier, as a four characters string (a charID). In
the context of Automation Plug-ins, the charID was used as the key in a Hash Map to retrieve the
associated event;
Action Manager 128

• What’s a Hash Map? Also known as Hash Table, it is a data structure that associates keys with
values. It does so via a Hash Function, which takes as the parameter the key, and computes the
index of an array that contains, and finally returns, the desired value. See here for a more
detailed explanation.

Try to run in ESTK (targeting Photoshop – annoying and frequent oversight) the following line.

charIDToTypeID( "UnsM" ); // 1433301837

It returns a long, apparently meaningless 32bit integer. The charIDToTypeID() method acts as the
Hash Function: it takes as a parameter the unique charID assigned to the UnSharpMask event
("UnsM", which vaguely reminds you the UnSharpMask word, doesn’t it?) and returns the index
– here 1433301837 – that Photoshop uses to look up the corresponding internal event. That number
is the typeID.
Not only events such as the UnSharpMask filter are referred to using this hashing mechanism, but
everything you might want, or need, to use. Go back to the initial Action Manager sharpening code;
can you spot "Amnt", "Rds ", and "Thsh"? Might they be Amount, Radius, and Threshold? Sure they
are.
By and large, you could think about charIDs as strings you use to reference all kinds of “useful
stuff” in Action Manager; you cannot operate on them directly (since they’re just hash keys). Hence,
you must pass them to the charIDToTypeID() function (the hash function), that returns an integer
number (the typeID) that Photoshop uses to look up the corresponding internal element.

A charID always consists of exactly four characters. What (we have assumed) is the
Radius’ charID is made with three letters plus one whitespace, like "Rds ". Another charID
is "N " (one letter, three spaces). If you omit the spaces, or in general if the charID isn’t made
with exactly four chars, you’ll get an “Illegal Argument” error.

Starting with Photoshop 5, Automation Plugins have allowed developers to include in the list of
available events also third-party, scriptable filters – which needed to have their unique identifiers.
In addition to that, other identifiers were added to cover new Photoshop features and commands
introduced with the version upgrade. As a result, somehow mnemonically-friendly four chars strings
couldn’t be up to the task anymore, and stringIDs were introduced.
A stringID is a more readable and descriptive unique identifier that, very much like a charID, is
used with a hash function this time called stringIDToTypeID() to get the same typeID thing (the
index used to look up the corresponding internal element).

stringIDToTypeID( "unsharpMask" ); // 1433301837

You have a sharp eye, so you’ve noticed that you’re getting the same typeID from both the "UnsM"
charID and the "unsharpMask" stringID (in this case: 1433301837).
Action Manager 129

stringIDToTypeID( "unsharpMask" ) === charIDToTypeID( "UnsM" ); // true

There is, in fact, a correspondence in both hash functions so you’re allowed to use the one that you
prefer the most, either the stringID or the charID⁷.

• But how do I know that "unsharpMask" and "UnsM" point to the same thing? First, they do
only if you use stringIDToTypeID() for the former and charIDToTypeID() for the latter – it’s
easy to get this wrong if you’re a beginner. Second, there’s a way, which I’m going to tell you
in a moment.

A long list of stringIDs is found within the Photoshop SDK in the PIStringTerminology.h file, but
you want to use four methods of the app object, namely:

// app. is optional
app.charIDToTypeID (charID); // returns a typeID
app.stringIDToTypeID (stringID); // returns a typeID
app.typeIDToCharID (typeID); // returns a charID
app.typeIDToStringID (typeID); // returns a stringID

You have also hash inverse functions, that you can combine to get the equivalent stringID from a
charID, passing through an intermediate typeID step.

// charID in, stringID out


function c2s(c) { return typeIDToStringID(charIDToTypeID(c)) }
c2s("UnsM"); // "unsharpMask"
c2s("Amnt"); // "amount"
// etc.

It’s now time to clean a bit the original ScriptingListenerJS.log output; I’ll show you below all
the steps that I usually perform on raw AM code to make it more readable, and as less unfriendly
as possible.
For your convenience, this is the starting point for the UnSharpMask event:

⁷Few old commands may not have stringIDs, and few new commands may not have charIDs.
Action Manager 130

1 var idUnsM = charIDToTypeID( "UnsM" );


2 var desc3 = new ActionDescriptor();
3 var idAmnt = charIDToTypeID( "Amnt" );
4 var idPrc = charIDToTypeID( "#Prc" );
5 desc3.putUnitDouble( idAmnt, idPrc, 300.000000 );
6 var idRds = charIDToTypeID( "Rds " );
7 var idPxl = charIDToTypeID( "#Pxl" );
8 desc3.putUnitDouble( idRds, idPxl, 1.000000 );
9 var idThsh = charIDToTypeID( "Thsh" );
10 desc3.putInteger( idThsh, 5 );
11 executeAction( idUnsM, desc3, DialogModes.NO );

Let’s start: first, I use a few shorthand functions.

function s2t(s) { return app.stringIDToTypeID(s) }


function c2t(c) { return app.charIDToTypeID(c) }

Second, I tend always to use stringIDs, which I manually find with the help of the c2s() function,
because they’re far more descriptive: see the substitution below.

1 function s2t(s) { return app.stringIDToTypeID(s) }


2
3 var idUnsM = s2t("unsharpMask");
4 var desc3 = new ActionDescriptor();
5 var idAmnt = s2t("amount");
6 var idPrc = s2t("percentUnit");
7 desc3.putUnitDouble( idAmnt, idPrc, 300.000000 );
8 var idRds = s2t("radius");
9 var idPxl = s2t("pixelsUnit");
10 desc3.putUnitDouble( idRds, idPxl, 1.000000 );
11 var idThsh = s2t("threshold");
12 desc3.putInteger( idThsh, 5 );
13 executeAction( idUnsM, desc3, DialogModes.NO );

Third, I rename the descriptor variables, which are usually named like desc3, desc88, etc. Then I
bring the creation of new ActionDescriptor instances on the very top of the snippet (don’t worry if
you still don’t know what an ActionDescriptor is – I’m cleaning the SL output for that very reason:
make it easier for you to learn AM).
Action Manager 131

1 function s2t(s) { return app.stringIDToTypeID(s) }


2
3 var d = new ActionDescriptor();
4
5 var idUnsM = s2t("unsharpMask");
6 var idAmnt = s2t("amount");
7 var idPrc = s2t("percentUnit");
8 d.putUnitDouble( idAmnt, idPrc, 300.000000 );
9 var idRds = s2t("radius");
10 var idPxl = s2t("pixelsUnit");
11 d.putUnitDouble( idRds, idPxl, 1.000000 );
12 var idThsh = s2t("threshold");
13 d.putInteger( idThsh, 5 );
14 executeAction( idUnsM, d, DialogModes.NO );

Lastly, I get rid of all the other variable declarations, replacing them where they’re used. E.g. the
vars in line 5, 6, 7 in the above snippet go in the putUnitDouble() of line 8; vars in 9 and 10 go in
the putUnitDouble() of line 11, etc.

1 function s2t(s) { return app.stringIDToTypeID(s) }


2
3 var d = new ActionDescriptor();
4 d.putUnitDouble( s2t("amount"), s2t("percentUnit"), 300.000000 );
5 d.putUnitDouble( s2t("radius"), s2t("pixelsUnit"), 1.000000 );
6 d.putInteger( s2t("threshold"), 5 );
7 executeAction( s2t("unsharpMask"), d, DialogModes.NO );

Now we’re getting somewhere! From this point onwards, I’ll use this kind of transformation, and
take it for granted.

Several free tools automatically beautify AM code. The first one has been xbytor’s
“LastLogEntry” from his xtools, which grabs the last entry in the SL log and can fix it.
Marek Omszański has shared his AM Code Replacement. Javier Aroche has published his
parse-action-descriptor-code on GitHub. Tomas Šinkūnas built Clean SL, Jaroslav Bereza
did ActionManagerHumanizer. The output of such tools may vary, so which one to use (if
any) is up to you.

6.5 ActionDescriptors and DialogModes: executeAction


Hopefully, the UnSharpMask event is not anymore as scary as it was at the beginning. Even if you
don’t know very much about AM yet, you can confidently place your bet on something. At least,
Action Manager 132

Amount, Radius, and Threshold are there with their values, clearly associated one another. How,
and why… it’s still unclear. Let’s find out.
It’s easier to grasp the meaning of that AM snippet starting from the last line of code: executeAction()
is the global function that dispatches the event.

// executeAction( eventID, descriptor, displayDialogs)


executeAction( s2t("unsharpMask"), d, DialogModes.NO );

The first parameter is the event’s ID (the event’s typeID retrieved either from the stringID or the
charID).

As I’ve mentioned earlier in this Chapter, the event system also relies upon a data structure that
carries all the needed information for Photoshop to run the filter, such as the Amount, Radius
and Threshold (the target is implied as the currently active Layer). This is the duty of the second
parameter, a so-called Descriptor object (here d): an instance of the ActionDescriptor class, which
is precisely a particular key/value pairs container.
Within an ActionDescriptor (aka AD), each key is defined with the typeID of what it represents,
while values can be of many different types: integers, Type Units, nested Descriptors, etc. Also the
method used to store a key/value pair in the Descriptor depends on the kind of data you’re going to
stick in it:

4 // ...
5 d.putUnitDouble( s2t("radius"), s2t("pixelsUnit"), 1.000000 );
6 d.putInteger( s2t("threshold"), 5 );
7 // ..

"threshold"⁸is an integer, you’re going to use the descriptor’s putInteger() function (line 6);
conversely, a slightly more complex value such as a floating point number coupled with its unit
of measure uses the putUnitDouble() method (line 5).
To sum up, an ActionDescriptor is a central data structure used in AM code to hold key/value pairs
(up to complex arrangements of deeply nested Descriptors), needed to execute AM events. We’ll use
ADs throughout the rest of the book, so you’re going to have many chances to review them.
The last parameter of the executeAction() method deals with the kind of user interaction allowed
during the event – also in case something goes wrong. There are three available constants for the
displayDialogs parameter:

• DialogModes.ALL will execute the event, and display (when available) its related dialog box:
in our example, the UnSharp Mask filter dialog is going to pop up, letting the user tweak the
Amount, Radius and Threshold values. It corresponds to ticking the “Toggle dialog on/off”
⁸For the sake of simplicity, I’m going to refer directly to the stringID (e.g. "threshold"), instead of writing something like “the typeID
correspondent to the "threshold" stringID which is used by Photoshop to lookup that very element”.
Action Manager 133

checkbox in the Actions palette. In my experience this is rarely wanted: usually, my routines
don’t require, nor need, user’s interaction after a script is launched: I rely on some sort of GUI
to let the user input values, but under some circumstances, it may be handy to let the native
dialog pop up. Please note that some events do not have any dialog (say, a Selection event), in
which case the action is performed silently.
• DialogModes.ERROR doesn’t require user interaction and relies upon the data passed in by the
ActionDescriptor. In case either the AD is incomplete (some key/values are missing) or the
values are out of range, default values are used. Conversely, an alert will display the error if
something goes wrong – say, you’re trying to apply the UnSharp Mask filter on a hidden layer.
• DialogModes.NO is an entirely silent mode: no dialogs will ever show; in case of troubles, an
error is thrown.

6.6 Building AM functions

At this point we’re ready to reach the first stage


of Action Manager enlightenment – borrowing
code from the ScriptListener log, refactoring it⁹,
and create functions to fill DOM gaps with Action
Manager.
As an example, I’ve chosen a Photoshop com-
mand that is not covered in the DOM: Image >
Adjustments > Black & White (I’ve checked in
the Photoshop JS Reference, and it’s not mentioned
anywhere).
I’ve left untouched all the parameters, and I’ve
ticked the Tint checkbox. Usually, when I tackle
such tasks – wrapping AM code with a function
that is easier to use – I run the command with
default values, and I inspect the ScriptListener log.
Then, I try to modify all the parameters, and I
check against the log whether as a result some
extra code lines have been written or not.
In some cases this leads to a massive AM code
generation: if you don’t believe it, try to open the Hue/Saturation adjustment, and tweak Hue,
Saturation and Lightness of all the seven colors (Master, Reds, Yellow, etc.), including changes in all
the color ranges (the rainbow bar). It’s an ActionManager tsunami.
If you want to fill DOM gaps with your functions, either you have to spend much time creating
them the most inclusive way possible, or you cover the features needed in that very moment,
⁹The refactoring step is theoretically optional, but you don’t want to skip it. If you happen to know the movie “Karate Kid” (1984), it’s the
equivalent of Mr. Miyagi’s “Wax on, Wax off!”.
Action Manager 134

procrastinating its refinement to some day in the future. The latter option is of course faster, but
during very boring and snowy afternoons you might decide to create your library of tools, and craft
the ideal function for that purpose – it’s really up to you.
Anyway, the raw code for the Black & White adjustment (our starting point) is as follows:

1 var idBanW = charIDToTypeID( "BanW" );


2 var desc48 = new ActionDescriptor();
3 var idpresetKind = stringIDToTypeID( "presetKind" );
4 var idpresetKindType = stringIDToTypeID( "presetKindType" );
5 var idpresetKindCustom = stringIDToTypeID( "presetKindCustom" );
6 desc48.putEnumerated( idpresetKind, idpresetKindType, idpresetKindCustom );
7 var idRd = charIDToTypeID( "Rd " );
8 desc48.putInteger( idRd, 40 );
9 var idYllw = charIDToTypeID( "Yllw" );
10 desc48.putInteger( idYllw, 60 );
11 var idGrn = charIDToTypeID( "Grn " );
12 desc48.putInteger( idGrn, 40 );
13 var idCyn = charIDToTypeID( "Cyn " );
14 desc48.putInteger( idCyn, 60 );
15 var idBl = charIDToTypeID( "Bl " );
16 desc48.putInteger( idBl, 20 );
17 var idMgnt = charIDToTypeID( "Mgnt" );
18 desc48.putInteger( idMgnt, 80 );
19 var iduseTint = stringIDToTypeID( "useTint" );
20 desc48.putBoolean( iduseTint, true );
21 var idtintColor = stringIDToTypeID( "tintColor" );
22 var desc49 = new ActionDescriptor();
23 var idRd = charIDToTypeID( "Rd " );
24 desc49.putDouble( idRd, 225.000458 );
25 var idGrn = charIDToTypeID( "Grn " );
26 desc49.putDouble( idGrn, 211.000671 );
27 var idBl = charIDToTypeID( "Bl " );
28 desc49.putDouble( idBl, 179.001160 );
29 var idRGBC = charIDToTypeID( "RGBC" );
30 desc48.putObject( idtintColor, idRGBC, desc49 );
31 executeAction( idBanW, desc48, DialogModes.NO );

It can look better, so I’m going to apply the same refactoring steps you’ve seen for the UnSharp Mask
filter: finding all the stringIDs, integrating the s2t() function, substituting all the vars in the AD
methods. I’ll skip right to the result, which is as follows.
Action Manager 135

1 function s2t(s) { return app.stringIDToTypeID(s) }


2 var d1 = new ActionDescriptor();
3 var d2 = new ActionDescriptor();
4 d1.putEnumerated( s2t("presetKind"),
5 s2t("presetKindType"),
6 s2t("presetKindCustom") );
7 d1.putInteger( s2t("red"), 40 );
8 d1.putInteger( s2t("yellow"), 60 );
9 d1.putInteger( s2t("grain"), 40 );
10 d1.putInteger( s2t("cyan"), 60 );
11 d1.putInteger( s2t("blue"), 20 );
12 d1.putInteger( s2t("magenta"), 80 );
13 d1.putBoolean( s2t("useTint"), true );
14 d2.putDouble( s2t("red"), 225.000458 );
15 d2.putDouble( s2t("grain"), 211.000671 );
16 d2.putDouble( s2t("blue"), 179.001160 );
17 d1.putObject( s2t("tintColor"), s2t("RGBColor"), d2 );
18 executeAction( s2t("blackAndWhite"), d1, DialogModes.NO );

Much cleaner and readable indeed! At this point it is just a matter of substituting the values with
parameters, wrap that with a function block, and decide the sort of API that it needs.
Wax on, wax off, there is a little strange thing to notice here before going any further. Line 9 and 15,
there are suspect "grain" stringIDs. Shouldn’t it be, as a matter of sheer common sense, "green"?
The original charID was a dull "Grn ". You wear your Sherlock Holmes hat and start experimenting:

c2t("Grn "); // 1198681632


s2t("grain"); // 1198681632
s2t("green"); // 1198681632

Both stringIDs and the charID result in the same typeID. Let’s try to convert the two stringIDs
back to charID.

function s2c(s) {return typeIDToCharID(stringIDToTypeID(s)) }


s2c("grain"); // "Grn "
s2c("green"); // "Grn "

As a double check, if you look at the "PIStringTerminology.h" from the SDK, it contains the "green"
stringID¹⁰; so the conclusion here is that two or more stringIDs can point to the same charID (and
consequently to the same typeID), but the reverse is not allowed. A charID or a typeID are always
univocally defined.
¹⁰The "green"/"grain" story has been explained by Tom Ruark here.
Action Manager 136

Second interesting point: I told you that I usually start with the dialog’s default values, then I try
different combination to see what happens in the logged code. In this very case, the only thing that
I could do was to check the Tint option on.

The only difference I got has been in the "useTint" boolean, like:

d1.putBoolean( s2t("useTint"), true ); // Tint enabled


d1.putBoolean( s2t("useTint"), false ); // Tint disabled

Strangely enough, at least to me, "red", "grain" and "blue" coordinates are set in the d2
ActionDescriptor even if the Tint is disabled: the dialog doesn’t show it as grayed out, it’s blank.
Small glitch, though, nothing to be worried about. The point here is that the Tint checkbox is linked
(quite intuitively) to the "useTint" boolean.
Also note that (lines 14-16) the Tint color deserves its Descriptor – in fact, refactoring the SL output
leads to two ActionDescriptors: d1 and d2. It’s a first simple example of nested Descriptors: as I wrote
before, a Descriptor key can also contain another Descriptor as the corresponding value (sometimes,
to tell the truth: up to unbearable levels). This is going to be handy in building our function: you
can get rid of the Tint color Descriptor altogether if the Tint is disabled – the adjustment works the
same. How do I now? I’ve tried: Action Manager is a field that requires to get your hands dirty. I’m
going to make the Tint colors as optional parameters: if they’re present, the second Descriptor is
created; otherwise I won’t bother with it.
We’re really close to building the function. There are at least two ways to pass the parameters, either
directly:

// Direct parameters
function applyBlackAndWhite(red, yellow, green, cyan, blue, magenta,
redTint, greenTint, blueTint) {
// ...
}

Alternatively, as properties of a single parameter object that is passed to the function.


Action Manager 137

/**
* Parameter object, where
* opt = {
* red : ...
* yellow : ...
* green : ...
* cyan : ...
* blue : ...
* magenta : ...
* redTint : ...
* greenTint : ...
* blueTint : ...
* }
*/
function applyBlackAndWhite(opt) {
// ...
}

While I started with the direct approach, lately I find myself preferring the object much more,
especially if required params come in a large number – but I guess it’s a matter of taste¹¹.
After long suspense, here’s finally the final product of our brain juices.

1 function s2t(s) { return app.stringIDToTypeID(s) }


2
3 function applyBlackAndWhite(opt) {
4
5 var d1 = new ActionDescriptor();
6 d1.putEnumerated( s2t("presetKind"),
7 s2t("presetKindType"),
8 s2t("presetKindCustom") );
9 d1.putInteger( s2t("red"), opt.red );
10 d1.putInteger( s2t("yellow"), opt.yellow );
11 d1.putInteger( s2t("grain"), opt.green );
12 d1.putInteger( s2t("cyan"), opt.cyan );
13 d1.putInteger( s2t("blue"), opt.blue );
14 d1.putInteger( s2t("magenta"), opt.magenta );
15
16 // If a tint RGB value is passed (here I check just for the first one)
17 // Tint is assumed as checked/true, and the AD created and filled
18 if (typeof opt.redTint != 'undefined') {
¹¹Speaking of parameters, there is an interesting article about so-called boolean traps that you may want to read – it’s also kind of funny:
it deals with the several, different scenarios where a boolean can mislead you. E.g., those dialogs like: “Do you want to Cancel?”, with “OK”
and “Cancel” as options - which is which?!
Action Manager 138

19 var d2 = new ActionDescriptor();


20 d1.putBoolean( s2t("useTint"), true );
21 d2.putDouble( s2t("red"), opt.redTint );
22 d2.putDouble( s2t("grain"), opt.greenTint );
23 d2.putDouble( s2t("blue"), opt.blueTint );
24 d1.putObject( s2t("tintColor"), s2t("RGBColor"), d2 );
25 } else {
26 // otherwise the Tint is unwanted, and set to false
27 d1.putBoolean( s2t("useTint"), false );
28 }
29 executeAction( s2t("blackAndWhite"), d1, DialogModes.NO );
30 }
31
32 // Examples of use – with Tint
33 applyBlackAndWhite({
34 red : -10,
35 yellow : 60,
36 green : 30,
37 cyan : 40,
38 blue : 60,
39 magenta : 0,
40 redTint : 200, /* Optional */
41 greenTint : 127, /* Optional */
42 blueTint : 127 /* Optional */
43 });

This process of manually running Photoshop commands, inspecting the ScriptListener output,
refactoring its code, and wrap a function around it, is quite common in my experience. Over time
you’ll become faster in doing this – and it’s a great learning experience (especially considering that
in the AM land, experience means a lot).

6.7 Extending the DOM with Action Manager


By now, you’re calling your function “as is”, and not as it were an actual DOM function.

// Now
applyBlackAndWhite({ /* ... */ });
// Perhaps...
app.activeDocument.activeLayer.applyBlackAndWhite({ /* ... */ });

The second form, at the moment, obviously throws an error. You might be tempted to add the
function to the prototype, but due to an ExtendScript bug/feature, a Photoshop Class is not available
until it has been used – see this forum thread.
Action Manager 139

Document; // undefined
ArtLayer; // undefined
app.activeDocument.activeLayer; // using both classes
Document; // Document()
ArtLayer; // ArtLayer()

It might be a little risky to call app.activeDocument.activeLayer for the sole purpose of extending
the DOM (what if there’s no Document open when doing it?). As a neat workaround, you can stick
an empty function inside the class as a new global variable (no var in the declaration); then you’re
allowed to add the prototype like with any other object.

function applyBlackAndWhite(opt) {
// ... AM implementation
}
if (typeof ArtLayer === 'undefined') {
ArtLayer = function() {};
}
// Assigning the AM function to the prototype
ArtLayer.prototype.applyBlackAndWhite = applyBlackAndWhite;

// Now it works!
app.activeDocument.activeLayer.applyBlackAndWhite({ /* ... */ });

This is definitely cool.

6.8 Nested Descriptors and ActionLists


The next level into your ascent to Action Manager enlightenment is to understand what the heck
that blob of code means after all. A brief checklist of salient elements that you need to remember is
found below.

• The executeAction() function is in charge of executing the action (aka event): it wants to
know which event (the eventID), how to deal with dialogs (the DialogModes constant), and
most importantly all the event related data: target, and parameters.
• An ActionDescriptor is a particular container of key/value pairs.
• Each key is defined by a number (typeID) which is an index that Photoshop uses to look up for
the actual stuff someplace in its meanderings.
• We get that key using a hash function stringIDToTypeID() and passing it the thing’s stringID.
This is what I advocate: alternatively, you can use the hash function charIDToTypeID() with a
charID.

To save you the need to flip few pages, here is the Black and White code again, that I’ve indented a
bit differently.
Action Manager 140

1 // Main AD
2 var d1 = new ActionDescriptor();
3 // Enumerated value for Preset
4 d1.putEnumerated( s2t("presetKind"),
5 s2t("presetKindType"),
6 s2t("presetKindCustom") );
7 // Integer for colors
8 d1.putInteger( s2t("red"), 40 );
9 d1.putInteger( s2t("yellow"), 60 );
10 d1.putInteger( s2t("grain"), 40 );
11 d1.putInteger( s2t("cyan"), 60 );
12 d1.putInteger( s2t("blue"), 20 );
13 d1.putInteger( s2t("magenta"), 80 );
14 // Boolean for the Tint
15 d1.putBoolean( s2t("useTint"), true );
16
17 // Secondary AD
18 var d2 = new ActionDescriptor();
19 // Floats for the colors
20 d2.putDouble( s2t("red"), 225.0 );
21 d2.putDouble( s2t("green"), 211.0 );
22 d2.putDouble( s2t("blue"), 179.0 );
23
24 // Inserting Secondary AD into Main AD
25 d1.putObject( s2t("tintColor"), s2t("RGBColor"), d2 );
26
27 // Executing the Action
28 executeAction( s2t("blackAndWhite"), d1, DialogModes.NO );

A way to depict the d1 ActionDescriptor graphically is found on the next page. Don’t ask me why
it is the way it is: I can only describe its existing structure, and infer the purpose of all its elements.
Action Manager programming is chiefly a matter of conjecturing upon the sense of existing, usually
nested, structures; then try to bend them to your advantage. When the venture is successful, your
ego is inflated like a hot-air balloon, and you feel omnipotent; when you fail, miserably, curses from
the past centuries spring from your mouth.
Action Manager 141

Black & White adjustment, d1 ActionDescriptor

Back on track: having d1 on its shoulder everything that the Black & White adjustment needs, let’s
then see what we’re talking about.
The dialog has a Presets dropdown menu that by the way turns to “Custom” as soon as you modify
the Default set of parameters. The Descriptor needs to store Presets information in the "presetKind"
key (line 4). A data type consisting of a set of named values that you could enumerate, is called
Enumeration Type¹²; in order to assign the value in this key you need to use the putEnumerated()
function, passing the Enumeration Type (in this case "presetKindType") and finally the value (here
"presetKindCustom"). Please note that I will be referring to the stringID for simplicity’s sake – you
should know that what’s passed is the corresponding typeID.
Next comes the set of color keys "red", "yellow", "green", "cyan", "blue", and "magenta" (lines 8-
13), that store integers, hence the simpler (compared to the enumerated) putInteger() method.
Remember that in every put-Something() method, the first parameter is the key.
Next, you need to store a boolean that is related to the use of the Tint (line 15). The key is "useTint"
and the method putBoolean().
Things get a little bit more elaborate when it comes to the Tint value, which is itself an object: a
secondary ActionDescriptor (d2, lines 18-22), that you build from scratch filling the three keys "red",
"grain", and "blue" with floats (called double), more or less the same way you did before. How do
you stick an ActionDescriptor as a key of another ActionDescriptor? Thanks to the putObject()
method (line 25): the first param, as always, is the key to fill ("tintColor"), second is the classID
("RGBColor"), and finally third is the actual ActionDescriptor instance.

• Why is the classID equal to s2t("RGBColor")? I don’t know. It could probably be also
"HSBColor" provided that both s2t("HSBColor") is meaningful to Photoshop, and you fill
it with three keys consistent with that colorspace (you can try – I leave this as your AM
homework). The lesson here is that when you deal with the Action Manager code that comes
from ScriptListener logs, you should reserve a slot in your long time memory for everything
you run into. In this Black & White command, you’ve found the "RGBColor" classID. Next
time you’ll need to specify a color for some other, unrelated command, it might be "RGBColor"
¹²Also known as Enumerated Type, see here.
Action Manager 142

as well; or perhaps something different, in which case you’ll try using that in Black & White.
Welcome to the world of experimental AM.

You already know about the final line (28): executeAction() wants the eventID, the big fat d1
descriptor and the DialogModes const. Again, why is the structure as I’ve depicted it in the previous
page illustration – an outer Descriptor, containing several keys of several different types, including
a nested Descriptor – and not something else? My answer is: wrong question. Wax on, wax off.

Do you fancy inspecting something slightly more complicated? Sure you do – here’s a mild version
(i.e., with only the Master and Blue range) of the Hue/Saturation command. I haven’t checked
“Colorized”, I invite you to test it and see whether the difference is noticeable or not.

1 var d1 = new ActionDescriptor();


2 var d2 = new ActionDescriptor();
3 var d3 = new ActionDescriptor();
4 var l1 = new ActionList();
5
6 d1.putEnumerated( s2t("presetKind"),
7 s2t("presetKindType"),
8 s2t("presetKindCustom") );
9 d1.putBoolean( s2t("colorize"), false );
10
11 d2.putInteger( s2t("hue"), -6 );
12 d2.putInteger( s2t("saturation"), 0 );
13 d2.putInteger( s2t("lightness"), 0 );
14 l1.putObject( s2t("hueSatAdjustmentV2"), d2 );
Action Manager 143

15
16 d3.putInteger( s2t("localRange"), 5 );
17 d3.putInteger( s2t("beginRamp"), 195 );
18 d3.putInteger( s2t("beginSustain"), 225 );
19 d3.putInteger( s2t("endSustain"), 255 );
20 d3.putInteger( s2t("endRamp"), 285 );
21 d3.putInteger( s2t("hue"), 38 );
22 d3.putInteger( s2t("saturation"), 26 );
23 d3.putInteger( s2t("lightness"), 0 );
24 l1.putObject( s2t("hueSatAdjustmentV2"), d3 );
25
26 d1.putList( s2t("adjustment"), l1 );
27 executeAction( s2t("hueSaturation"), d1, DialogModes.NO );

Woohoo, three ActionDescriptors, plus one new element, an ActionList! Let’s dig into this.
Pattern recognition is a helpful skill in AM-land; the first thing to spot in the main Descriptor d1 is
the very same Enumerated Type for the "presetKind" key (lines 6-8) that we’ve encountered in the
Black & White adjustment. The dialog sports an identical Preset dropdown list.
Similarly, the boolean "colorize" key (line 9) does a job that resembles very much "useTint" from
the previous example.
Looking for other d1.put... methods, we run into this putList() at line 26, where l1, an instance
of the new ActionList class, is used. Think about ActionLists as array-like structures, meant to hold
only elements of the same type. What does l1 contain? Two keys with the same stringID (note for
your future self: it’s possible!) "hueSatAdjustmentV2", which are made of ActionDescriptors (d2 and
d3).

In turn, d2 and d3 contain the actual data ("hue", "saturation", etc.). Do they differ? Yes. d2 is for
the Master adjustment – which I assume doesn’t need to specify a sub-range of the color spectrum
– whereas d3 is for the Blues: in fact, its "localRange" key is set to 5, which fits with Blue being at
index 5 in the colors dropdown items. So basically each defined color needs to specify its position
in the "localRange" key: if that’s missing, a Master correction is assumed.
d3 also has keys for "beginRamp"/"endRamp" and "beginSustain"/"endSustain": the slider’s handlers
in the spectrum bar that define the starting and end points of Blues, and their feather. According to
the numbers, the Ramp is the outer set of handlers, Sustain the inner one.
Can you wrap your head around that structure? Let me try to give you a pseudo-JSON interpretation,
that might help.
Action Manager 144

1 {
2 'presetKind': enum 'presetKindType.presetKindCustom',
3 'colorize': false,
4 'adjustment': AL [
5 'hueSatAdjustmentV2': AD {
6 'hue' : int -6,
7 'saturation' : int 0,
8 'lightness' : int 0
9 },
10 'hueSatAdjustmentV2': AD {
11 'localRange' : int 5,
12 'beginRamp' : int 195,
13 'beginSustain' : int 225,
14 'endSustain' : int 255,
15 'endRamp' : int 285,
16 'hue' : int 38,
17 'saturation' : int 26,
18 'lightness' : int 0
19 }
20 ]
21 }

So basically there are two ADs contained as "hueSatAdjustmentV2"¹³ keys of an AL, which in turn
is the "adjustment" key of the main AD. It’s perhaps easier to abstract the idea and imagine what
d1 would look like when other color ranges are involved (say, Master, Blues, and Yellows) – try that
as an exercise.
Please also note the various Action Manager methods. So far we’ve seen several put-something(),
which depend on the kind of values you need to associate to a key: putInteger() is for integers,
putBoolean() for booleans, etc. Some of them require an extra, middle parameter: for instance
putEnumerated() requires the Enum Type; putObject() instead wants the classID, but only when
you set an AD as a key of another AD (you must omit classID when sticking an AD as a key of
an ActionList). These and all the other methods we’ve not run into yet are documented in the JS
Reference, which I urge you to consult.

Repetition and variation on a theme are a particularly successful learning routine, so here’s the
last example of refactoring and structure analysis before moving onwards: a Guide Layout. Simple
Guides can be scripted via DOM, but the recently introduced Layout feature¹⁴ hasn’t got a DOM
¹³Why V2? Another answer in the “Wrong question. Wax on, wax off” category.
¹⁴I have the impression that new PS features are not getting any DOM coverage by default. The problem arises when Adobe engineers
create features that don’t leave traces in the SL log either: how are we supposed to find them in AM?
Action Manager 145

makeup and must be automated with Action Manager. I take the chance to banish all doubts about
Action Manager vs. DOM.

• Are DOM and AM scripting mutually exclusive? No, they’re definitely not! Think about it
this way: Action Manager has a well-defined purpose because it holds up the implementation
of Actions, through its event system. Everything that can be recorded into Actions (and extra
stuff too – more on this later on) has an AM counterpart. Conversely, only a subset of what can
be expressed with AM has been exposed to the DOM interface. As a result, all DOM scripting
overlaps with AM; whereas all the rest is the Action Manager kingdom.

Back to Guide Layout (AM refactored code


in the next page): since the dialog has the
usual Preset dropdown, you’re going to ex-
pect a "presetKind" Enumerated Type, which
in fact is there. Guide Layout has Gut-
ters and Margins expressed with a unit
of measurement (default is in pixels, but
the dialog accepts a variety of options),
so .putUnitDouble() is needed. There’s a
boolean key for clearing existing guides
("replace"), and integers for Columns and
Rows count.
The structure resembles more the Black &
White example rather than Hue/Saturation.
Two ActionDescriptors are still needed, and the inner one only, d2, seems to carry the largest part of
the actual Guides data (rows and columns, gutters, margins). It is then put in the main Descriptor d1
in the "guideLayout" key, which happens to have the same "guideLayout" classID – please don’t
ask me why.
A rather intriguing Enumerated Type is the "guideTarget" – as you remember, I told you that
the Descriptor optionally carries the Action target: otherwise, the default target is used (generally
speaking: the current document, layer, selection, etc.). At first, I was a bit puzzled: I’ve not run
the Guide Layout on a document with Artboards, so as you can see in the screenshot, the Target
dropdown is grayed out, and I didn’t notice it. This very Enum takes care of the actual Guides target,
which can be as in our case the current Canvas ("guideTargetCanvas"), or if you have Artboards
"guideTargetAllArtboards", and finally "guideTargetSelectedArtboards" for the selected ones
only.
Since I was familiar with the Preset Enumerated Type – which uses the three "presetKind",
"presetKindType", and "presetKindCustom" parameters for respectively the key, the Enumerated
Type and the value – I would have expected something like "guideTarget", "guideTargetType",
Action Manager 146

and "guideTargetCanvas". Instead, for some reason that we mortals can’t know, "guideTarget" is
repeated equal two times – i.e., for the Enumerated Type as well¹⁵.

1 var d1 = new ActionDescriptor();


2 var d2 = new ActionDescriptor();
3 d1.putBoolean( s2t("replace"), true );
4 d1.putEnumerated( s2t("presetKind"),
5 s2t("presetKindType"),
6 s2t("presetKindCustom") );
7
8 d2.putInteger( s2t("colCount"), 12 );
9 d2.putUnitDouble( s2t("colGutter"), s2t("pixelsUnit"), 20 );
10 d2.putInteger( s2t("rowCount"), 2 );
11 d2.putUnitDouble( s2t("rowGutter"), s2t("pixelsUnit"), 20 );
12 d2.putUnitDouble( s2t("marginTop"), s2t("pixelsUnit"), 40 );
13 d2.putUnitDouble( s2t("marginLeft"), s2t("pixelsUnit"), 40 );
14 d2.putUnitDouble( s2t("marginBottom"), s2t("pixelsUnit"), 40 );
15 d2.putUnitDouble( s2t("marginRight"), s2t("pixelsUnit"), 40 );
16
17 d1.putObject( s2t("guideLayout"), s2t("guideLayout"), d2 );
18 d1.putEnumerated( s2t("guideTarget"),
19 s2t("guideTarget"),
20 s2t("guideTargetCanvas") );
21 executeAction( s2t("newGuideLayout"), d1, DialogModes.NO );

The following pseudo-JSON format might help to understand the structure.

1 {
2 'presetKind': enum 'presetKindType.presetKindCustom',
3 'replace' : true,
4 'guideLayout': AD 'guideLayout' {
5 'colCount' : int 12,
6 'colGutter' : unit 'pixelsUnit', 20,
7 'rowCount' : int 2,
8 'rowGutter' : unit 'pixelsUnit', 20,
9 'marginTop' : unit 'pixelsUnit', 40,
10 'marginLeft' : unit 'pixelsUnit', 40,
11 'marginBottom' : unit 'pixelsUnit', 40,
12 'marginRight' : unit 'pixelsUnit', 40
13 },
14 'guideTarget': enum 'guideTarget.guideTargetCanvas'
15 }
¹⁵A certain degree of inconsistency may find its root in the fact that “after all, developers are humans”, reliable sources have told me.
Action Manager 147

These three examples should be enough to get you started in your research, let’s now climb to the
next level.

6.9 Getting data


If, as I hope, you’ve spent some time in the PS JavaScript Reference looking for AM stuff, you
may have noticed that ActionDescriptors and ActionLists have both setters – put methods such as
putInteger() – and getters, like getInteger() and getDouble(). Time has come for us to use them.

As we saw previously, scripting an event (or, as AM puts it, scripting an Action) with Action Manager
is a matter of building the right Descriptor, and pass it to executeAction(). Being ActionDescriptors
key/value containers, nothing prevents us from creating one “in the lab” for experimentation
purposes.
The indentation is slightly bizarre, but in my intention should help to visualize the structure:
elements more to the right are “farther” from the outer container d1 – like branches that fork from
the main trunk.

1 var d1 = new ActionDescriptor(),


2 d2 = new ActionDescriptor(),
3 d3 = new ActionDescriptor(),
4 l1 = new ActionList();
5
6 d1.putInteger(s2t("dailyExpressoConsumption"), 2);
7 d1.putBoolean(s2t("isMacUser"), true);
8
9 d2.putString(s2t("model"), "MacPro");
10 d2.putInteger(s2t("year"), 2009);
11 l1.putObject( s2t("Apple"), d2 );
12
13 d3.putString(s2t("model"), "MacBookPro");
14 d3.putInteger(s2t("year"), 2015);
15 l1.putObject( s2t("Apple"), d3 );
16
17 d1.putList(s2t("computer"), l1);

Weird? If you run the code, you get Result: undefined – which is fine. You’ve created a Descriptor,
what’s the matter?

• How’s it possible that PS knows about s2t("dailyExpressoConsumption")?! The result, I


mean the typeID, is 4804. If you try feeding it to the reverse hash function, i.e. typeIDToStringID(4804),
you get "dailyExpressoConsumption" back¹⁶. Unless you restart Photoshop, in which case
¹⁶The trick doesn’t work with charID; you can’t convert a custom stringID to charID.
Action Manager 148

it is undefined as expected. These elements point to a temporary, session-based creation of


the typeID/stringID link. Perhaps some stringIDs map directly to existing (sort of “static”)
charIDs, others make PS dynamically create a typeID. Anyway, the answer is that Photoshop
can know about s2t("dailyExpressoConsumption").

Let’s try to inspect d1, using the AM peculiar functions.

d1.count; // 3 (count is to AD as length is to Arrays);


// Get the key
d1.getKey(0); // 3924 (what?)
typeIDToStringID(d1.getKey(0)); // "dailyExpressoConsumption" (ah!)
// Get the key type
d1.getType(3924); // DescValueType.INTEGERTYPE
d1.getType(d1.getKey(0)); // DescValueType.INTEGERTYPE (same as above)
// Get the key's value
d1.getInteger(s2t("dailyExpressoConsumption")) // 2 (hurray!)

First, you get the number of descriptor’s keys, using the count property (same as Array.length, but
for Action Descriptors). The key is retrieved with getKey(), which expects the key’s index as the
parameter. Since the key is stored using its typeID, this is exactly what’s returned (so you need to
convert it back to stringID to have it in a readable form).
To get the key’s value, you need to know what type it is, for each type has its getter: in other
words, you can’t getDouble() when the value is an integer. The desc getType() expects the key’s
typeID as the parameter, and returns a DescValueType constant – like BOOLEANTYPE, DOUBLETYPE,
ENUMERATEDTYPE etc. (find the complete list in the JS Reference).

Turns out that the "dailyExpressoConsumption" key is of type DescValueType.INTEGERTYPE, so


you’re allowed to getInteger(), passing – similarly to getType() – the key’s typeID. Slightly better
version of the code above

function t2s(t) { return typeIDToStringID(t) }


for (var i = 0, len = d1.count; i < len; i++) {
$.writeln("Key " + i + ": " + t2s( d1.getKey(i) ) +
" [" + d1.getType(d1.getKey(i))+ "]");
}
// Key 0: dailyExpressoConsumption [DescValueType.INTEGERTYPE]
// Key 1: isMacUser [DescValueType.BOOLEANTYPE]
// Key 2: computer [DescValueType.LISTTYPE]

What we’re doing now is commonly referred to as “Inspecting Descriptors”, and if you want to be
successful, not having a life really helps. At this point, it’s easy to grab the primitive values of your
interest in our simple descriptor. Fancy "isMacUser"? Easy.
Action Manager 149

d1.getBoolean(s2t("isMacUser")); // true

What about nested, more complex objects, how do you get what lies inside them? We have an
ActionList object, so the getList() method seems appropriate. It accepts the key as the parameter
and returns the AL. Which is inspectable the same way the original d1 AD was; the (somehow
tedious) process goes as follows.

// Let's get the List Descriptor into a variable


var l1 = d1.getList(s2t("computer"));
// How many items?
l1.count; // 2
// What kind of type?
l1.getType(0); // DescValueType.OBJECTTYPE
// to be continued...

We’re after the ActionList content, so let’s first extract it (the l1 ActionList) from the d1 Action-
Descriptor via getList(). Please note that I’m calling it l1 to match the code I’ve used to build
the AD in the first place, but right here the variable name could be anything. ActionLists have the
same count property, but, as opposed to AD that uses key/value pairs, they’re index-based – every
get...() method accepts the element index. Moreover, all elements in the same AL are of the same
type, so I’m just checking the first one. Onwards.

// The classID associated to the Action Descriptor stored


// in the first element of the ActionList
t2s( l1.getObjectType(0) ); // "Apple"
// Extract the Descriptor from the first AL element
var d2 = l1.getObjectValue(0);
// Inspecting the inner descriptor
for (var i = 0, len = d2.count; i < len; i++) {
$.writeln("Key " + i + ": " + t2s( d2.getKey(i) ) +
" [" + d2.getType(d2.getKey(i))+ "]");
}
// Key 0: model [DescValueType.STRINGTYPE]
// Key 1: year [DescValueType.INTEGERTYPE]
// And finally the primitive values.
d2.getString(s2t("model")); // MacPro
d2.getInteger(s2t("year")); // 2009
// The same applies for the second AL item
var d3 = l1.getObjectValue(1);
for (var i = 0, len = d3.count; i < len; i++) {
$.writeln("Key " + i + ": " + t2s( d3.getKey(i) ) +
" [" + d3.getType(d3.getKey(i))+ "]");
Action Manager 150

}
// Key 0: model [DescValueType.STRINGTYPE]
// Key 1: year [DescValueType.INTEGERTYPE]
d3.getString(s2t("model")); // MacBookPro
d3.getInteger(s2t("year")); // 2015

The l1 AL contains two items which happen to be of type DescValueType.OBJECTTYPE (i.e.


ActionDescriptors). When an AD is put inside an AL, it’s associated with a classID, that is
retrieved with the getObjectType() (here, "Apple"). The actual ActionDescriptor is returned by
getObjectValue() and stored in the d2 var. Both methods are called passing the AL element index.

What follows closely resembles the work on the outer, d1 Descriptor: loop through the keys, find
their DescValueType, and finally access the actual values using the correspondent methods. The same
applies to the second element of the AL.
I admit that peeping through home-grown, organic ActionDescriptors isn’t particularly fun, but the
purpose here is to flex your Action Manager muscles. Besides, every executeAction() call returns its
AD, so there might be circumstances where this kind of inspection is convenient. However, probing
Descriptors is exceptionally useful when they come straight from Photoshop: you need to master
the last AM object, which is the subject of the next section.

6.10 ActionReference
With Scripting, can you tell whether the currently active document’s bit depth is 8, 16 or 32 bits?
This one might catch you unaware: there’s no Action to perform to get a Scripting Listener log, nor
is there a DOM equivalent. I’ll tell you.

1 var ref = new ActionReference ();


2 ref.putProperty (s2t ("property"), s2t ("depth"));
3 ref.putEnumerated (s2t ("document"), s2t ("ordinal"), s2t ("targetEnum"));
4 var bitDepth = executeActionGet (ref).getInteger(s2t ("depth"));
5 $.writeln("The document bit depth is: " + bitDepth + " bits, Master.");

Contemplate the above appetizer for a moment: elegant indeed. By the end of this Chapter, you’ll
be able to write your own, similar getter, and feel a little bit like Young Dr. Frankenstein.
Bear with me: open a multi-layered document (a minimal example is shown below), select¹⁷ a
different layer, then inspect the log – find the refactored code after that. For such a simple task,
the AM is remarkably cryptic.

¹⁷I mean “make it active”, and not “select a portion of it with one of the available selection tools, e.g., the rectangular marquee tool”.
Action Manager 151

1 var d1 = new ActionDescriptor();


2 var l1 = new ActionList();
3 var r1 = new ActionReference();
4 r1.putName( s2t("layer"), "New BG" );
5 d1.putReference( s2t("null"), r1 );
6 d1.putBoolean( s2t("makeVisible"), false );
7 l1.putInteger( 3 );
8 d1.putList( s2t("layerID"), l1 );
9 executeAction( s2t("select"), d1, DialogModes.NO );

When I gave you a historical perspective on AM implementa-


tion, I’ve briefly touched the idea that Descriptors associated
with events should also contain information about the event’s
Target. In some circumstances, it’s safe to consider it implied
(like in the Black & White or Hue/Saturation examples), e.g.,
the active selection, of the active layer, of the active document.
PS goes up the chain until it finds something suitable to
perform the Action upon: if there’s no active selection, it tries
with the active layer.
However, in other circumstances (like with events such as
"select"), the question cannot be evaded. What’s the event
target, what am I selecting after all?
The ActionReference purpose is precisely to define a Target path through the Photoshop containment
hierarchy; in other words, an AR is stored within a Descriptor’s key as a mean to keep track of the
event’s Target.
The AR is instantiated from the ActionReference Class, and the actual reference created using one
of the available put...() methods. In our case, ScriptListener uses .putName() method to insert
"New BG" (the actual name of the layer I’ve selected myself in my file) as the "layer" key. Which
suggests that the Target of the "select" Action will be… a layer named "New BG". If you look at
the JS Reference (page 44), you’ll see a variety of other methods which names are usually quite
self-explanatory in terms of what they can be used for in defining a Target, e.g. putIdentifier(),
putIndex(), etc.

Having set the layer name as our Target reference, we must putReference(), i.e., stick the AR within
a key of the d1 Descriptor. However, why is the key holding that AR equal to "null"? Hard to
understand. My educated guess is that in AM land, Targets can sometimes be implicit, hence the
"null" key. Kind of weak, right? Perhaps. The excellent news is that both "null" and "target" lead
to the same typeID:

s2t("null") === s2t("target"); // true!


Action Manager 152

Given that, you can put it this way: we’re storing the ActionReference into the "target" key of the
ActionDescriptor. I prefer to use that, over "null", because it makes the code more readable, and
meaningful. If ScriptListener uses "null", that’s its problem.
To sum up, in our example the "select" Action is performed on a Target specified through an
ActionReference. There’s also a "makeVisible" boolean set to false, even if in my case the layer
was actually visible – I can imagine this would force the visibility in case it’s invisible – and an
apparently optional ActionList that contains the "LayerID" integer (e.g., even if you comment it out,
the code keeps working). I suspect¹⁸ it’s information used to univocally define the Target in case
there are more layers with the same name string.
Another variation on the same theme – try to guess what this code does.

1 var d1 = new ActionDescriptor();


2 var r1 = new ActionReference();
3 r1.putClass( st2("marqueeRectTool") );
4 d1.putReference( s2t("target"), r1 );
5 d1.putBoolean( st2("dontRecord"), true );
6 d1.putBoolean( st2("forceNotify"), true );
7 executeAction( s2t("select"), d1, DialogModes.NO );

This AM selects the Rectangular Marquee Tool. Similarly, here the AR is used to define the class
of the Target, hence putClass(). "dontRecord" means “do not put this into the actions panel if the
actions panel is recording”, while "forceNotify" sends out the event to all listeners (you can safely
forget about both of them).
Another closely related example:

1 var d1 = new ActionDescriptor();


2 var r1 = new ActionReference();
3 r1.putEnumerated( s2t("channel"), s2t("channel"), s2t("blue") );
4 d1.putReference( s2t("target"), r1 );
5 executeAction( s2t("select"), d1, DialogModes.NO );

What does it do? Right, it selects the Blue channel. In this case, channels are enumerable¹⁹, the
ScriptListener uses putEnumerated(), passing "channel" as both the key and the enum type.
These simple examples have served the purpose of familiarizing with the ActionReference object,
and his role of Target’s information holder within a Descriptor. However, AR can do wonders when
you use it as a rod to fish for Descriptors in Photoshop’s pond.
¹⁸Don’t blame me if I use words such as “suspect”, “imagine”, or “guess” – speculation is the daily bread of the ActionManager developer.
¹⁹I fall short of answers to the question: why is a Channel enumerable hence putEnumerated(), while a Tool such as the Rectangular
Marquee is not, hence putClass()?
Action Manager 153

6.11 Inspecting Descriptors: executeActionGet


The new idea that I’d like to introduce you to, at this stage, is that the ActionManager system –
among the rest – keeps track of a large number of Descriptors keys relative to the status of:

• The Application (Photoshop)


• Documents
• Layers
• Channels
• Paths
• History
• ActionSets and Actions

What am I talking about? Photoshop can hand you juicy Descriptors of utterly meaningful objects
such as itself (the application object) and the other objects listed above, thanks to the recently
introduced ActionReference Class. You can cut them open with the skills you’ve perfected in the
Getting data section, and find all kind of goods in them.

Application

There’s a globally available method which returns an ActionDescriptor, and accepts as the only
parameter an ActionReference: executeActionGet(). Depending on how you construct the Refer-
ence, the returned Descriptor refers to one of the above-listed elements (Documents, Layers, etc.);
for instance, this is how you get perhaps the most important AD: the application’s.

1 var r1 = new ActionReference();


2 var d1 = new ActionDescriptor();
3 r1.putEnumerated( s2t("application"), s2t("ordinal"), s2t("targetEnum") );
4 d1 = executeActionGet(r1);

d1 now contains an amazing number of properties, such as the Brushes list, the Photoshop serial
string, Font names, recent files, display Preferences, you name it. Some of them are easy to grab,
others are deeply nested into Descriptors. As an alternative, which leads to the very same AD, it’s
possible to execute the "get" event.
Action Manager 154

1 var r1 = new ActionReference();


2 var d1 = new ActionDescriptor();
3 r1.putEnumerated( s2t("application"), s2t("ordinal"), s2t("targetEnum") );
4 d1.putReference(s2t("target"), r1);
5 var d2 = executeAction( s2t("get"), d1, DialogModes.NO );

Descriptor Inspectors

Whatever syntax you’ll use, such a jumbo Descriptor object would be almost impossible to manage
without a way to plot its data at once, and (even better) visualize its structure. Such tool is commonly
referred to as a “Descriptor Inspector” and some script developers over the years wrote their version;
what we used a few pages ago to tear apart the AD we built from scratch ourselves is a quite
rudimental AD Inspector too. I’m going to give you a few alternatives, then propose you a new
solution that has been recently introduced and is poorly documented. Mind you, in this case the
Reflection object is of no use – it returns only count and typename properties.
The first code comes originally from Mike Hale, that I’ve refactored a bit, also to support more
DescValueType (latest PS versions introduced LARGEINTEGERTYPE), and integrated with some code
from Tom Ruark.

1 function t2s(t) { return typeIDToStringID(t) }


2
3 // The actual Descriptor Inspector function
4 function checkDesc(desc) {
5 var c = desc.count,
6 str = '';
7 for (var i = 0; i < c; i++) {
8 str += 'Key ' + i + ' = ' + t2s(desc.getKey(i)) +
9 ': ' + desc.getType(desc.getKey(i)) +
10 ' = ' + getValues(desc, i) + '\n';
11 };
12 $.writeln(str);
13 };
14 // Utility function to extract the correct values from the keys
15 function getValues(desc, keyNum) {
16 var kTypeID = desc.getKey(keyNum);
17 switch (desc.getType(kTypeID)) {
18 case DescValueType.OBJECTTYPE:
19 return (desc.getObjectValue(kTypeID) +
20 "_" + t2s(desc.getObjectType(kTypeID)));
21 break;
22 case DescValueType.LISTTYPE:
Action Manager 155

23 return desc.getList(kTypeID);
24 break;
25 case DescValueType.REFERENCETYPE:
26 return desc.getReference(kTypeID);
27 break;
28 case DescValueType.BOOLEANTYPE:
29 return desc.getBoolean(kTypeID);
30 break;
31 case DescValueType.STRINGTYPE:
32 return desc.getString(kTypeID);
33 break;
34 case DescValueType.INTEGERTYPE:
35 return desc.getInteger(kTypeID);
36 break;
37 case DescValueType.LARGEINTEGERTYPE:
38 return desc.getLargeInteger(kTypeID);
39 break;
40 case DescValueType.DOUBLETYPE:
41 return desc.getDouble(kTypeID);
42 break;
43 case DescValueType.ALIASTYPE:
44 return desc.getPath(kTypeID);
45 break;
46 case DescValueType.CLASSTYPE:
47 return desc.getClass(kTypeID);
48 break;
49 case DescValueType.UNITDOUBLE:
50 return (desc.getUnitDoubleValue(kTypeID) +
51 "_" + t2s(desc.getUnitDoubleType(kTypeID)));
52 break;
53 case DescValueType.ENUMERATEDTYPE:
54 return (t2s(desc.getEnumerationValue(kTypeID)) +
55 "_" + t2s(desc.getEnumerationType(kTypeID)));
56 break;
57 case DescValueType.RAWTYPE:
58 var tempStr = desc.getData(kTypeID);
59 var rawData = new Array();
60 for (var tempi = 0; tempi < tempStr.length; tempi++) {
61 rawData[tempi] = tempStr.charCodeAt(tempi);
62 }
63 return rawData;
64 break;
65 default:
Action Manager 156

66 break;
67 };
68 };

As you see, checkDesc() uses the count prop to loop over the AD keys; then it logs their index, type,
and finally calls an utility function to output their values too. getValues() main purpose is to check
against DescValueType and use the appropriate getter. An example use of this would be:

1 var r1 = new ActionReference();


2 var d1 = new ActionDescriptor();
3 r1.putEnumerated( s2t("application"),
4 s2t("ordinal"),
5 s2t("targetEnum") );
6 d1 = executeActionGet(r1);
7 checkDesc(d1);
8 // Key 0 = rulerUnits: DescValueType.ENUMERATEDTYPE = rulerPixels_rulerUnits
9 // Key 1 = exactPoints: DescValueType.BOOLEANTYPE = false
10 // Key 2 = numberOfCacheLevels: DescValueType.INTEGERTYPE = 2
11 // Key 3 = numberOfCacheLevels64: DescValueType.INTEGERTYPE = 2
12 // ...
13 // Key 91 = documentArea: DescValueType.OBJECTTYPE = [ActionDescriptor]_null
14 // Key 92 = localeInfo: DescValueType.OBJECTTYPE = [ActionDescriptor]_null

Which is great. Yet, if you run the code yourself, you’ll notice that
the keys representation is flat: for instance, if you look at key #90
–MRUColorList, whatever it means – the logged type is LISTTYPE
(an ActionList) but the actual value is just the "[ActionList]"
string. If you want to inspect that AL further, then you need to
extract it and manually look inside.
A variation on this theme, but with built-in nested objects scan, is
by the developer Matias Kiviniemi and can be found in this forum
post.
A different kind of inspector is by xbytor, who is the author of a
milestone set of freely available libraries and tools called xtools,
that is widely known in the Photoshop Scripting community
as immensely useful, to say the least. The Getter.jsx is the
library that you want to check out. Please note that it has several
dependencies: you can either download the entire xtools package
and run it from its location so that all the #include directives are properly evaluated, or choose the
+22K likes version in the /app folder.
Action Manager 157

In addition, xbytor built a tool around his Getter which is called GetterDemo.jsx and provides you
with a handy GUI in which you can select the kind of descriptor you’re interested in (Application,
Actions, etc.): the result of the inspection is saved on your Desktop as a Getter.xml file.
With GetterDemo.jsx you get a complete picture of all nested descriptors; for instance, I read in
the xml log that the application Descriptor contains a "presetManager" ActionList, which in turn
contains a "Brush" ActionDescriptor, which in turn holds a "Name" ActionList, that eventually
provides you with several Strings.

1 <?xml version="1.0" encoding="iso-8859-1" ?>


2 <PhotoshopInfo>
3 <ActionDescriptor count="93">
4 <Enumerated symname="RulerUnits" sym="RlrU" enumeratedTypeString="RulerUnits"
5 enumeratedType="RlrU" enumeratedValueString="RulerPixels"
6 enumeratedValue="RrPx"/>
7 <Boolean symname="ExactPoints" sym="ExcP" boolean="false"/>
8 <Integer symname="NumberOfCacheLevels" sym="NCch" integer="2"/>
9 <Integer symname="NumberOfCacheLevels64" sym="NC64" integer="2"/>
10 <Boolean symname="UseCacheForHistograms" sym="UsCc" boolean="false"/>
11 <!-- etc. -->

A third option is made by the Corsican developer Michel Mariani, as a part of his own JSON Action
Manager library. Have a look at his Get Application Info Code; it returns a JSON format of the
application Descriptor, that can be either visualized or saved on disk.

1 {
2 "rulerUnits":
3 {
4 "<enumerated>": {
5 "rulerUnits": "rulerPixels"
6 }
7 },
8 "exactPoints": {
9 "<boolean>": false
10 },
11 "numberOfCacheLevels": {
12 "<integer>": 2
13 },
14 "numberOfCacheLevels64": {
15 "<integer>": 2
16 },
17 "useCacheForHistograms": {
18 "<boolean>": false
19 }, // etc. etc.
Action Manager 158

The Czech developer Jaroslav Bereza has shared a very appropriately named ActionManagerHu-
manizer, find below as an example of the output, the result for the "textKey" layer property.

1 ({
2 _obj: "object",
3 textKey: {
4 _obj: "textLayer",
5 antiAlias: {
6 _enum: "antiAliasType",
7 _value: "antiAliasSharp"
8 },
9 boundingBox: {
10 _obj: "boundingBox",
11 bottom: {
12 _unit: "pixelsUnit",
13 _value: 9.87586975097656
14 },
15 left: {
16 _unit: "pixelsUnit",
17 _value: 1
18 },
19 right: {
20 _unit: "pixelsUnit",
21 _value: 91.3291778564453
22 },
23 // ...

Tom Ruark himself has a GitHub repository, where he pushed a remarkable version of a Descriptor
Inspector – see Getter.jsx.
Among the variety of methods available, I’ve found particularly handy to use a very little
documented option. How do I know it then, you might ask – and it’s a fair question. As an HTML
Panels developer too, each time that a new version of Photoshop is released, I’ve the habit of peeping
into the Photoshop folders looking for the various Panels that are bundled with the application.
Apparently, several new features that PS sports are put out to contract, so to speak, to HTML Panels.
Examples are the “New File” dialog, the “Welcome” application frame with Recent Files, or the brand
new Search dialog. Within these internally used Panels a lurker might find hidden gems, such as the
following:
Action Manager 159

1 var r1 = new ActionReference();


2 var d1 = new ActionDescriptor();
3 r1.putEnumerated( s2t("application"),
4 s2t("ordinal"),
5 s2t("targetEnum") );
6 d1 = executeActionGet(r1);
7 // So far so good
8 var convertDesc = new ActionDescriptor();
9 convertDesc.putObject( s2t("object"), s2t("object"), d1 );
10 var jsonDesc = executeAction( s2t("convertJSONdescriptor"),
11 convertDesc, DialogModes.NO );
12 jsonDesc.getString( s2t("json") );

The result is a long JSON string, which gives you a nice detailed vision of what the Descriptor looks
like, nested objects included – even if the props seem to be quite scrambled, compared to the other
methods:

1 {
2 "$PnCK": {
3 "_enum": "cursorKind",
4 "_value": "brushSize"
5 },
6 "MRUColorList": [{
7 "_obj": "RGBColor",
8 "blue": 46,
9 "grain": 46,
10 "red": 46
11 }, {
12 "_obj": "CMYKColorClass",
13 "black": 0,
14 "cyan": 72,
15 "magenta": 0,
16 "yellowColor": 0
17 }, {
18 "_obj": "RGBColor",
19 "blue": 50,
20 "grain": 50,
21 "red": 50
22 }, // etc...

Lastly, another breed of Inspector code (this time coming from the Adobe Generator project), can
be found buried in the /connectionsdk/samples/mac/networkclientprototype/SampleJSX folder of
the Photoshop SDK. Mind you, it works only for inspecting "document" and "layer".
Action Manager 160

1 var d1 = new ActionDescriptor();


2 var r1 = new ActionReference();
3 r1.putProperty( s2t("property" ), s2t("json") );
4 r1.putEnumerated( s2t( "document"),
5 s2t( "ordinal" ),
6 s2t( "targetEnum" ));
7 d1.putReference( s2t( "target" ), r1 );
8
9 // Filter params, otherwise it uses default settings
10 // d1.putBoolean( s2t("compInfo"), true );
11 // d1.putBoolean( s2t("imageInfo"), true );
12 // d1.putBoolean( s2t("layerInfo"), true );
13 // d1.putBoolean( s2t("expandSmartObjects"), true );
14 // d1.putBoolean( s2t("getTextStyles"), true );
15 // d1.putBoolean( s2t("getFullTextStyles"), true );
16 // d1.putBoolean( s2t("selectedLayers"), true );
17 // d1.putBoolean( s2t("getDefaultLayerFX"), true );
18 // d1.putBoolean( s2t("getPathData"), true );
19
20 executeAction(s2t("get"), d1, DialogModes.NO).getString(s2t("json"));
21
22 // {
23 // "bounds": {
24 // "bottom": 760,
25 // "left": 0,
26 // "right": 1830,
27 // "top": 0
28 // },
29 // "count": 257,
30 // "depth": 8,
31 // "file": "/Users/davidebarranca/Desktop/Girl.jpg",
32 // "generatorSettings": false,
33 // "globalLight": {
34 // "altitude": 30,
35 // "angle": 30
36 // },
37 // "id": 707,
38 // "layers": [
39 // { // ...

With these tools ready, inspecting ActionDescriptor to retrieve all kind of information is going to be
if not easy, at least easier than it appeared before; let’s try getting a couple of otherwise impossible
properties.
Action Manager 161

Say that for some reason you want to know the kind of Color Sampler size the user has set
(1px, 3x3px, 5x5px, etc.). You run a Descriptor Inspector on the application object and you find
an "eyeDropperSample" enumerable, type "eyeDropperSampleType" which current value on my
Photoshop happens to be "sample5x5".

1 {
2 "eyeDropperSample": {
3 "_enum": "eyeDropperSampleType",
4 "_value": "sample5x5"
5 }, // ...
6 }

This is what we were looking for, but of course, you can’t use this information as is: it must be
implemented in your code to check that at runtime. How? You retrieve the Descriptor, then extract
the bit of interest, as you’ve done previously. Please note that from now on I’ll assume in the code
the existence of s2t(), t2s() and similar functions.

1 var r1 = new ActionReference();


2 var d1 = new ActionDescriptor();
3 r1.putEnumerated( s2t("application"),
4 s2t("ordinal"),
5 s2t("targetEnum") );
6 d1 = executeActionGet(r1);
7 t2s(d1.getEnumerationValue(s2t("eyeDropperSample"))); // sample5x5

Let’s review that code: we’re getting the application Descriptor, stored in d1. "eyeDropperSample"
is found unpacked (not nested inside other objects), and it turns out to be an enumerated value, so we
need to use the getEnumerationValue() method. In turn, getEnumerationValue() requires as the pa-
rameter the Descriptor’s key, that stores the enumerated: in our case it’s s2t("eyeDropperSample").
The returned value (i.e., what’s stored in the "eyeDropperSample" key) is a typeID, so the need to
use t2s(), to convert it back into a human-readable form. The result is the "sample5x5" string. By
Jove, we got it!
Action Manager 162

Get only one property at a time!


Retrieving the entire application Descriptor is an intensive task; unless you need to check
several of its elements, it certainly is a better idea to get only the property of your
interest. Compare the previous "eyeDropperSample" code with the one that follows, and
when possible use the latter one:

1 var r1 = new ActionReference();


2 var d1 = new ActionDescriptor();
3 r1.putProperty( s2t("property"), s2t("eyeDropperSample"));
4 r1.putEnumerated( s2t("application"),
5 s2t("ordinal"),
6 s2t("targetEnum") );
7 d1 = executeActionGet(r1);
8 // d1.count; // 1
9 t2s(d1.getEnumerationValue(s2t("eyeDropperSample")));

If you check the count property, it’s just 1 – i.e. you’ve got only the "eyeDropperSample"
key²⁰ – much faster, and resources-savvy. Please note that either you get the entire
Descriptor, or a single property only: I haven’t find the way to add a second one. If you’re
interested in few properties, it might still be faster to request them one by one, instead of the
entire descriptor. The order matters: putProperty() must be placed before putEnumerated()
otherwise an error is thrown. As a last remark, the above holds true not only for the
"application", but for all objects you can extract Descriptors of.

As a slightly more complex example, let’s find the "kuiBrightnessLevel" key value: a property
linked to the Photoshop GUI brightness level, as you’d find in the "Photoshop CC > Preferences"
menu, Interface tab, Appearance section: in other words, whether the Interface should be black, dark
gray, mid-gray or light gray.

As I get from a Descriptor Inspector output, that key is nested inside the "interfacePrefs" key,
which is an ActionDescriptor itself:

²⁰For some reason, "eyeDropperSample" is an enumerated value for sizes up to 5x5, then it becomes an integer, so mind your method.
Action Manager 163

1 {
2 "interfacePrefs": {
3 "_obj": "interfacePrefs",
4 // ...
5 "kuiBrightnessLevel": {
6 "_enum": "uiBrightnessLevelEnumType",
7 "_value": "kPanelBrightnessMediumGray"
8 }, // ...
9 }
10 }

The strategy here is to get from the application object the value of the "interfacePrefs" key (an
AD), then query it for the "kuiBrightnessLevel" enum value.

1 var r1 = new ActionReference();


2 var d1 = new ActionDescriptor();
3 r1.putProperty( s2t("property"), s2t("interfacePrefs"));
4 r1.putEnumerated( s2t("application"),
5 s2t("ordinal"),
6 s2t("targetEnum") );
7 d1 = executeActionGet(r1);
8 var interfaceDesc = d1.getObjectValue(s2t("interfacePrefs"))
9 t2s(interfaceDesc.getEnumerationValue(s2t("kuiBrightnessLevel")));
10 // either: kPanelBrightnessOriginal, kPanelBrightnessLightGray,
11 // kPanelBrightnessMediumGray, kPanelBrightnessDarkGray

Documents

Earlier in this section I’ve promised you more goods, e.g. the Layer’s Descriptor. As you may now
guess, it’s just a matter of passing the correct ActionReference:

1 var r1 = new ActionReference();


2 var d1 = new ActionDescriptor();
3 r1.putEnumerated( s2t("document"),
4 s2t("ordinal"),
5 s2t("targetEnum") );
6 d1 = executeActionGet(r1);

As a result, here is a chunk of the received Descriptor:


Action Manager 164

1 {
2 //...
3 "copyright": false,
4 "count": 2,
5 "depth": 8,
6 "documentID": 682,
7 "fileInfo": {
8 "_obj": "fileInfo"
9 },
10 "fileReference": {
11 "_path": "/Users/davidebarranca/Desktop/Girl.jpg"
12 },
13 "format": "JPEG",
14 "guidesVisibility": true,
15 "hasBackgroundLayer": true,
16 "height": {
17 "_unit": "distanceUnit",
18 "_value": 602
19 },
20 // ...

While there’s only one application, there may be several Documents: which one is the above code
referring to? The answer is, as you may guess, the currently active one. But wait… The ActionRefer-
ence purpose is, by definition, to define a Target path through the Photoshop containment hierarchy,
isn’t it? So, why can’t AR point to a different Target, rather than defaulting to the active layer?
In fact, this is possible, sharp reader. Among the various ActionReference methods, there are
three promising ones, namely putIdentifier() (Identifier is a synonym of ID), putName(), and
putIndex(), that I’ll test having a couple of documents open in Photoshop: Girl.jpg and for the
parity of the sexes²¹, Boy.jpg.

1 var r1 = new ActionReference();


2 var d1 = new ActionDescriptor();
3 // AR points to the Document, by Name
4 r1.putName( s2t("document"), "Boy.jpg");
5 d1 = executeActionGet(r1);
6 // Let's check...
7 d1.getString(s2t("title")); // Boy.jpg!

Let’s try getting by ID because each open document has one (as you’ve seen above in the Descriptor’s
JSON); I’ll use the logged 682 in conjunction with the putIdentifier() method.
²¹Some would say that at least 4 extra documents are required to account for the possible sexes in the human species, not to mention the
multitude of acknowledged genders; for simplicity’s sake let’s stick with two, OK?
Action Manager 165

1 var r1 = new ActionReference();


2 var d1 = new ActionDescriptor();
3 // AR points to the Document, by ID
4 r1.putIdentifier( s2t("document"), 682);
5 d1 = executeActionGet(r1);
6 // Let's check...
7 d1.getString(s2t("title"));
8 // Girl.jpg!

Now the third of the mentioned ActionReference methods: putIndex(). Indexes, in this case, are not
zero-based, so the first document you’ve opened (the oldest) has an index equal to one, the second
is two, etc.

1 var r1 = new ActionReference();


2 var d1 = new ActionDescriptor();
3 // AR points to the Document, by index
4 r1.putIndex( s2t("document"), 1);
5 d1 = executeActionGet(r1);
6 // Let's check...
7 d1.getString(s2t("title"));
8 // Girl.jpg!

Guess what, if you try to get a document’s AD by index using zero, an error is thrown. Similarly to
what I’ve suggested for the application descriptor, you’re allowed, and encouraged, to retrieve the
property of interest only, and not the entire document’s AD.

1 var r1 = new ActionReference();


2 var d1 = new ActionDescriptor();
3 r1.putProperty(s2t("property"), s2t("depth"));
4 r1.putName( s2t("document"), "Girl.jpg" );
5 d1 = executeActionGet(r1);
6 d1.count; // 1
7 d1.getInteger(s2t("depth")); // 8

Layers

You may start to see the pattern here. Closely related to Documents, also the Layer ActionDescriptor
can be retrieved. As follows the generic code to get the currently active Layer.
Action Manager 166

1 var r1 = new ActionReference();


2 var d1 = new ActionDescriptor();
3 // AR points to the Active Layer
4 r1.putEnumerated( s2t("layer"),
5 s2t("ordinal"),
6 s2t("targetEnum") );
7 d1 = executeActionGet(r1);

The logged Descriptor has its specific keys.

1 {
2 "_obj": "object",
3 "background": true,
4 "bounds": {
5 "_obj": "rectangle",
6 "bottom": {
7 "_unit": "pixelsUnit",
8 "_value": 652
9 },
10 "left": {
11 "_unit": "pixelsUnit",
12 "_value": 0
13 },
14 "right": {
15 "_unit": "pixelsUnit",
16 "_value": 1024
17 }, // ...

In case there’s no active layer (e.g., when you click in a blank area
of the Layers palette, and no layer is highlighted), ActionManager
targets the upper-most one.
Here with Layers as well, the ActionReference can point to a
specific Target Layer, with the same putIdentifier(), putName(),
and putIndex() methods – with one, important caveat – as you
can see using a test document such as the one which Layers palette
is on the left.
The starting point is the Document shown here at the right, with
three layers, none of which happens to be active.
First, let’s get the "Background" layer by name, hence the
putName() method.
Action Manager 167

1 var r1 = new ActionReference();


2 var d1 = new ActionDescriptor();
3 // AR points to the Layer, by Name
4 r1.putName( s2t("layer"), "Background");
5 d1 = executeActionGet(r1);
6 // Let's check...
7 d1.getString(s2t("name")); // Background!

Second experiment, the "Flipped" layer by ID; here, I’m getting the ID first, and I’m using it in the
putIdentifier() method.

1 app.activeDocument.layers.getByName("Flipped").id; // 2
2 var r1 = new ActionReference();
3 var d1 = new ActionDescriptor();
4 // AR points to the Layer, by ID
5 r1.putIdentifier( s2t("layer"), 2);
6 d1 = executeActionGet(r1);
7 // Let's check...
8 d1.getString(s2t("name")); // Flipped!

Lastly, using putIndex(); as you remember, the topmost layer has an index equal to zero, so I would
expect to get the "Vignetted" layer Descriptor. However, I’m going to be disappointed.

Similarly to what you’ve seen with documents, indexes in the DOM kingdom as opposed to the
ActionManager land work very differently. Topmost layer always has index zero using DOM
Scripting, while the bottom layer has an index equal to the number of layers in the stack, minus
one – for indexes are zero-based. ActionManager works in reverse, so the bottom layer has index
zero, the topmost is equal to the number of layers minus one (see the screenshot A).
Except when there’s no Background layer (screenshot B, where I’ve double clicked on Background
and accepted the proposed layer name to unlock it). In this case, indexes in ActionManager are no
more zero-based, but one-based: as a result, the topmost has an index equal to three. You’ll get more
information about layers and AM later on, I’ve just had to justify the code to get the "Vignetted"
layer’s AD by index, which is finally as follows.
Action Manager 168

1 var r1 = new ActionReference();


2 var d1 = new ActionDescriptor();
3 // AR points to the Layer, by index
4 r1.putIndex( s2t("layer"), 2); // mind the index
5 d1 = executeActionGet(r1);
6 // Let's check...
7 d1.getString(s2t("name")); // Vignetted!

Since an error is thrown when trying to get a layer’s AD by index using zero, when there’s no
Background layer, and also when accessing the layer’s backgroundLayer property, some developers
use the following pattern:

1 try {
2 app.activeDocument.backgroundLayer;
3 var AMindex = 0; // it has a Background Layer
4 } catch(e) {
5 // no Background layer, the error is caught here
6 var AMindex = 1;
7 };

At this stage it might be redundant, but I’d like to stress again that you can get just one key of
the entire "layer" Descriptor, as you’ve done with the "application" and the "document". In the
following snippet, I’m getting the Layer Color tag only (I’ve previously set the tag to Orange):

1 var r1 = new ActionReference();


2 var d1 = new ActionDescriptor();
3 r1.putProperty( s2t("property"), s2t("color"));
4 r1.putName( s2t("layer"), "Flipped");
5 d1 = executeActionGet(r1);
6 // Let's check...
7 t2s(d1.getEnumerationValue(s2t("color"))); // orange

But there’s more! Since Layers, in the Photoshop containment hierarchy, are children of the
Document element, can I build the ActionReference so that it targets a specific layer, of a specific
Document (as opposed as the active one)? You can bet it. Using the Girl.jpg and Boy.jpg documents
I had earlier, we can write:
Action Manager 169

1 var r1 = new ActionReference();


2 var d1 = new ActionDescriptor();
3 // Let's be savvy and get a single key only
4 r1.putProperty( s2t("property"), s2t("color"));
5 // AR points to the Layer by index...
6 r1.putIndex( s2t("layer"), 1 );
7 // ... and to the Document by name
8 r1.putName( s2t("document"), "Girl.jpg" );
9 d1 = executeActionGet(r1);
10 // Let's check...
11 t2s(d1.getEnumerationValue(s2t("color"))); // orange

Please note that the order with which you’re targeting the key matters, and is child-to-parent: here’s
first the "property", then the "layer", and finally the "document". Of course, you can mix the way
you target each element: say, all by name, one by ID, and another by index, etc.

Traversing Layers

Speaking of Layers, since their ActionManager representation has a flat hierarchy, by definition it
gets rid of the complication brought by LayerSets and nested content: as an example, the following
code iterates through all the layers and runs a function on each one of them.

1 // Traverse a Document and apply a function to all the Layers,


2 // Background and LayerSets included
3 // Accepts a function as a parameter
4 function traverseLayers (fun) {
5
6 // Internal utility function: select a layer based on its ID
7 function selectLayerByID(id) {
8 var r = new ActionReference();
9 var d = new ActionDescriptor();
10 r.putIdentifier(s2t('layer'), id);
11 d.putReference(s2t('target'), r);
12 d.putBoolean(s2t('makeVisible'), false)
13 executeAction(s2t('select'), d, DialogModes.NO);
14 }
15
16 // Find the number of Layers in the Document
17 // Mind you: Background layer doesn't count,
18 // LayerSets count for two (the start and end pointer)
19 var r = new ActionReference();
20 r.putEnumerated(s2t('document'), s2t('ordinal'), s2t('targetEnum'));
21 var layerCount = executeActionGet(r).getInteger(s2t('numberOfLayers'));
Action Manager 170

22
23 // Traversing the layers backwards so that top ones are processed first
24 for (var i = layerCount; i >= 1; i--) {
25 // I'm allowed to re-assign the r var here
26 var r = new ActionReference();
27 r.putIndex(s2t('layer'), i);
28 // Descriptor of the Layer with Index = i
29 var d = executeActionGet(r);
30 // Getting the ID of the i-th Layer
31 var layerID = d.getInteger(s2t('layerID'));
32 // Process only if it's not the AM Layer that "closes" a LayerSet
33 if ('layerSectionEnd' != t2s(d.getEnumerationValue(s2t('layerSection')))) {
34 selectLayerByID(layerID);
35 fun(d); // passing the descriptor, in case it'll be useful
36 }
37 }
38 // run also on the background layer, if present
39 try {
40 app.activeDocument.activeLayer = app.activeDocument.backgroundLayer;
41 fun(d);
42 } catch(e) { /* accessing the background if not preset would throw an error */ }
43
44 }

If you want to follow the order with which layers are processed, try this variation, that renames
them according to a global counter:

1 var idx = 0;
2 var ren = function() { app.activeDocument.activeLayer.name = idx++ };
3 traverseLayers(ren);

You’ll see that layers are going to be renamed from top to bottom in ascending order (0, 1, 2, etc.).
Another slightly different approach to traverse Layers, this time filtering out LayerSets, uses a
forward loop capped differently based on the presence/absence of a Background Layer.
Action Manager 171

1 var r = new ActionReference();


2 r.putEnumerated(s2t('document'), s2t('ordinal'), s2t('targetEnum'));
3 // Find the number of Layers in the Document
4 // Mind you: Background layer doesn't count, LayerSets count for two
5 var layerCount = executeActionGet(r).getInteger(s2t('numberOfLayers'));
6 var layersInfo = [],
7 i = 0;
8 try {
9 app.activeDocument.backgroundLayer;
10 } catch(e) {
11 // if the Document hasn't a Background layer, an error is thrown
12 // and the i counter is added 1
13 i++;
14 }
15
16 for (i; i <= layerCount; i++) {
17 var r = new ActionReference();
18 r.putIndex(s2t('layer'), i);
19 // Descriptor of the Layer with Index = i
20 var d = executeActionGet(r);
21 // Getting a bunch of information
22 var layerName = d.getString(s2t('name')),
23 id = d.getInteger(s2t('layerID')),
24 layerType = t2s(d.getEnumerationValue(s2t('layerSection'))),
25 isLayerSet = (layerType == 'layerSectionContent') ? false : true;
26 // Pushing them in the layersInfo array
27 layersInfo.push({
28 layerName : layerName,
29 id : id,
30 layerSet : isLayerSet
31 })
32 }
33 // Logging the array
34 $.writeln(layersInfo.toSource());

If you take as an example the Layers Palette of the last screenshot, you can discern the two cases:

• when the Background layer is present, the layerCount is 2 (for the Background is ignored when
getting 'numberOfLayers'); the i counter is set to zero (line 10) and the loop goes from zero to
two, hence 3 values with the correct AM indexes: 0, 1, 2.
• when the Background layer is absent, the layerCount is 3; the i counter, which was originally
zero, is added one in the catch (line 16) that result from the error that is thrown when trying
Action Manager 172

to execute activeDocument.backgroundLayer; the loop this time goes from one to three, hence
three values with the correct AM indexes: 1, 2, 3.

Channels

The plot repeats: that’s very good because Dopamine is released, and our brain is happy.

1 var r1 = new ActionReference();


2 var d1 = new ActionDescriptor();
3 // AR points to the Active Channel
4 r1.putEnumerated( s2t("channel"),
5 s2t("ordinal"),
6 s2t("targetEnum") );
7 d1 = executeActionGet(r1);

The active Channel, most of the times, is the RGB composite, and the Descriptor isn’t as rich as the
Layer’s.

1 {
2 "_obj": "object",
3 "channelName": "RGB",
4 "count": 3,
5 "visible": true,
6 "histogram": [
7 0,
8 1,
9 2, // ...

Apparently, count is the number of the available Channels, except for the RGB composite. A sudden
break in the Dopamine flow (it’s been a fleeting pleasure, as it may happen), things are peculiar when
it comes to getting them by name or index. If the Channel’s Descriptor you want to get is among
the default set – R, G, and B for RGB, or C, M, Y and K for CMYK – you need to use putEnumerated().

1 var r1 = new ActionReference();


2 var d1 = new ActionDescriptor();
3 r1.putEnumerated( s2t("channel"),
4 s2t("channel"),
5 s2t("red") ); // or "RGB", "green", "blue"
6 d1 = executeActionGet(r1);

Conversely, you’re allowed to get a Channel by name if it’s an alpha channel:


Action Manager 173

1 var r1 = new ActionReference();


2 var d1 = new ActionDescriptor();
3 // AR points to the Active Channel
4 r1.putName( s2t("channel"), "Blue copy");
5 d1 = executeActionGet(r1);

Indexes are available, although they’re not zero-based (one is for R, two is for G, etc. with no
composite).

1 var r1 = new ActionReference();


2 var d1 = new ActionDescriptor();
3 // AR points to the Active Channel
4 r1.putIndex( s2t("channel"), 3); // blue
5 d1 = executeActionGet(r1);

Paths
Getting a Path descriptor when a path is selected is a piece of cake for you now.

1 var r1 = new ActionReference();


2 var d1 = new ActionDescriptor();
3 r1.putEnumerated( s2t("path"),
4 s2t("ordinal"),
5 s2t("targetEnum") );
6 d1 = executeActionGet(r1);
7 // {
8 // "ID": 977,
9 // "_obj": "object",
10 // "count": 2,
11 // "itemIndex": 1,
12 // "kind": {
13 // "_enum": "pathKind",
14 // "_value": "normalPath"
15 // },
16 // "pathContents": {
17 // "_obj": "pathClass",
18 // "defaultFill": true,
19 // "pathComponents": [
20 // {
21 // "_obj": "pathComponent",
22 // "shapeOperation": { ...

Fact is that paths are more likely to be not selected than selected, so you need to get them either by
name:
Action Manager 174

1 var r1 = new ActionReference();


2 var d1 = new ActionDescriptor();
3 r1.putName( s2t("path"), "Path 1");
4 d1 = executeActionGet(r1);

Or by index (not zero based):

1 var r1 = new ActionReference();


2 var d1 = new ActionDescriptor();
3 r1.putIndex( s2t("path"), 1);
4 d1 = executeActionGet(r1);

And of course by ID:

1 var r1 = new ActionReference();


2 var d1 = new ActionDescriptor();
3 r1.putIdentifier( s2t("path"), 977);
4 d1 = executeActionGet(r1);

Lastly, the so-called “Work Path” Descriptor can be retrieved using a single putProperty() call.

1 var r1 = new ActionReference();


2 var d1 = new ActionDescriptor();
3 r1.putProperty( s2t("path"), s2t("workPath"));
4 d1 = executeActionGet(r1);

History

The History AD is retrieved with the usual putEnumerated().

1 var r1 = new ActionReference();


2 var d1 = new ActionDescriptor();
3 r1.putEnumerated( s2t("historyState"), // or "snapshotClass"
4 s2t("ordinal"),
5 s2t("targetEnum") );
6 d1 = executeActionGet(r1);
7 // {
8 // "ID": 997,
9 // "_obj": "object",
10 // "auto": false,
11 // "count": 32,
Action Manager 175

12 // "currentHistoryState": true,
13 // "historyBrushSource": false,
14 // "itemIndex": 2,
15 // "name": "Snapshot 1"
16 // }

Which is apparently equivalent to:

1 var r1 = new ActionReference();


2 var d1 = new ActionDescriptor();
3 r1.putProperty( s2t("historyState"), s2t("currentHistoryState"));
4 d1 = executeActionGet(r1);

As you see, I’m spending less time on these last Descriptors because the concepts you’ve seen in the
first ones still apply here.

ActionSets and Actions

The last elements that this long ride of AD getters covers are somehow peculiar. You can’t get an
ActionSet Descriptor with the expected putEnumerated(), but names are fine.

1 var r1 = new ActionReference();


2 var d1 = new ActionDescriptor();
3 r1.putName( s2t("actionSet"), "Margulis PPW Actions v 4.3");
4 d1 = executeActionGet(r1);
5 // {
6 // "_obj": "object",
7 // "count": 6,
8 // "itemIndex": 1,
9 // "name": "Margulis PPW Actions v 4.3",
10 // "numberOfChildren": 17
11 // }

When you don’t know names, indexes work too: but there’s really no way, to the best of my
knowledge, to know in advance how many ActionSets are available (no count property). As a result,
it’s a common practice to loop and use a try/catch block and keep iterating until it breaks.
Action Manager 176

1 function logDescriptor (ad) {


2 var convertDesc = new ActionDescriptor();
3 convertDesc.putObject( s2t("object"), s2t("object"),ad );
4 var jsonDesc = executeAction( s2t("convertJSONdescriptor"),
5 convertDesc, DialogModes.NO );
6 $.writeln(jsonDesc.getString( s2t("json")));
7 }
8 var i = 1, r1, d1;
9 while (true) {
10 r1 = new ActionReference();
11 d1 = new ActionDescriptor();
12 r1.putIndex( s2t("actionSet"), i );
13 try {
14 d1 = executeActionGet(r1);
15 logDescriptor(d1); // or do whatever you need to do
16 } catch(e) {
17 break;
18 }
19 i++;
20 }
21 $.writeln("Found " + i + " ActionSet" + ((i > 1) ? "s" : ""));

As you’ve seen in the ActionSet’s AD log, there’s a promising "numberOfChildren" property, that
you can use to loop and dig further to reach the Actions’ Descriptors finally. But first, let’s say that
you want to get Actions of a particular ActionSet (that you know either by name or index).

1 var r1 = new ActionReference();


2 var d1 = new ActionDescriptor();
3 r1.putIndex(s2t("action"), 2); // index are as usual one-based
4 r1.putName( s2t("actionSet"), "Margulis PPW Actions v 4.3");
5 d1 = executeActionGet(r1);
6 // { "_obj": "object",
7 // "count": 16,
8 // "itemIndex": 2,
9 // "name": "Shadows/Highlights and OK",
10 // "numberOfChildren": 1,
11 // "parentIndex": 1,
12 // "parentName": "Margulis PPW Actions v 4.3" }

Similarly to what we did when requesting a property only from a Descriptor, order matters: you
need to go from child to parent, so the "action" first, then the "actionSet". Looping through all
Actions is just a matter of getting the ActionSet’s "numberOfChildren" integer first.
Action Manager 177

1 var r1 = new ActionReference();


2 r1.putName( s2t("actionSet"), "Margulis PPW Actions v 4.3");
3 var len = executeActionGet(r1).getInteger(s2t("numberOfChildren"));
4 for (var j = 1; j <= len; j++) {
5 var r2 = new ActionReference();
6 var d1 = new ActionDescriptor();
7 r2.putIndex(s2t("action"), j);
8 r2.putName( s2t("actionSet"), "Margulis PPW Actions v 4.3");
9 d1 = executeActionGet(r2);
10 logDescriptor(d1); // or do whatever you need to do
11 }

Now you can combine both loops, and build your all-ActionSets-all-Actions Descriptors getter,
which I leave you as an exercise. As a side note, I’ve been assigning the d1 variable to a new
ActionDescriptor() instance throughout all these snippets just for clarity’s sake, but it’s not strictly
required in JavaScript.

1 // ...
2 var r2 = new ActionReference();
3 r2.putIndex(s2t("action"), j);
4 r2.putName( s2t("actionSet"), "Margulis PPW Actions v 4.3");
5 var d1 = executeActionGet(r2);
6 // ...

6.12 Getting Descriptors as Streams


If you happen to probe Adjustment Layers such as Curves, Channel Mixer, Color Balance, etc. with
one of the Descriptor inspectors techniques I’ve covered, you’ll find it quite hard to get meaningful
information about the adjustment status – i.e. the current settings of it: the numeric values of, say,
a Hue/Saturation layer.
Let’s use as an example a Black & White adjustment with default settings.
Action Manager 178

1 var r1 = new ActionReference();


2 var d1 = new ActionDescriptor();
3 r1.putProperty(s2t("property"), s2t("adjustment"));
4 r1.putEnumerated( s2t("layer"),
5 s2t("ordinal"),
6 s2t("targetEnum") );
7 d1 = executeActionGet(r1);
8
9 // {
10 // "_obj": "object",
11 // "adjustment": [
12 // {
13 // "_obj": "blackAndWhite"
14 // }
15 // ]
16 // }

The json getter seems unable to log the content of the "adjustment" ActionList though, so let’s
switch back to a full manual approach – a good chance to revise your skills.

1 // Let's be sure there's only an ActionList in there...


2 d1.count; // 1
3 // ... that the key is "adjustment"...
4 t2s(d1.getKey(0)); // "adjustment"
5 // ... and we're talking about an ActionList
6 d1.getType(s2t("adjustment")); // DescValueType.LISTTYPE
7 // Getting the ActionList
8 var l1= d1.getList(s2t("adjustment"));
9 // What's in there?
10 l1.count; // 1
11 l1.getType(0); // DescValueType.OBJECTTYPE
12 t2s(l1.getClass(0)); // "blackAndWhite"
13 // same as:
14 // t2s(l1.getObjectType(0)); // "blackAndWhite"
15 // Get the ActionDescriptor
16 var d2 = l1.getObjectValue(0);
17 // What's in there?
18 d2.count; // 1
19 t2s(d2.getKey(0)); // legacyContentData
20 d2.getType(s2t("legacyContentData")); // DescValueType.RAWTYPE
21 var rawData = d2.getData(s2t("legacyContentData"));

At this point we can state that the "adjustment" key of the Layer Descriptor contains an ActionList;
Action Manager 179

which in turn contains a single ActionDescriptor of class “blackAndWhite”; which eventually


contains a single key "legacyContentData" of type DescValueType.RAWTYPE. By the way, you could
use a more compact command:

1 var rawData = executeActionGet(r1)


2 .getList(s2t("adjustment"))
3 .getObjectValue(0)
4 .getData(s2t("legacyContentData"));

So, what’s that raw data thing? Good question. The answer seems to be: “it depends”. If you’re lucky,
it’s just a stream of raw data that you can turn into a Descriptor, using the appropriate fromStream()
method.

1 var d3 = new ActionDescriptor();


2 d3.fromStream(rawData);

The information we’re interested into is finally packed into the d3 Descriptor:

1 {
2 "_obj": "object",
3 "blackAndWhitePresetFileName": "",
4 "blue": 20,
5 "bwPresetKind": 1,
6 "cyan": 60,
7 "grain": 40,
8 "magenta": 80,
9 "red": 40,
10 "tintColor": {
11 "_obj": "RGBColor",
12 "blue": 179.001,
13 "grain": 211.001,
14 "red": 225
15 },
16 "useTint": false,
17 "yellow": 60
18 }

Other circumstances are far less fortunate, i.e., when a Descriptor can’t be created straight away
from raw data. We can try writing the rawData on disk for further inspection with a Hex Viewer.
Action Manager 180

1 var datFile = new File("~/Desktop/legacy.dat");


2 datFile.encoding = 'BINARY';
3 datFile.open('w');
4 datFile.write(rawData);
5 datFile.close();

I personally use SynalyzePro, but free alternatives are available, such as 0xED on Mac, or HxD on
Windows. The following is the Hex view of the saved "legacyContentData" from a Hue/Saturation
adjustment:

Per se, it visualizes a bunch of irrelevant data, unless you know where to look. When binary data is
stored in a data stream, it is just a linear sequence of information; as an example, let’s consider the
string "DB2016120401". Quite unintelligible, unless you know (say) that the first two letters ( DB) are
the author signature; the following eight represent the date in a YYYYMMDD format (20161204), and
the last two the revision number of this Chapter.
Without this information, you can’t decode the string; similarly, you need a map for the rawData
stream – that comes to you as the Adobe Photoshop File Formats Specification²².

This document describes the way various Photoshop formats (e.g. psd or tiff) store data. It turns
out (by comparison) that in this case the "legacyContentData" shares a lot with the Hue/Saturation
settings file (8BHA on Mac, .AHV on Windows) so that the same map can be used to inspect our
legacy.dat file.

The first two bytes are the version (2), the third byte is 0 (use settings for hue-adjustment and not
colorization), the fourth byte is ignored. Fifth to tenth bytes are for Colorization (not used in this
case so that we can skip them). The next six bytes are for the master hue, saturation and lightness
actual values – two bytes each.
²²Updated HTML version here, or a less recent yet slightly different PDF version.
Action Manager 181

And so on, following the File Format specification. Of course, this is something we do at home, in
snowy Sunday afternoons: in your scripts, you must find a way to parse such streams programmat-
ically. An example is found in this thread.

6.13 AM Setters
Retrieving properties from Photoshop elements is undoubtedly useful, but wouldn’t be even more
fun to set them? Alas, this is not always possible, as far as I can tell. In this section we’ll try to build
Descriptors and "set" them into Photoshop: it’s perhaps the most esoteric area of all ActionManager,
at least for me.
We’ve run into the "kuiBrightnessLevel" Descriptor before; the getter we wrote is repeated below
for your convenience:

1 var r1 = new ActionReference();


2 var d1 = new ActionDescriptor();
3 r1.putProperty( s2t("property"), s2t("interfacePrefs"));
4 r1.putEnumerated( s2t("application"),
5 s2t("ordinal"),
6 s2t("targetEnum") );
7 d1 = executeActionGet(r1);
8 var interfaceDesc = d1.getObjectValue(s2t("interfacePrefs"))
9 t2s(interfaceDesc.getEnumerationValue(s2t("kuiBrightnessLevel")));

Is this key writable, and how? We’re fortunate here since this action leaves a trace in the
ScriptListener log. I could call quit, problem solved, but instead, a comparative analysis of both
snippets might lead to some insights. The application object has an "interfacePrefs" key, which
value is an ActionDescriptor. In turn, this AD has a "kuiBrightnessLevel" key: its corresponding
Action Manager 182

value "kPanelBrightnessMediumGray" is of enum type "uiBrightnessLevelEnumType", as this log


shows.

1 {
2 "interfacePrefs": {
3 "_obj": "interfacePrefs",
4 "kuiBrightnessLevel": {
5 "_enum": "uiBrightnessLevelEnumType",
6 "_value": "kPanelBrightnessMediumGray"
7 }, //...
8 }
9 }

What does the refactored ScriptListener code look like?

1 var d1 = new ActionDescriptor();


2 var r1 = new ActionReference();
3 r1.putProperty( s2t("property"), s2t( "interfacePrefs" ) );
4 r1.putEnumerated( s2t("application"), s2t("ordinal"), s2t("targetEnum") );
5 d1.putReference( s2t("target"), r1 );
6 var d2 = new ActionDescriptor();
7 d2.putEnumerated( s2t( "kuiBrightnessLevel" ),
8 s2t( "uiBrightnessLevelEnumType" ),
9 s2t( "kPanelBrightnessLightGray" ) );
10 d1.putObject( s2t("to"), s2t( "interfacePrefs" ), d2 );
11 executeAction( s2t("set"), d1, DialogModes.NO );

It first creates an ActionReference instance, then (as we saw when getting properties), it fills it from
child to parent, very much like if we were about to executeActionGet(). Instead, the Reference is
put as the value of a "target" key in a blank d1 Descriptor.
A second new Descriptor (d2) is then created, and given a "kuiBrightnessLevel" key: an enu-
merable, of type "uiBrightnessLevelEnumType" and value "kPanelBrightnessLightGray". This d2
Descriptor is then put as the value of a d1 "interfacePrefs" key thanks to the putObject() method;
apparently the first parameter s2t("to") is the actual key, while s2t("interfacePrefs") is the
classID – according to the JS Reference. Eventually, this d1 Descriptor is "set". Where? In the
place its own ActionReference points, the "interfacePrefs" key of the "application" enumerable
target.
If you think about it, we’re building an incomplete (but hierarchically correct) structure of the
application Descriptor, containing only the keys we’re interested in; then we set it. Is this pattern
repeatable? Let’s see.
Say that I want to tick the Auto-Select checkbox that appears in the Photoshop options bar when
the Move tool is selected.
Action Manager 183

You can try yourself, but I can tell you that ScriptListener doesn’t log this no matter how hard you
try. What could we do? I’ll describe you the entire trial and (many) errors process, which is as iffy
as it gets, but it’s (in my personal opinion) incredibly useful as a real-world case study.
I’ve not listed Tools among the elements you can get Descriptors from (like Layers, Documents, etc.),
and for a reason. I’ve tried in several ways, with no luck.

1 // Naively...
2 var r1 = new ActionReference();
3 r1.putEnumerated(s2t("tool"), s2t("ordinal"),s2t("targetEnum"));
4 var d1 = executeActionGet(r1); // NO;
5 // Like with Channels?
6 var r1 = new ActionReference();
7 r1.putEnumerated(s2t("tool"), s2t("tool"),s2t("targetEnum"));
8 var d1 = executeActionGet(r1); // NO;
9 // Classes maybe...
10 var r1 = new ActionReference();
11 r1.putClass(s2t("moveTool"));
12 var d1 = executeActionGet(r1); // NO;
13 // Do we have anything alcoholic to drink in this house?

Inspecting the entire application Descriptor, there’s a promising key.

1 "currentToolOptions": {
2 "ASGr": true,
3 "Abbx": false,
4 "AtSl": false,
5 "_obj": "currentToolOptions"
6 }, // ...

Alas (even if “alas” hasn’t really been my original exclamation) "ASGr" is the weirdest charID
ever seen, and it doesn’t translate into any meaningful stringID – even stranger. "AtSl" can be
considered as Auto Select, maybe? If I manually select it and log the application Descriptor again,
it turns to true. So, "ASGr" is the Auto Select Group option, and "Abbx", with a giant leap of faith,
is Show Transformation Controls.
Following the same idea of the "kuiBrightnessLevel", I can try to get the "currentToolOptions"
Descriptor and learn from the getting process how to put it. The first part should be a piece of cake
for the experienced ActionManager juggler you are.
Action Manager 184

1 var r1 = new ActionReference ();


2 ref.putProperty (s2t ("property"), s2t ("currentToolOptions"));
3 ref.putEnumerated (s2t ("application"),
4 s2t ("ordinal"),
5 s2t ("targetEnum"));
6 var d1 = executeActionGet (ref)
7 .getObjectValue (s2t ("currentToolOptions"));
8 // ERROR!

That’s really weird. Why on earth shouldn’t Photoshop gently hand me the "currentToolOptions"
Descriptor? In the above code I’ve first restricted the application Reference to the "currentToolOptions"
key, let’s get the entire application AD, and then extract the key.

1 var r1 = new ActionReference ();


2 ref.putEnumerated (s2t ("application"),
3 s2t ("ordinal"),
4 s2t ("targetEnum"));
5 var d1 = executeActionGet (ref)
6 .getObjectValue (s2t ("currentToolOptions"));
7 // WORKS!

I’m puzzled, to say the least. I could ignore that and go ahead, but it’s an intriguing error,
so I post in the Forums and Michel Mariani answers me. It turns out that you can’t get the
"currentToolOptions" key, but if you get the "tool" instead, you’re given a Descriptor containing
both "tool" and "currentToolOptions".

1 var r1 = new ActionReference ();


2 r1.putProperty (s2t ("property"), s2t ("tool"));
3 r1.putEnumerated (s2t ("application"),
4 s2t ("ordinal"),
5 s2t ("targetEnum"));
6 var d1 = executeActionGet (r1);
7 logDescriptor(d1);
8
9 // {
10 // "_obj": "object",
11 // "currentToolOptions": {
12 // "$ASGr": true,
13 // "$Abbx": true,
14 // "$AtSl": false,
15 // "_obj": "currentToolOptions"
16 // },
Action Manager 185

17 // "tool": {
18 // "_enum": "moveTool",
19 // "_value": "targetEnum"
20 // }
21 // }

Can I, in the same fashion, rebuild that Descriptor and "set" it? Who knows, let me give that a try.

1 var d1 = new ActionDescriptor();


2 var r1 = new ActionReference();
3 r1.putProperty( s2t("property"), s2t( "currentToolOptions" ) );
4 r1.putEnumerated( s2t("application"), s2t("ordinal"), s2t("targetEnum") );
5 d1.putReference( s2t("target"), r1 );
6 var d2 = new ActionDescriptor();
7 d2.putBoolean( c2t( "AtSl" ), true);
8 d1.putObject( s2t("to"), s2t( "currentToolOptions" ), d2 );
9 executeAction( s2t("set"), d1, DialogModes.NO );
10 // ERROR.

Dang it! I’ve mirrored the (working) "kuiBrightnessLevel" code, it should be flawless: is this broken
because of the "currentToolOptions" bug? I’ve tried switching it with "tool", to no avail. Should
I nest a Descriptor one more level deeper? This business is getting too tricky. As a last hope, I post
in the Forums and Tom Ruark himself points me in the right direction.

1 var d1 = new ActionDescriptor();


2 var r1 = new ActionReference();
3 r1.putClass(s2t("moveTool"));
4 d1.putReference(s2t("target"), r1);
5 var d2 = new ActionDescriptor();
6 d2.putBoolean(c2t("AtSl"), true);
7 d1.putObject(s2t("to"),s2t("target"),d2)
8 executeAction(s2t("set"),d1,DialogModes.NO); // Hurray!

Wow. And I can tell you something: it even works when the currently selected tool is not the Move
Tool. If you look at the ScriptListener code for selecting that tool:
Action Manager 186

1 var d1 = new ActionDescriptor();


2 var r1 = new ActionReference();
3 r1.putClass( s2t("moveTool") );
4 d1.putReference( s2t("target"), r1 );
5 executeAction( s2t("select"), d1, DialogModes.NO );

It is quite similar indeed. In both getter and setter, the AR points to its target via putClass(); in the
setter, I did put d2 in the "currentToolOptions" classID, while Tom uses "target". I would have
never found that without his help.
To test the effectiveness of Tom’s suggestion, I’m going to try setting another tool preference. I’ve
chosen the Spot Healing Brush Tool.

As you know, it can use three algorithms (Content-Aware, Create Texture, Proximity Match) – I’d
like to set Create Texture. First I need to find the name of this tool: I manually select it and run an
application Descriptor; I’ll find the name under the "tool" key:

1 "tool": {
2 "_enum": "spotHealingBrushTool",
3 "_value": "targetEnum"
4 }, // ...

At this point I know the first part of my code, which will be:

1 var d1 = new ActionDescriptor();


2 var r1 = new ActionReference();
3 r1.putClass(s2t("spotHealingBrushTool"));
4 d1.putReference(s2t("target"), r1);
5 var d2 = new ActionDescriptor();

To find the relevant parts, I need to go back to the logged application Descriptor and look up the
"currentToolOptions" key,
where these options are set.
Action Manager 187

1 "currentToolOptions": {
2 "$SmmS": {
3 "_enum": "$SmmT",
4 "_value": "$CntW"
5 },
6 "$SmpS": {
7 "_enum": "$SmpT",
8 "_value": "$SrcS"
9 },
10 "$StmA": false,
11 "$StmB": false,
12 "$StmI": false,
13 "$StmS": true, // ...

They aren’t as friendly as I would have hoped, but getting the entire log few times, manually
switching to the available options in the GUI, I find that "CntW" is Content-Aware, and "CrtT"
is Create Texture – what I want to set. So "SmmS" is the key, "SmmT" the enum type (whatever they
mean), and "CrtT" the desired value. As a result, the final, working code is as follows.

1 var d1 = new ActionDescriptor();


2 var r1 = new ActionReference();
3 r1.putClass(s2t("spotHealingBrushTool"))
4 d1.putReference(s2t("target"), r1);
5 var d2 = new ActionDescriptor();
6 d2.putEnumerated(c2t('SmmS'), c2t('SmmT'), c2t('CrtT'));
7 d1.putObject(s2t("to"),s2t("target"),d2);
8 executeAction(s2t("set"),d1,DialogModes.NO);

That’s reassuring. Another option that should be similarly doable (but it’s slightly more complex) is
setting the brush properties of this very tool (e.g., the diameter). If you look below its application
AD, you’ll find a nested "brush" descriptor:

1 "currentToolOptions": {
2 // ...
3 "brush": {
4 "_obj": "computedBrush",
5 "angle": {
6 "_unit": "angleUnit",
7 "_value": 0
8 },
9 "diameter": {
10 "_unit": "pixelsUnit",
Action Manager 188

11 "_value": 15.8609
12 },
13 "flipX": false,
14 "flipY": false,
15 "hardness": {
16 "_unit": "percentUnit",
17 "_value": 100
18 }, // ...

This "brush" Descriptor, of class "computedBrush"²³, contains the "diameter" UnitValue, and that’s
what needs to be targeted. How to?
Compared to the previous case (the Content-Aware option), the key I want to address is just one
more level nested.

1 var d1 = new ActionDescriptor();


2 var r1 = new ActionReference();
3 r1.putClass(s2t("spotHealingBrushTool"))
4 d1.putReference(s2t("target"), r1);
5 var d3 = new ActionDescriptor();
6 d3.putUnitDouble(s2t("diameter"), s2t("pixelsUnit"), 211);
7 var d2 = new ActionDescriptor();
8 d2.putObject(s2t("brush"), s2t("computedBrush"), d3);
9 d1.putObject(s2t("to"), s2t("target"),d2);
10 executeAction(s2t("set"), d1, DialogModes.NO);

Funnily enough, if you change the diameter yourself, the ScriptListener logs the code! And it’s
completely different from the one you’ve just read:

1 var d1 = new ActionDescriptor();


2 var r1 = new ActionReference();
3 r1.putEnumerated( s2t("brush"),
4 s2t("ordinal"),
5 s2t("targetEnum") );
6 d1.putReference( s2t("target"), r1 );
7 var d2 = new ActionDescriptor();
8 d2.putUnitDouble( s2t("masterDiameter"), s2t("pixelsUnit"), 11 );
9 d1.putObject(s2t("to"), s2t("brush"), d2);
10 executeAction( s2t("set"), d1, DialogModes.NO );
²³There are two kinds of Brushes. Computed are geometrically generated (e.g., round or elliptic, with or without feathering); Sampled ones,
instead, come from a raster image, that is used as the brush base.
Action Manager 189

All our brain juice is then wasted? No! I’m sure that if you think about the ActionReference from
the ScriptListener code, you’ll spot what makes these two ActionManager snippets very different.
Can you?
The putEnumerated() method, with "something", "ordinal", and "targetEnum" should ring a bell
in your head: it’s what we’ve been using so far to mean “the currently active something”: either
Document, Layer, etc. Here, whichever tool you happen to have selected – that supports a brush –
it’s going to have its diameter set. Conversely, the snippet that I’ve provided, precisely targets the
"spotHealingBrushTool" only, even if it’s not the currently active tool. Mind you, my code works
for the "paintbrushTool" as well, but only if the brush type is "computedBrush"; "sampledBrush"
ignores the command (very likely because of a bug).

With brushes, this long, dense Chapter ends. Throughout the book, more ActionManager code will
be written – so you’ll have further chances to familiarize with it. In my own experience, a fair
amount of frustration has to be taken into account: there are properties that you can get, but not
set; some of them are out of the AM reach altogether, or orphans that seem to actively run away
from you and hide in AM’s thick forest; others may be plainly impossible to code. It isn’t too far
from the truth, perhaps, to infer that AM has not to be intended as food for third-party developers
consumption: more likely, we’re talking about an internal-use only API that Photoshop engineers
manipulate for, say, automated tests and QA.
No matter how long it will take you to domesticate it, ActionManager is a fundamental skill in
Photoshop scripting, and the results are definitely worth the effort.
7. User Interfaces
Scripts, as processing engines, may not need a Graphical User Interface (GUI) at all: parameters can
be hardwired or computed on the fly, and the code directly modified when the requirements change.
Yet, either if you plan to distribute your work to a broader audience – say, colleagues in a company’s
department, or clients of yours – or in case the parameters must be defined at runtime, a GUI is a
primary feature.

7.1 The technology dilemma


When it comes to GUIs, you have at least two valid alternatives to choose from, and I’m afraid
there’s no clear winner: the options should be evaluated on a case by case basis.
As it’s been briefly mentioned in Chapter 5, the ExtendScript language has a peculiar ScriptUI Class¹,
which is used to build what I’ll be calling here Scripted Dialogs. On the other hand, you can quite
easily embed Script engines within CEP² Panels (aka HTML Panels), and use Panels as powerful
interfaces on top of JSX. As follows a checklist of their respective pros and cons.

Compatibility

TL;DR Scripted Dialog can successfully run in old versions, e.g. CS3 and possibly even
earlier too³. HTML Panels, instead, are supported from CC (version 14.0) onwards only.
Yet… it’s not that simple.

Wider backward compatibility isn’t all rainbows and unicorns: true, Scripted Dialogs can run on
CS6 – let’s use that as a milestone of the pre-Cloud era – yet dialogs may look and behave quite
differently among versions, platforms, not to mention host applications. ScriptUI is known to have
been implemented in a very different way from each team in all the Adobe’s major applications. I’m
talking about both the cosmetic and functional sides.
Things have more or less stabilized in Photoshop with the so-called Mondo rendering engine (the one
used to draw Windows in Scripting), but the last transition hasn’t been bloodless. For instance, the
TreeView component has vanished, and new bugs have been introduced. In the following illustration,
you can find the same dialog rendered in four Photoshop versions. CS6 is missing, because it is
¹ScriptUI is more an add-on, rather than a branch of the original ExtendScript specs. From the user standpoint, it’s part of the language,
for the ScriptUI Class is globally available.
²CEP stands for Common Extensibility Platform.
³I’ve never aimed at anything before CS3, also because that’s the version in which we’ve been first allowed to obfuscate the code with
JSXBIN.
User Interfaces 191

identical to CC, but doesn’t support retina (high PPI) displays – it will look pixelated. Every one of
these screenshots come from the same code: spot the differences!

CC (top left), CC 2014 (bottom left), CC 2015.5 (top right), CC 2017 (bottom right)

Compared to Scripted Dialogs, HTML Panels span over a shorter range of versions, but they are
implemented, in my opinion, in a more consistent fashion. Mind you: before them, there were Flash
Panels: introduced in CS4, they tried to find their place in Photoshop until CS6, then Adobe has
deprecated them. CC is the only “bridge” version supporting both Flash and HTML Panels; from CC
2014 onwards, Flash passed away in Photoshop⁴.
You might be tempted to code Flash panels for pre-CC versions, and HTML for post-CC. Let’s set
aside considerations about how many old versions your software should support, which is not
something that I can abstract into the rule: if you find a client who drowns you in cash to build
a Flash panel, sport your better smile and code it. Let me explain why this is not a wise idea, in my
opinion.
First, the tooling is obsolete and wasn’t particularly good, to begin with. Since late 2011 Flex⁵ has
been donated to the Apache Foundation and is now open source, there might be better options that
I don’t know – even if the “Extension Builder” plugin made available by Adobe for development
was meant to run in Flex Builder only (not my cup of tea). Second, any Flex SDK or Photoshop bug
you’re going run into will keep being there, unfixed, ‘till the end of your time on earth; third, you
have to write from scratch all the code twice – one for the Flash panel, one for HTML panel. If you
⁴Flash has been supported some extra years in InDesign, where the transition phase to HTML has been longer.
⁵Flash/Flex can be used interchangeably in this context: Flash panels were build using the Flex SDK.
User Interfaces 192

don’t use ActionScript libs for Scripting, you’re allowed to share just the JSX code.

That said, there is an ongoing project supported by the InDesign developer Gabe Harbs
called CEP Royale, which goal is to port CEP to Apache Royale: a “productive, open-source
frontend application technology that lets you code in MXML & AS3 and output to different
formats”, according to the Apache Foundation. If you already dig ActionScript and you’re
a nostalgic of the Flash days, you can give it a try.

Behaviour

TL;DR Scripted Dialogs are modal, HTML Panels modeless. Sort of… Read along.

A dialog is called modal if you cannot interact with the rest of the host application’s GUI (panels,
documents, etc.) while the dialog is running: Photoshop beeps and refuses to cooperate. You are only
allowed to interact with the dialog itself – clicking buttons, dragging sliders, typing into forms, etc.
Conversely, a modeless (or non-modal) GUI sits there in the Photoshop interface, ready for your
input, but won’t steal the focus from the host application. You are free to switch from the dialog to
the Photoshop interface and vice-versa.
Image Adjustments are good candidates to illustrate the modal vs. modeless behavior difference.
As you know, they can be applied in two different ways: either directly on the layer’s pixels (from
the 'Image > Adjustments' menu), or as Adjustment Layers (using the correspondent button in
the Layers panel, and accessing the actual parameters in the Properties panel). This is true for all
Adjustments, let’s check the Color Balance as an example here:

Color Balance Adjustment: modal (left), modeless (right)

In theory, ScriptUI includes both modal and modeless windows; however, in Photoshop only modal
dialogs are available. The truth is that the implementation of modeless ones, so-called Palettes in
Photoshop, is inadequate⁶: it has changed over versions and platforms, and currently it’s plain buggy,
⁶InDesign fully supports ScriptUI modeless windows; actually, InDesign support of the whole ScriptUI specs is much more complete.
User Interfaces 193

up to the point that it is safe to consider modeless dialog unavailable – if only because we have been
told so by Adobe engineers themselves. If non-modal GUIs are a requirement, then (they say) CEP
Panels are the proper solution.
On the other side, even if HTML Panels are used as modeless 99% of the times, nothing prevents
you from building modal Panels as well, if needed: it’s a matter of tweaking a single parameter in a
configuration file.
As a rule of thumb, you would need a modeless dialog for a GUI that is meant to be always around,
not interfering with the rest of the Photoshop interface: say, an Export panel with several “Save As”
buttons, e.g., JPG, PNG, etc. In turn, something like a Batch dialog can be modal: you run it when
needed, input all the required parameters, let it do its business, then close it.

Modal dialogs, by definition, won’t let the user do anything with Photoshop but interact with
the dialog itself. If you think about it, this is a crucial requirement for Scripts which purpose
is to apply image processing routines (like a Filter would do): tweaking the parameters in
the GUI usually involves resetting the document to a known, unprocessed state, and then re-
apply the algorithm from scratch. With modal dialog this is a piece of cake: the user cannot,
say, mess with the Layers palette or switch to a different document in the meantime.
Moreover, modal dialogs explicitly set entry and exit points: when the dialog is called, you
can run an initialization routine; when the dialog quits, a similar cleaning routine can be
triggered. With modeless Panels – that are supposedly always on – it’s not impossible, but
way much more difficult to know when something starts, ends, or we’re just in the middle
of a processing session; and at the same time deal with edge cases, many of the unwanted
user’s behaviours, and multiple documents.

Appearance

TL;DR Scripted Dialogs are for engineers, HTML Panels better suited to designers.

Quite a harsh and funny statement, but somehow true nonetheless; let me explain, and add more
needed details. Both Scripted Dialogs and HTML Panels provide you with several GUI essential
elements: Text, Checkbox, Slider, DropDown List, etc. that can be composed to form the actual user
interface. The sheer number of these basic elements is roughly the same; the striking difference is
in the level (and need) of customization.

CEP Panels can be dressed up as Birds of Paradise: you can tweak every element to your taste,
from the fonts used, to size and kind of buttons borders, rainbow drop shadows, whatever graphic
perversion comes to your mind, it’s very likely doable. The downside is that you must decide the
style of the GUI yourself (i.e., set the aspect of every element, the background, etc.) and keep it in
sync with the Photoshop interface brightness.
User Interfaces 194

Conversely, Scripted Dialogs come with pre-


styled elements, with a minimal set of avail-
able tweaks: it’s possible to bend them to your
graphic designer will, but only to a smaller
extent. The big deal is that you don’t have to
necessarily worry about styling at all, and get
a reasonably good user experience, consistent
with the rest of the Photoshop GUI.
Unless you want your GUI to pop up and
hurt the user’s eyes, you’re probably after a
look and feel that blends with Photoshop; the
bottom line is that Scripted Dialogs provide a
decent match for free, not too much customiz-
able, but production-ready out of the box. CEP Panels can be turned into native Photoshop panels
(mostly because part of the Photoshop’s own interface is, in fact, made with CEP Panels) but at a
development cost.

Technology

TL;DR Scripted Dialogs are integrated within your scripts, while CEP Panels are a
different piece of technology that embeds your scripts. Also, the ScriptUI class is evaluated
by the same ExtendScript engine, while the CEP Panel operations run on a JavaScript
engine, that must communicate with the ExtendScript engine exchanging messages back
and forth.

I’ve left the programming details at the end on purpose because I wanted you to evaluate other
aspects first. Scripted Dialogs are nothing but an additional, native feature of ExtendScript: as
a consequence, they aren’t semantically nor functionally different from any other part of your
Scripting code.
CEP Panels are an entirely separate thing. You won’t really use them as the GUI of your Scripts;
more precisely, CEP Panels are going to embed and use your Scripts, submitting them for evaluation
to the Photoshop engine – this is a fundamental switch in perspective. When dealing with scripted
GUIs, you can think about ScriptUI as a subsidiary class, whereas CEP Panels are equally important
and/or powerful compared to the Script code they drive.
A dedicated section will follow, but basically CEP Panels are web applications running within a
Google Embedded Framework instance (a portable Google Chrome browser, so to speak): like any
other web application running in a browser, it’s all about HTML, CSS, and JavaScript. While CEP
defines the elements architecture (buttons, forms, you name it), CSS deals with styling. JS has two
major roles.

• It is in charge of the Panel’s operations – e.g., elements handlers: when this button is clicked,
send a POST request to a server, and pass the data from this other field, etc.
User Interfaces 195

• It manages the host application operations, in other words: it runs Photoshop scripts.

This task is performed indirectly because the JavaScript engine that powers CEP Panels doesn’t know
a single thing about ExtendScript: so how can a Panel run JSX code? It sends string messages to the
ExtendScript engine, which parses and executes them. Details will follow in a later section, but for
now think about two files: a .js and a .jsx. The JS listen for, say, a button click event in the GUI,
and when it’s triggered, it hands to the JSX engine the "runRoutine()" string, twelve meaningless
chars; the JSX engine looks up for the runRoutine() function in the JSX file, finds and executes it,
passing back to the JS engine the returned value. The whole process is asynchronous, i.e., when the
JS engine has dispatched the message to its JSX colleague, it keeps on doing its own business.
This messaging system is perhaps not ideal, but it has proved to be fast enough – in fact in some
circumstances refreshing a Scripted Dialog to reflect GUI changes may take longer than sending
back and forth messages between JS and JSX engines.

Which solution to the GUI problem is your best fit, a Scripted Dialog or an CEP Panel, it’s not
something I can tell: you need to evaluate yourself pros and cons of each approach, alongside with
its development cost.
Here I’ll be focusing mostly on Dialogs. A summary section about Panels is found at the end of this
Chapter anyway, to let you taste the salty water of this vast development sea.

7.2 ScriptUI
As I’ve mentioned earlier, ScriptUI is an ExtendScript add-on that provides you with the tools needed
to build and interact with User Interfaces. Per se, the Class is not very useful: since I’m a fan of the
reflection interface, let me show you some of its properties using a slightly tweaked helper function,
that makes use of recursion to build and return a JSON object.

1 // Tweaked to show nested properties as well


2 function reflectObjJSONProps(Obj) {
3 var props = Obj.reflect.properties;
4 var res = {};
5 for (var i = 0; i < props.length; i++) {
6 try {
7 if (props[i].name.charAt(0) === "_") continue;
8 if (props[i].name === "reflect") continue;
9 if ((typeof Obj[props[i].name]) === 'object' ) {
10 res[props[i].name] = reflectObjJSONProps(Obj[props[i].name]);
11 } else {
User Interfaces 196

12 res[props[i].name] = Obj[props[i].name];
13 }
14 } catch(e) { ; }
15 }
16 return res;
17 };
18 // Remember to include json2.js library
19 JSON.stringify(reflectObjJSONProps(ScriptUI));

Some selected properties from the JSON result are as follows.

1 {
2 "Alignment": {
3 "AFTER": 8, "BEFORE": 7,"BOTTOM": 2,"CENTER": 6,
4 "FILL": 5, "LEFT": 3,"RIGHT": 4,"TOP": 1
5 },
6 "FontStyle": { "BOLD": 1, "BOLDITALIC": 3, "ITALIC": 2, "REGULAR": 0 },
7 "WritingDirection": { "LTR": 0, "RTL": 1 },
8 "applicationFonts": {
9 "dialog": {
10 "family": ".AppleSystemUIFont",
11 "name": ".AppleSystemUIFont",
12 "size": 13,
13 "style": "",
14 "substitute": ""
15 },
16 "palette": { /* ... */ },
17 "window": { /* ... */ },
18 },
19 "coreVersion": "6.2.2",
20 "environment": {
21 "keyboardState": {
22 "altKey": false,
23 "ctrlKey": false,
24 "metaKey": false,
25 "shiftKey": false
26 },
27 "textDirection": 0
28 },
29 "frameworkName": "Mondo",
30 "version": "3.2.9",
31 // ... etc.
User Interfaces 197

There is more (which, as you’ve already heard from me, is covered in the JS Tools Guide, page 105
et seq.), here among the rest I’ve shown "Alignment" and "FontStyle" enumerated constants – that
you’ll use when defining new fonts or placing elements; two version numbers, alongside with the
"frameworkName", that, of course, varies depending on the application.

Please note that the framework is now Mondo (which for your information means world in Italian),
but ESTK in my machine uses MacOSX, Photoshop CS6 was Flex, while InDesign CC 2017 has Drover
– apparently Adobe teams value their freedom of choice rather than standard bodies. Mondo, as far
as I’ve been told, is the same rendering engine that draws plugin windows in Photoshop, so it seems
to be a wise pick after all.

7.3 Documentation
You’ll be looking for documentation on Scripted Dialogs a lot – and that’s fine, there’s much ground
to be covered. As opposed to other topics, I find myself here much more in need of a Reference kind
of documentation, rather than more discursive Guides: hopefully, this Chapter is going to handle
much of the theory involved, so you’ll know how to use functions, create elements, etc. Yet, there
are always doubts about details such as creation parameters, and the like. For that, keep handy the
JavaScript Tools Guide: it has everything you need to know about ScriptUI.
While the Tools Guide is excellent as a Reference, you can look for a more hands-on approach in
the remarkable, and freely available, ScriptUI for Dummies, written and regularly updated by the
InDesign developer Peter Kahrel.
Since both resources are quite extensive (about one hundred pages the Adobe guide, one hundred
and thirty the other) I’ll do my best here to try a third, different approach. My goal is to leave you
with a solid understanding of the ScriptUI architecture by examples, explaining both the basics and
those annoying little details that one tends to overlook. By the end of this Chapter, you can navigate
the otherwise intimidating JS Tools Guide with confidence, and enjoy Kahrel’s demo dialogs and
finesses.

7.4 The Window object


If you look at the "applicationFonts" property of the previously logged ScriptUI Class, you’ll see it
contains specs for three entries: "dialog", "palette", "window", which in fact are the three available
kinds of interfaces you can theoretically build thanks to ScriptUI.
The Window Class is always instantiated to build dialogs, except for creating the three elements
you’ve already met in Chapter 5: I’m talking about the alert(), confirm() and prompt() dialogs,
which in fact are all Window class methods. You can shorten the Window.alert() call into alert(),
since the ExtendScript engine will look up in the prototypal chain of the globally available objects.
So far I’ve always used “Scripted Dialogs” as a common name for ScriptUI Windows: it’s now time
to discern what’s the difference between the three possible GUIs.
User Interfaces 198

var d = new Window('dialog'); // Modal, working


var p = new Window('palette'); // Modeless, bugged (don't use it in PS)
var w = new Window('window'); // Modeless, bugged (don't use it in PS)

A 'dialog' Window is the commonly used workhorse for Photoshop interfaces. It’s modal, so as
long as it’s open, the user won’t be able to interact with the rest of the host application UI. It has an
optional title bar, no closing button but an optional maximize one – users will have to either type
the ESC key or click a button that you must provide to dismiss it.
The 'palette' is the Dialog’s modeless sister. It has a close button, which in Photoshop is useless
since the Palette won’t stick around for long: as soon as you create it, it flashes and dies like one
of those particles that scientists create underground at the CERN’s Large Hadron Collider. If you
absolutely need a modeless window, CEP Panels are the best choice. A couple of Palette hacks are
found on my blog here and here, but after many years of frustration, I now advise against their use
– if only, as I’ve already mentioned, because engineers made clear that Palettes are not meant to
work in Photoshop; and if they do, it’s by accident.
Finally, there’s a 'window' Window, with collapsing, expanding and closing buttons as well. I’ve
never run into such a Window in my scripting career – I’ve tried here for I’m a curious boy: it has
a behavior similar to the Palette (non modal, tends to die immediately, hacks work here too), but it
shows the same bugs of the modeless sister; live safe and peaceful, and forget about it.
Focusing on Dialogs, let’s now see the optional constructor parameters: this pattern is going to be
used with other elements too.

// new Window (type [, title, bounds, {creation_properties}]);


var d = new Window('dialog', 'A Dialog', [100,100,300,300], {resizeable: true});
d.show();

Please note that instantiating a window doesn’t immediately show it: that’s the job of the
appropriately named show() method. In every ScriptUI element you’re going to build, besides the
type (here 'dialog'), everything is either optional⁷; such parameters can be defined in a later stage,
⁷In the Documentation, listing parameters in square brackets means they’re optional.
User Interfaces 199

with the exception of the creation properties object; speaking of which, the only one that works in
Dialogs is the resizeable boolean.
Bounds⁸ are ubiquitous and deserve some extra coverage. You might wonder what the [100,100,300,300]
array means: as is, it defines the x,y coordinates of the top-left and bottom-right corners of the
window: topleft-x, topleft-y, bottomright-x, bottomright-y. As a result, the Dialog is a 200px
square that starts at the 100,100 location in your display. Yet, bounds can be defined in several other
ways: as follows all the valid alternate syntaxes.

// Array [ x-left, y-top, x-right, y-bottom ]


[100,100,300,300]
// Object { left, top, right, bottom }
{ left:100, top:100, right:300, bottom:300 }
// String "left, top, right, bottom"
"left:100, top:100, right:300, bottom:300"
// Object { x, y, width, height }
{ x:100, y:100, width:200, height:200 }
// String "x, y, width, height"
"x:100, y:100, width:200, height:200"

As I’ve just mentioned, params are optional – if you want to address bounds and title later on (maybe
you’re going to set them dynamically), the following statement is perfectly fine.

var d = new Window('dialog', undefined, undefined, {resizeable: true});

The many read/write properties of the Window object, and everything else, can be set as usual.

// Not resizeable, because the creation property object is missing


var d = new Window('dialog');
d.bounds = [100,100,300,300];
d.title = "A square dialog";
d.show();

7.5 Containers and Controls


ScriptUI identifies two kind of elements that a Window instance can hold:

• Containers: Panel, Group, Tabbed Panel, Tab.


• Controls: Button, IconButton, Image, StaticText, EditText, Checkbox, RadioButton, Progress-
bar, Slider, Scrollbar, Listbox, DropDownList, TreeView⁹, ListItem, Custom.
⁸While bounds define the Window content size, the whole frame (title bar and borders included) is found in the frameBounds property.
⁹From CC2015 onwards, TreeView is not supported anymore.
User Interfaces 200

Containers can accommodate other Containers, and of course Controls – this is the way all Dialogs
are composed. How? Each element in the list above can be inserted into a Window via the add()
method, passing the element type, and optional parameters: which, very much like the Window
itself, can be set later if the control is accessible (e.g., it’s been stored in a variable).

var d = new Window('dialog');


var b = d.add('button', undefined, 'Click me!');
d.show();

This snippet creates a button, with no bounds specified (the undefined parameter: ScriptUI so-called
“LayoutManager” does its best to fill in the required parameters with meaningful defaults), and a
"Click me!" text. An example of some of the common properties as follows:

1 var d = new Window('dialog');


2 var b1 = d.add('button');
3 b1.text = "Normal Button";
4 b1.size = [100, 50];
5 // b1.size = { width: 100, height: 50 }; // alternate syntax
6 b1.helpTip = "Click the button to have the button clicked";
7
8 var b2 = d.add('button');
9 b2.text = "A disabled button";
10 b2.enabled = false;
11 $.writeln(b2.preferredSize); // 148,25
12 d.show()

In the second button, the preferredSize property contains the element’s size, as it’s been automat-
ically set by the LayoutManager to house the provided text¹⁰. You can specify width or height only:
the other parameter should be set to an empty string, e.g. [100, ''].
With Mondo, it’s possible to create special Call To Action (CTA) buttons – slightly bigger, with
round corners – setting the name in the creation properties object: they’re chiefly associated with the
Accept/Dismiss behavior, so either ok or cancel make the button a CTA. Strangely enough, every
other button that happens to belong to the same Container (either the Window itself or a Group,
Panel, etc.), gets the same CTA treatment.

¹⁰preferredSize is a read/write property, so you’re allowed to set it too.


User Interfaces 201

1 var d = new Window('dialog');


2 // { name: 'Cancel' } will work too
3 var b1 = d.add('button', undefined, undefined, { name: 'Ok' });
4 b1.text = "CTA Button";
5 var b2 = d.add('button');
6 b2.text = "Another CTA";
7 d.show();

In this case, the Enter keypress triggers the


b1 click handler (that you’ll meet in the fol-
lowing pages), since it’s by default associated
with the "Ok" button.
Containers can be added to the Window the
same way, with Controls or other nested Con-
tainers within them. I’ll deal with positioning
in a short while, have a look at this example
first of a series of Radio Buttons within a Panel, and grouped CTA buttons.

1 var d = new Window('dialog', 'Options');


2 d.alignChildren = ['fill', ''];
3
4 var p = d.add('panel', undefined, 'Pet');
5 p.alignChildren = ['left',''];
6 var r1 = p.add('radiobutton', undefined, 'Cat');
7 var r2 = p.add('radiobutton', undefined, 'Dog');
8 var r3 = p.add('radiobutton', undefined, 'Iguana');
9 r1.value = true; // selects the first Radio Button
10
11 var g = d.add('group');
12 g.orientation = 'column';
13 g.alignChildren = ['fill','top'];
14 var b1 = g.add('button', undefined, 'OK', { name: 'Ok' });
15 var b2 = g.add('button', undefined, 'Nah', { name: 'Cancel' });
16
17 d.show();

7.6 Layout
User Interfaces 202

In the previous snippet, I’ve added some new properties, namely


orientation and alignChildren, that pertain to Containers: they’re are
used to instruct the LayoutManager on how to automatically arrange their
contained elements.
Depending on your preference, it’s possible to exactly size and position each
Group, Slider, etc. within the Window, or use the built-in ScriptUI capacity
to deal with the GUI’s geography, based on the above mentioned (and few
other) properties.
Personally, I tend to use automatic layouts most of the times; be aware that
older Photoshops can handle elements in a slightly different way, so your
perfectly arranged layout may break a little bit – check it on all versions if
you care about backward compatibility.

Orientation

With possible values equal to 'row', 'column' or 'stack', the orientation determines whether the
child elements of a Window or Container should be laid down in a row, a column, or allowed to be
piled one on top of the other.

1 var d = new Window('dialog', 'Options');


2 d.alignChildren = ['fill', ''];
3
4 var p = d.add('panel', undefined, 'Delete');
5 p.orientation = 'stack';
6 p.alignChildren = ['left','']
7 var c1 = p.add('checkbox', undefined, 'Layers');
8 var c2 = p.add('checkbox', undefined, 'Channels');
9 var c3 = p.add('checkbox', undefined, 'Paths');
10
11 d.show();

Column, row, and stack orientation


User Interfaces 203

As you see, the 'stack' orientation implies that the topmost element (the first declared) hides the
ones below it – it has a higher z-index, one would say. The automatic layout is disabled, and you
need to position elements manually.

1 var d = new Window('dialog', 'Stack example', [0,0,220,134]);


2 d.orientation = 'stack';
3
4 var b1 = d.add('button', undefined, 'Layers');
5 b1.location = [20,20];
6 b1.size = [100,30];
7
8 // alternatively, set the bounds straight into the constructor
9 d.add('button', { x:20, y:60, width:100, height:30 }, 'Channels');
10 d.add('button', { x:130, y:20, width:70, height:70 }, 'GO');
11 d.add('statictext', { x:20, y:92, width:180, height:30 },
12 'manually positioned elements');
13
14 d.center(); // centers the Window in the screen
15 d.show();

Please note that you must specify the Windows bounds for the
'stack' orientation to take effect. I’ve set the origin point to
[0,0], which is OK since the Dialog is then displayed in the
middle of the screen thanks to the center() call.
The position of every element is set with the location property
(either as an array, or an object literal with x and y props),
while the size determines width and height. This can be handy
if you need to create dynamically and space Controls using,
say, a pointer: otherwise, a bounds object fed to the constructor
works the same.

align and alignChildren

Back to automatic layout¹¹, as soon as you’ve defined either row or column as the orientation in a
Container, you may want to set the specific way each child Control should horizontally or vertically
align in there – which is what alignChildren is all about.
If the Controls are lined in a row, alignment is vertical: either top, center, bottom; whereas in column
arrangements you can set horizontal alignment: left, center, right. A special fill constant makes
the control occupy all the available space in the specified direction (horizontal or vertical).
¹¹Alignment also applies to stacked elements, that instead, I tend to arrange manually.
User Interfaces 204

Let’s look at aligning the children in a row orientation first (y axis):

1 var d = new Window('dialog', 'alignChildren: Rows');


2 d.size = [750,100];
3 d.orientation = 'row';
4 d.alignChildren = ['fill','fill'];
5
6 var p1 = d.add('panel', undefined, 'top');
7 p1.orientation = 'row';
8 p1.alignChildren = 'top';
9 p1.add('edittext', [0,0,40,20]);
10 p1.add('edittext', [0,0,40,20]);
11 p1.add('edittext', [0,0,40,20]);
12
13 var p2 = d.add('panel', undefined, 'center');
14 p2.orientation = 'row';
15 p2.alignChildren = 'center';
16 p2.add('edittext', [0,0,40,20]);
17 p2.add('edittext', [0,0,40,20]);
18 p2.add('edittext', [0,0,40,20]);
19
20 var p3 = d.add('panel', undefined, 'bottom');
21 p3.orientation = 'row';
22 p3.alignChildren = 'bottom';
23 p3.add('edittext', [0,0,40,20]);
24 p3.add('edittext', [0,0,40,20]);
25 p3.add('edittext', [0,0,40,20]);
26
27 var p4 = d.add('panel', undefined, 'fill');
28 p4.orientation = 'row';
29 p4.alignChildren = 'fill';
30 p4.add('edittext', [0,0,40,20]);
31 p4.add('edittext', [0,0,40,20]);
32 p4.add('edittext', [0,0,40,20]);
33
34 d.show();
User Interfaces 205

The special array syntax has been used in the Window container (line 4), where fill has been set
for both horizontal and vertical alignment¹². This means that the four Panels have been set free to
inflate, and become as tall and wide as the Container allows them: in fact, I did not specify height
or width for them – the only explicit size is the Window’s.
Now let’s check Column orientation, in which the most evident axis is the y.

1 var d = new Window('dialog', 'alignChildren: Columns');


2 d.size = [600,160];
3 d.orientation = 'row';
4 d.alignChildren = ['fill','fill'];
5
6 var p1 = d.add('panel', undefined, 'left');
7 p1.alignChildren = 'left';
8 p1.add('edittext', [0,0,40,20]);
9 p1.add('edittext', [0,0,20,20]);
10 p1.add('edittext', [0,0,60,20]);
11
12 var p2 = d.add('panel', undefined, 'center');
13 p2.alignChildren = 'center';
14 p2.add('edittext', [0,0,40,20]);
15 p2.add('edittext', [0,0,20,20]);
16 p2.add('edittext', [0,0,60,20]);
17
18 var p3 = d.add('panel', undefined, 'right');
19 p3.alignChildren = 'right';
20 p3.add('edittext', [0,0,40,20]);
21 p3.add('edittext', [0,0,20,20]);
22 p3.add('edittext', [0,0,60,20]);
23
24 var p4 = d.add('panel', undefined, 'fill');
25 p4.alignChildren = 'fill';
26 p4.add('edittext', [0,0,40,20]);
27 p4.add('edittext', [0,0,20,20]);
¹²The order is [horizontal,vertical].
User Interfaces 206

28 p4.add('edittext', [0,0,60,20]);
29
30 d.show();

Also note that, even if in columns the most meaningful alignment axis is the x, there’s an implicit
top alignment for the y: as you see in the screenshot, they’re grouped towards the Panel’s ceiling.

While a general alignment policy for all the children is convenient, single child elements (either
nested Containers and Controls) can override the parent rule and move to a different position
according to their own alignment property. Please note that while alignChildren is a Container-
only prop, that affects the contained elements, alignment applies to both Containers and Controls,
directly affecting them.

1 /* Excerpt from the code: find the full version in the bundled code folder */
2 var d = new Window('dialog');
3 d.orientation = 'row';
4 d.alignChildren = ['fill','fill'];
5 // ...
6
7 var cp = d.add('panel', undefined, 'Vertical alignment in columns');
8 cp.orientation = 'row';
9 cp.spacing = 4;
10 cp.margins = 20;
11 cp.alignChildren = ['fill','fill'];
12 cp.size = [400,200];
13
14 var cpp1 = cp.add('panel');
15 cpp1.orientation = 'column';
16 cpp1.alignChildren = ['fill','top'];
17 cpp1.spacing = 2;
18 cpp1.margins = 2;
19 var b1 = cpp1.add('button', undefined, 'top'); // no need to override
20 var b2 = cpp1.add('button', undefined, 'center');
User Interfaces 207

21 b2.alignment = ['fill', 'center'];


22 var b3 = cpp1.add('button', undefined, 'bottom');
23 b3.alignment = ['fill', 'bottom'];
24
25 var cpp2 = cp.add('panel');
26 cpp2.orientation = 'column';
27 cpp2.alignChildren = ['fill','top'];
28 cpp2.spacing = 2;
29 cpp2.margins = 2;
30 var b4 = cpp2.add('button', undefined, 'top'); // no need to override
31 var b5 = cpp2.add('button', undefined, 'top'); // no need to override
32 var b6 = cpp2.add('button', undefined, 'bottom');
33 b6.alignment = ['fill', 'bottom'];
34
35 var cpp3 = cp.add('panel');
36 cpp3.orientation = 'column';
37 cpp3.alignChildren = ['fill','top'];
38 cpp3.spacing = 2;
39 cpp3.margins = 2;
40 var b4 = cpp3.add('button', undefined, 'top'); // no need to override
41 var b5 = cpp3.add('button', undefined, 'bottom');
42 b5.alignment = ['fill', 'bottom'];
43 var b6 = cpp3.add('button', undefined, 'bottom');
44 b6.alignment = ['fill', 'bottom'];
45 // ... etc.

Margins and Spacing

Lastly, two Containers-only properties help to craft your layouts:

• margins are the number of pixels between the edges of a container and its external child
elements (what in web development would be called padding).
• spacing is the number of pixels separating one child element from its adjacent sibling element
(what in web development would be called margins, which is kind of awkward, isn’t it?)

While margins can be an array of [left, top, right, bottom] numbers, an object with the same
properties, or a single number (to set them all equal), spacing is always a single number. A visual
illustration is as follows.
User Interfaces 208

7.7 Coding Conventions


So far I’ve used a rather standard way of building the Dialog, but there are several different styles
that developers may want to use. Let’s take as an example this very simple GUI:
Variables based style

1 var d = new Window('dialog');


2 d.text = 'Coding Conventions';
3 d.orientation = 'column';
4 d.alignChildren = ['fill','fill'];
5 var p = d.add('panel');
6 p.orientation = 'row';
7 p.text = "Radius";
8 var s = p.add('slider');
9 s.size = [200,30];
10 s.value = 10;
11 var e = p.add('edittext', undefined, '10');
12 var g = d.add('group');
13 g.alignChildren = ['right','fill'];
14 var c = g.add('checkbox');
15 c.alignment = ['left','fill'];
16 c.text = "Preview";
17 c.value = true;
18 var cb = g.add('button', undefined, 'Cancel');
19 var ob = g.add('button', undefined, 'Ok');
20 d.show();

As a side note, I might have been influenced by an article called Why I Do Not Use Meaningful
Variable Names (Anymore) written by the InDesign developer Marc Autret, since I’ve started using
minimal names myself too – even if I haven’t such a strict naming convention as Marc has. However,
that’s not the point of this section.
User Interfaces 209

This tiny Dialog is quite simple: the Win-


dow orientation is set to column, and the
two main elements are a Panel and a Group
(which is, basically, a hidden container). Each
one of them holds a row of elements: a Slider
and an EditText the first, a Checkbox and
a couple of Buttons the second. Please note
that the Checkbox uses the alignment prop to
override the Group’s alignChildren, as we’ve
seen in the previous example.
I used to call this coding style “Variable based”, because each element that I may need to refer to
(either now, while building the dialog structure, or later, when I’ll be wiring event listeners) is stored
into a variable. Retrieving values is quite easy – say that you want to know whether the Preview
checkbox is ticked: it’s just a matter of checking c.value.
Besides this approach, others are using the Dialog object itself to keep track of the elements, so that
the same code becomes:
Object based style

1 var dlg = new Window('dialog');


2 dlg.text = 'Coding Conventions';
3 dlg.orientation = 'column';
4 dlg.alignChildren = ['fill','fill'];
5 dlg.radiusPanel = dlg.add('panel');
6 dlg.radiusPanel.orientation = 'row';
7 dlg.radiusPanel.text = "Radius";
8 dlg.radiusPanel.radiusSlider = dlg.radiusPanel.add('slider');
9 dlg.radiusPanel.radiusSlider.size = [200,30];
10 dlg.radiusPanel.radiusSlider.value = 10;
11 dlg.radiusPanel.radiusText = dlg.radiusPanel.add('edittext', undefined, '10');
12 dlg.buttonsGroup = dlg.add('group');
13 dlg.buttonsGroup.alignChildren = ['right','fill'];
14 dlg.buttonsGroup.preview = dlg.buttonsGroup.add('checkbox');
15 dlg.buttonsGroup.preview.alignment = ['left','fill'];
16 dlg.buttonsGroup.preview.text = "Preview";
17 dlg.buttonsGroup.preview.value = true;
18 dlg.buttonsGroup.cancelBtn = dlg.buttonsGroup.add('button', undefined, 'Cancel');
19 dlg.buttonsGroup.okBtn = dlg.buttonsGroup.add('button', undefined, 'Ok');
20 dlg.show();

I’ve used more meaningful names here, but the critical point is that each element is stored as a
property within the dlg object, and can be accessed using the dot syntax; e.g., the Preview value is
User Interfaces 210

dlg.buttonsGroup.preview.value. Also note that the indentation is optional, and added for clarity’s
sake.
The next option makes use of a so-called “Resource String”: an object-based notation, embedded
within a long string, that is then fed to the Window constructor. Please note that ExtendScript
requires a backslash \ at the end of each line, to consider the string as multiline – ESTK gives you
instant feedback if the code is properly formatted or not, e.g., there’s an extra white space after the
backslash. Alternatively, you can wrap the string with triple """ quotes¹³:

1 var s1 = "This is a string that can be written\


2 using several \
3 lines";
4 // This is a string that can be written using several lines
5 var s2 = """This is
6 a true
7 multiline string""";
8 // This is
9 // a true
10 // multiline string

The code from our test dialog becomes:


Resource String based style

1 var res = """dialog {


2 text: 'Coding Conventions',
3 orientation: 'column',
4 alignChildren: ['fill','fill'],
5 radiusPanel: Panel {
6 orientation: 'row',
7 text: 'Radius',
8 radiusSlider: Slider {
9 size: [200,30],
10 value: 10
11 },
12 radiusText: EditText {
13 text: 10
14 }
15 },
16 buttonsGroup: Group {
17 alignChildren: ['right','fill']
18 preview: Checkbox {
¹³The triple quotes """ used in the context of a String will keep track of the existing carriage returns as well, transforming their content
into an actual multi-lines string, whereas the backslash \ is just a handy way for you to spread the text to several lines – nevertheless the result
is a one-line String.
User Interfaces 211

19 alignment: ['left','fill'],
20 text: 'Preview',
21 value: true
22 },
23 cancelBtn: Button { text: 'Cancel'},
24 okBtn: Button { text: 'Ok'}
25 }
26 }""";
27 var dlg = new Window(res);
28 dlg.show();

The example value of Preview here is found precisely like in the previous example. I tend to use
Resource Strings a lot: the only downside is that when there’s something wrong (and please note I
didn’t write “if”: it’s just a matter of time) the error that is thrown is somewhat difficult to interpret.
For instance, I’ve tried misspelling the cancel Buton, and I’ve got: "Bad argument: Invalid resource
format. Error in line 23, at character offset 246, in {...} - Buton is not an object"
with all the string repeated between curly braces. And this only if you wrap the whole code with a
try/catch block and log the error to the Console, otherwise ESTK truncates the message. That aside,
I like the compactness and clarity of this code.
Please note that in the Resource String, element names are written using capital letters, so
'edittext' becomes 'EditText', etc.

There’s a fourth, hybrid approach, that uses bits of resource strings in one of the traditional
conventions. For instance, here’s the variables style mixed with strings:
Hybrid style – Variable and Resource Strings

1 var d = new Window('dialog');


2 d.text = 'Coding Conventions';
3 d.orientation = 'column';
4 d.alignChildren = ['fill','fill'];
5 var p = d.add("Panel { orientation: 'row', text: 'Radius' }");
6 var s = p.add("Slider { size: [200,30], value: 10 }");
7 var e = p.add("EditText { text: 10 }");
8 var g = d.add("Group { alignChildren: ['right','fill'] }");
9 var c = g.add("Checkbox { alignment: ['left','fill'], \
10 text: 'Preview', value: true }");
11 var cb = g.add("Button { text: 'Cancel'}");
12 var ob = g.add("Button { text: 'Ok'}");
13 d.show();

This one is the most compact version of all (also because I’ve returned to short variable names). It
might be easier to debug, and it represents a valid alternative indeed.
User Interfaces 212

7.8 Components minimal reference


Now that you’ve got a proper understanding about layout tools and coding conventions, before going
any further I’d like to briefly look at essential GUI elements, with very minimal code examples on
their creation properties – e.g. name, shared by all Containers and Controls.
Group, as you know is the basic invisible Container:

var d = new Window('dialog');


var grp = d.add('group', [0,0,100,100]);

Panel is a group with default border and a text description:

d.add('panel', undefined, 'black', { borderStyle: 'black' });

The border can be styled dadaistically in five different ways, only three of which kind-of work:
'black',
the default 'etched', that to my eyes looks quite flat and gray, and 'gray', that appears
undeniably white.
Tabbed Panels also must contain Tabs, which in turn are regular Containers (below, I’ve put the
ubiquitous button):

1 var tp = d.add('tabbedpanel', [0,0,200,100]);


2 var tab1 = tp.add('tab', undefined, 'First'),
3 tab2 = tp.add('tab', undefined, 'Second'),
4 tab3 = tp.add('tab', undefined, 'Third');
5 var btn1 = tab1.add('button', [60,20,50,30], 'Nice Tab');

Buttons are everywhere, just remember that if the name is either cancel or ok (case insensitive),
the button itself and every sibling of it belonging to the same container gets the CTA treatment: by
default bigger, and wide rounded corners – if you want to keep them separate, add child Groups
User Interfaces 213

Similar to Buttons are IconButtons, which have two interest-


ing features: they support an image (a .png file), or can behave
like toggle buttons, where the button value prop is linked to
the “pressed” status. Alternatively, at least this is what they’re
supposed to do since the current version is broken when it
comes to title and text properties – see in the next screenshot
CC 2014 compared to CC 2017

1 var winRes = """dialog {


2 orientation: 'row',
3 alignChildren: ['fill', 'fill'],
4 g1: Group {
5 orientation: 'column',
6 btn: Button { text: 'OK', alignment: ['','center'] },
7 st: StaticText { text: 'CTA Button', alignment: ['','bottom'] }
8 },
9 g2: Group {
10 orientation: 'column',
11 btn: Button { text: 'Apply', alignment: ['','center'] },
12 st: StaticText { text: 'Button', alignment: ['','bottom'] }
13 },
14 g3: Group {
15 orientation: 'column',
16 btn: IconButton {
17 alignment: ['','center'],
18 properties: {style: 'toolbutton'}
19 },
20 st: StaticText { text: 'iconbutton', alignment: ['','bottom'] }
21 }
22 g4: Group {
23 orientation: 'column',
24 btn: IconButton {
25 alignment: ['','center'],
26 title: 'Download',
27 properties: {style: 'button'}
28 },
29 st: StaticText { text: 'iconbutton', alignment: ['','bottom'] }
30 }
31 g5: Group {
32 orientation: 'column',
33 btn: IconButton {
34 alignment: ['','center'],
35 properties: {toggle: 'true'}
User Interfaces 214

36 },
37 st: StaticText { text: 'iconbutton', alignment: ['','bottom'] }
38 },
39 g6: Group {
40 orientation: 'column',
41 btn: IconButton {
42 alignment: ['','center'],
43 properties: {toggle: 'true'}
44 },
45 st: StaticText { text: 'iconbutton', alignment: ['','bottom'] }
46 }
47 }""";
48 var dlg = new Window(winRes);
49 // Icons
50 var icon = File(File($.fileName).path + '/resources/download.png');
51 dlg.g3.btn.icon = icon;
52 dlg.g4.btn.icon = icon;
53 // Set these props here because in the Resource String they have no effect
54 // (BTW they're broken in CC 2018, work only until CC 2014)
55 dlg.g5.btn.text = "Toggle Button OFF";
56 dlg.g5.btn.value = false;
57 dlg.g6.btn.text = "Toggle Button ON";
58 dlg.g6.btn.value = true;
59
60 dlg.show();

As you see, you’re allowed to mix the coding conventions


(variable based and resource string), adding props later (I do this frequently, especially with styling
– covered later in this Chapter).
IconButtons share with Images a couple of interesting, recently added features: retina (high PPI)
User Interfaces 215

displays, and Photoshop themes (UI brightness) support.

1 var dlg = new Window('dialog');


2 var icon = File(File($.fileName).path + '/resources/download.png');
3 var i = dlg.add('image',undefined, icon)
4 dlg.show();

The above code presupposes the existence of a download.png file, better


if 24bit with transparency support, in the specified path. A2X file¹⁴ can
be provided to support retina displays, with the @2X suffix (so in this
case download@2X.png). Moreover, the @Dark suffix is available too, to
support dark gray themes. As a result, you’ll have:

• download.png – standard size, light theme;


• download@2X.png – double size, light theme;
• download@Dark.png - standard size, dark theme;
• download@2X@Dark.png - double size, dark theme;

Be aware that the default sized file (no @2X suffix) must exist, and the light theme files are going to
be used by default with every theme if the @Dark version is not available¹⁵.
It’s worth mentioning that you can also embed images as binary strings: it’s a matter of reading the
image file (e.g. .png) on disk, and writing it back on a .txt file for convenience.

1 var imageFile = File(File($.fileName).path + '/resources/Flower.png');


2 imageFile.open('r');
3 imageFile.encoding = 'binary';
4 // Read it and save the result in a variable
5 var binaryString = imageFile.read();
6 imageFile.close();
7 // Create the output textfile (Flower.png.txt)
8 var textFile = new File(imageFile.fullName + ".txt");
9 textFile.open ("w");
10 textFile.encoding = "binary";
11 // Use toSource() to stringify it
12 textFile.write (binaryString.toSource ());
13 textFile.close();

The trick, so to speak, is to use .toSource() while writing the binary string in the text file. As a
result, you’re going to get Flower.png.txt, which content looks like:
¹⁴In fact, it’s four times the resolution, but two times the sides: if the original image is, say, 100x100 pixels, the @2X is 200x200 (40K against
10K total pixels).
¹⁵Which means that you need to come up with an image that works with both dark and light backgrounds.
User Interfaces 216

(new String("\u0089PNG\r\n\x1A\n\x00\x00\x00\rIHDR\x00\x00\x00(\x00\x00\x00\x1E\
b\x03\x00\x00\x00i\x03\u00AC\u00EF\x00\x00\x00\x19tEXtSoftware\x00AdobeImageRead
yq\u00C9e<\x00\x00\x03(iTXtXML:com.adobe.xmp\x00\x00\x00\x00\x00<?xpacket begin=
\"\u00EF\u00BB\u00BF\" id=\"W5M0MpCehiHzreSzNTczkc9d\"?> <x:xmpmeta xmlns:x=\"ad
...
... many other lines...
...
x02\f\x00\u00F1\u00E3\u00A2\x16Q.\u00B3\x02\x00\x00\x00\x00IEND\u00AEB`\u0082"))

You need to strip the initial (new String( and the final )) characters manually, and paste the
resulting long, long string into a variable: it works like loading a regular .png file from disk.

1 var dlg = new Window('dialog');


2 // Instead of this:
3 // var flower = File(File($.fileName).path + '/resources/Flower.png');
4 // Use this:
5 var flower = "\u0089PNG\r\n\x1A\n\x00\x00/* ................................ */x00IEND\u00AEB`\u0082"
6 var i = dlg.add('image',undefined, flower);
7 dlg.show();

The simpler form of text possible, StaticText is used for titles, labels, or descriptive texts; creation
properties such as multiline and scrolling are used for long strings – although you’ll see that the
look is quite different.

1 var dlg = new Window('dialog');


2 dlg.orientation = 'row';
3 dlg.alignChildren = ['fill','top']
4
5 var bounds = [0,0,150,60];
6 var text1 = "A static text";
7 var text2 = "A static text that can span through multiple lines";
8 var text3 = "A static text that can span through multiple lines, /* etc */";
9
10 var t1 = dlg.add('statictext', bounds, text1);
11 var t2 = dlg.add('statictext', bounds, text2, {multiline:true});
12 var t3 = dlg.add('statictext', bounds, text3, {multiline:true, scrolling:true});
13
14 dlg.show();
User Interfaces 217

A different kind of text is EditText, a field that the user can type in, copy from, or paste to. The
aspect of such Control has changed in recent years, and some features (such as borderless) are still
buggy.

1 var dlg = new Window('dialog');


2 // etc. same as the previous example
3
4 var t1 = dlg.add('edittext', bounds, text1);
5 var t2 = dlg.add('edittext', bounds, text2, {multiline:true});
6 var t3 = dlg.add('edittext', bounds, text3, {multiline:true, scrolling:true});
7 var t4 = dlg.add('edittext', bounds, text4, {borderless:true });
8 var t5 = dlg.add('edittext', bounds);
9 t5.textselection = "Selected text";
10 t5.active = true; // only way to make textselection actually selected
11 // noecho is for password fields
12 var t6 = dlg.add('edittext', bounds, text1, {noecho:true});
13
14 dlg.show();

Checkboxes and RadioButtons are also frequently used (the latter grouped, to trigger their peculiar
behavior).

1 var dlg = new Window('dialog');


2 dlg.orientation = 'row';
3 dlg.alignChildren = ['fill','fill'];
4
5 var p1 = dlg.add('panel', undefined, 'checkbox');
6 p1.alignChildren = ['left','top'];
7 var cb1 = p1.add('checkbox', undefined, 'Option 1');
8 var cb2 = p1.add('checkbox', undefined, 'Option 2');
9 var cb3 = p1.add('checkbox', undefined, 'Option 3');
User Interfaces 218

10 cb1.value = true;
11 cb3.value = true;
12
13 var p2 = dlg.add('panel', undefined, 'radiobutton');
14 p2.alignChildren = ['left','top'];
15 var rb1 = p2.add('radiobutton', undefined, 'Choice 1');
16 var rb2 = p2.add('radiobutton', undefined, 'Choice 2');
17 rb2.value = true;
18 var rb3 = p2.add('radiobutton', undefined, 'Choice 3');
19 dlg.show();

For both Controls, the text property works as the label, and
value deals with the selected/unselected status: it’s read/write,
so (as you see in the above snippet), you can programmatically
select them.
If querying the single checkbox value is the way to retrieve its
“checked” status, to get the selected radiobutton contained in
the group, you need to loop through them, as children of their
parent Group.

1 for (var i = 0, len = p2.children.length; i < len; i++) {


2 if (p2.children[i].value == true) { $.writeln(p2.children[i].text) }
3 }

A ProgressBar is what you would expect.

1 var dlg = new Window('dialog');


2 var pb = dlg.add('progressbar', undefined, 1, 100);
3 pb.value = 0;
4 var btn = dlg.add('button', undefined, 'Progress...');
5 // onClick function will be covered later
6 btn.onClick = function() {
7 pb.value +=10;
8 if (pb.value > 99) { dlg.close(); }
9 }
10 dlg.show();

I’ve always been using Progress bars in their own, pop-up 'palette' (very much like native Filters’
progress bars), but lately, they don’t seem to work correctly anymore.
User Interfaces 219

In the above snippet, the button itself triggers the bar advancement via the
value property, and closes the dialog when it’s done. I’ll cover onClick()
and other Events callbacks in a short while.
Among Controls with sub-optimal behavior, Scrollbars deserve a place in
the list; when they’re automatically added in Text fields (either edittext
or statictext) they’re fine and, especially on Mac, not very intrusive.
Expressely creating scrollbars make them bigger (and frankly quite ugly),
and when it comes to their behavior – I mean: what they’re supposed to do – you’re on your own.

Peter Kahrel has an excellent section on scrollbars, that


if you’re interested in the topic, I urge you to read.
Basically, it’s a matter of creating a viewport, e.g., a
bigger (either taller or wider) container, which you
can only see a portion of: scrollbars programmatically
change its origin (move it), so that the moving container
gives the impression of content scrolling. Don’t use
Scrollbars as sliders – bad times on Mac.
Sliders are the proper Control used to pick a value in a range; besides the value, they have a minvalue
and maxvalue; in the following snippet, there are two Sliders, one going from zero to twenty-five,
the other ranging from minus twenty-five to twenty-five.

1 var dlg = new Window('dialog');


2 dlg.orientation = 'row';
3 dlg.alignChildren = ['fill','top'];
4 var hg = dlg.add('group');
5 hg.orientation = 'column';
6 hg.alignChildren = ['right','top'];
7 var g1 = hg.add('group');
8 g1.orientation = 'row';
9 g1.alignChildren = ['right','bottom'];
10 // dlg.add('slider', bounds, value, minvalue, maxvalue);
11 var st1 = g1.add('statictext', undefined, '[0..50]');
12 var sl1 = g1.add('slider', [0,0,150,40], 25, 0, 50);
13 var et1 = g1.add('edittext', undefined, sl1.value);
14 var g2 = hg.add('group');
15 g2.orientation = 'row';
16 g2.alignChildren = ['right','bottom'];
17 // dlg.add('slider', bounds, value, minvalue, maxvalue);
18 var st2 = g2.add('statictext', undefined, '[-25..25]');
19 var sl2 = g2.add('slider', [0,0,150,40], 0, -25, 25);
20 var et2 = g2.add('edittext', undefined, sl2.value);
21 var vg = dlg.add('group');
User Interfaces 220

22 vg.orientation = 'column';
23 var sl3 = vg.add('slider',[0,0,25,100]);
24
25 dlg.show();

It’s possible to create a vertical slider setting the bounds


or size appropriately, with the height higher than the
width (e.g. [0,0,25,100]). In the past, vertical sliders
were available on Mac only, but with recent versions of
Photoshop, you can create them on both platforms.
There is no “step” equivalent for the Slider (a property
that takes into account the smallest amount of change
while dragging the handler, e.g., 0.1 or 1 – you need to work around this yourself manipulating the
Slider’s value; more on that later).
Listbox is a container of ListItems: in other words, a table-like list of items. A few examples as
follows:

1 // Simple Listbox
2 var dlg = new Window('dialog', 'Simple');
3 dlg.orientation = 'row';
4 dlg.alignChildren = ['fill','top'];
5
6 var items = ['Photoshop', 'InDesign', 'Illustrator'];
7
8 var lb1 = dlg.add('listbox', undefined, items, {
9 showHeaders: true,
10 // Titles aren't shown in PS anyway...
11 columnTitles: ['CC Apps']
12 });
13 lb1.size = [100,100];
14
15 dlg.show();
16
17 // Multi Column – Won't work at all in PS
User Interfaces 221

18 // (Window constructor like in the previous example)


19
20 var lb1 = dlg.add('listbox', undefined, undefined, {
21 numberOfColumns: 3,
22 showHeaders: true,
23 columnTitles: ['CC Apps', 'Domain', 'Do I use it']
24 });
25 lb1.size = [300,100];
26
27 var ps = lb1.add('item','Photoshop');
28 ps.subItems[0].text = 'Sure enough';
29 ps.subItems[1].text = 'Daily';
30
31 var id = lb1.add('item','InDesign');
32 id.subItems[0].text = 'Sort of';
33 id.subItems[1].text = 'Rarely';
34
35 var il = lb1.add('item','Illustrator');
36 il.subItems[0].text = 'Not really';
37 il.subItems[1].text = 'Never';
38
39 // Thumbnails and Selection
40 // (Window constructor like in the previous example)
41
42 var lb1 = dlg.add('listbox');
43 lb1.size = [300,100];
44
45 var ps = lb1.add('item','Photoshop');
46 ps.image = new File("/* Valid path to Image, e.g. PNG */");
47
48 var id = lb1.add('item','InDesign');
49 id.selected = true;
50
51 var il = lb1.add('item','Illustrator');
52 il.checked = true; // Won't work
53
54 dlg.show();

Listboxes are another Control for which the implementation doesn’t shine: multi-column lists don’t
work anymore with Mondo (from CC 2015 included, onwards); items’ tick won’t show; header won’t
show either. It seems like these features have been disregarded when porting to the new rendering
engine, and it’s not clear when (or even if) fixes will be released.
I won’t cover TreeView here since it is extinct with Mondo (please note that it works on other apps
User Interfaces 222

such as InDesign since they use a different framework).


Similar to the Listbox, the DropDownList too contains ListItems, and does the same job more
discreetly: it opens on click – if you’re versed in HTML, it’s the equivalent of the <s > tag. There
are also Bugs in showing the thumbnail when the open list item is selected (apparently, there’s no
way to make it appear in the closed list).

1 /* Simple DropDownList */
2 var dlg = new Window('dialog', 'Simple');
3 dlg.orientation = 'row';
4 dlg.alignChildren = ['fill','top'];
5
6 var items = ['Photoshop', 'InDesign', 'Illustrator']
7
8 var lb1 = dlg.add('dropdownlist', undefined, items);
9 lb1.selection = 2;
10
11 dlg.show();
12
13 /* With separator and thumbs */
14 // (Window constructor like in the previous example)
15
16 // '-' act as a separator
17 var items = ['Photoshop', 'InDesign', '-', 'Affinity Designer'];
18
19 var lb1 = dlg.add('dropdownlist', undefined, items);
20 lb1.preferredSize = [200, 40];
21
22 lb1.selection = 0; // Preselected item
23 lb1.items[3].text = "Illustrator" // You can change it...!
24 lb1.items[2].enabled = false; // Make the separator unselectable
25 lb1.items[0].image = new File ("/* Valid path to Image, e.g. PNG */");
26
User Interfaces 223

27 dlg.show();

Custom is an often disregarded component, the reason being that it provides you with nothing but a
blank canvas that you can fill via its onDraw() function: in fact, a Custom element has no predefined
aspect and behavior whatsoever, which is up to you to set. In my experience, its usefulness is twofold:
you can draw on it (e.g., plot lines and shapes); and it can mimic an existing component, working
around the original component bugs, if any. An example will be presented only after we’ve talked
about Events.

7.9 Events, and Event handlers


The whole point of building a GUI is to let users interact with it. Each time you click a button, select
a checkbox, type in a TextEdit field, Events are fired; they can also be built ad hoc, programmatically.
One way or the other, listening for, and respond to Events is a crucial part of any ScriptUI code.

Window and Controls Callbacks


The easiest way to respond to an Event is to use the Event-handling callbacks that ScriptUI makes
available for each type of object (Window, Panel, Button, etc.), as camelCase functions named on
plus the Event you’re interested into (e.g. onClick, onChange, etc.). A simple example is as follows:

1 var d = new Window('dialog', 'Simple Handlers',


2 undefined, { resizeable:true } );
3 d.preferredSize = [50,100];
4 var btn1 = d.add('button', undefined, 'Click me...');
5 var btn2 = d.add('button', undefined, 'Close the Window');
6
7 // Dialog Callbacks
8 d.onMove = function() { alert("Window Moved") }
9 d.onClose = function() { alert("Window Closed") }
10 d.onResize = function() { alert("Window Resized") } // Won't work
11
12 // Buttons Callbacks
13 btn1.onClick = function() { alert("Button Clicked") }
14 btn2.onClick = function() {
15 alert("About to be closed...")
16 d.close();
17 }
18
19 d.center();
20 d.show();
User Interfaces 224

As you see, you’ve attached functions to the .onMove, .onClose,


.onResize methods of the Window object, and one function for each
button’s onClick.
Things won’t exactly go as expected if you run the code (at least on
a Mac). At first, you’ll get a “Window Resized” alert for no reason,
followed by a similarly puzzling “Window Moved”. You might think
that the reason is the d.center() at line 17 – the Window is created,
then moved to the center, hence the Event – yet, if you comment this out, the popup still appears:
it’s just another bug. Also, if you resize the Dialog when it finally shows, nothing happens in terms
of alerts.
On the other hand, moving the Window afterward triggers its callback; also, the bt1 click handler
works properly. When you click the second button, “About to be closed…” appears first, then the
dialog fires the .onClose() callback attached to the Window, and you get the final “Window Closed”
message¹⁶.
Please note that you can assign to the handler either an anonymous or a named function, it’ll work
the same:

1 // Anonymous function
2 btn1.onClick = function() { alert("Button Clicked") }
3
4 // Named function
5 function btn1ClickHandler() { alert("Button Clicked") }
6 btn1.onClick = btn1ClickHandler;
7
8 // If you use a function expression, make sure you declare the variable
9 // before assigning it to the onClick callback otherwise it'll be undefined.
10 var btn1ClickHandler = function() { alert("Button Clicked") } // HERE'S OK
11 btn1.onClick = btn1ClickHandler;
12
13 // This won't work because when assigned, the btn1ClickHandler is still undefined
14 btn1.onClick = btn1ClickHandler;
15 var btn1ClickHandler = function() { alert("Button Clicked") } // NOT HERE!

There is a limited list of possible handlers that work this way (see the JS Tools Guide, page 122),
here’s a commented selection for the Window object (JS Tools Guide, page 122).

• onActivate: Called when the user makes the window active by clicking it or otherwise making
it the keyboard focus. It may be called twice, but it appears to work.
• onClose: Called when a request is made to close the window, either by an explicit call to the
close() function or by a user action (clicking the OS-specific close icon in the title bar). The
¹⁶If you dismiss the dialog with the esc key, the only dialog you’ll get is “Window Closed”.
User Interfaces 225

function is called before the window closes; it can return false to cancel the close operation. It
works properly.
• onDeactivate: Called when the user makes a previously active window inactive; for instance
by closing it, or by clicking another window to change the keyboard focus. I’ve not been able
to make it work properly, it fires multiple times for no reason.
• onDraw: Called when a container or control is about to be drawn. Allows the script to modify
or control the appearance, using the control’s associated ScriptUIGraphics object. The handler
takes one argument, a DrawState object. Mostly useful with Custom Components.
• onMove: Called when the window has been moved. It fires unnecessarily when the Window is
created, and then the behavior appears to be regular.
• onMoving: Called while a window is being moved, each time the position changes. It doesn’t
work.
• onResize: Called when the window has been resized. It doesn’t work.
• onResizing: Called while a window is being moved, each time the position changes. It doesn’t
work.
• onShow: Called when a request is made to open the window using the show() method before
the window is made visible, but after the automatic layout is complete. A handler can modify
the results of the automatic layout. It works properly.

For regular Components, the list is different; also note that one type of handler may apply to some
Components but not to others. For instance, while it makes sense to listen for onClick events in, say,
RadioButtons, Checkboxes, and Buttons, the onChange is of no use for Buttons. See page 147 of the
JS Tools Guide.

• onActivate: Called when the user gives a control the keyboard focus by clicking it or tabbing
into it.
• onClick: Called when the user clicks one of the following control types: Button, IconButton,
Checkbox, RadioButton.
• onChange: Called when the user finishes making a change in one of the following control types:
DropDownList, EditText, Listbox, Scrollbar, Slider.
• onChanging: Called for each incremental change in one of the following control types: EditText,
Scrollbar, Slider.
• onDeactivate: Called when the user removes keyboard focus from a previously active control
by clicking outside it or tabbing out of it.
• onDoubleClick: Called when the user double-clicks an item in a Listbox control. The list’s
selection property is set to the clicked item.
• onDraw: Called when a container or control is about to be drawn. Allows the script to modify
or control the appearance, using the control’s associated ScriptUIGraphics object. Used mostly
with Custom Components.
User Interfaces 226

Please note that these handlers are not passed any information about the original event
that has triggered them, as you would expect if you’re familiar with JavaScript; it’s not the
case here. You should think about these functions as somehow simplified callbacks that are
provided to you as handy shortcuts. For a more powerful event handling, you should use a
different system, which is the subject of the next section.

Event Listeners

The entire set of ScriptUI Events available is definitely larger than the few, more common callbacks
described so far. The proper way to implement Event handling is to use the addEventListener()
method. A simple example is as follows:

1 var d = new Window('dialog', 'addEventListener');


2 d.orientation = 'column';
3 d.alignChildren = ['fill','top']
4 d.preferredSize = [150,100];
5
6 var cb = d.add("CheckBox { text: 'Select me', value: false, \
7 properties: { name: 'myCheckBox' } }")
8 var et = d.add("EditText { properties: { name: 'myEditText' } }");
9 var sl = d.add("Slider { properties: { name: 'mySlider' } }")
10 var bt = d.add("Button { text: 'Click me', \
11 properties: { name: 'myButton' } }");
12
13 function commonHandler(evt) {
14 $.writeln(evt.timeStamp);
15 $.writeln("Event Type: " + evt.type);
16 $.writeln("Event Target: " + evt.currentTarget.properties.name);
17 $.writeln("Value: " + evt.currentTarget.value);
18 $.writeln("Text: " + evt.currentTarget.text);
19 $.writeln("\n=================================\n");
20 }
21
22 cb.addEventListener('click', commonHandler);
23 et.addEventListener('keydown', commonHandler);
24 sl.addEventListener('changing', commonHandler);
25 bt.addEventListener('click', commonHandler);
26
27 d.center();
28 d.show();
User Interfaces 227

See the new .addEventListener() method in lines 22-25. I’ve first created
a commonHandler() named function (declared in line 13), which is then at-
tached as a shared callback to all the four Components listeners: CheckBox,
EditText, Slider, and Button. Each one of them listens to its event: 'click',
'keydown' (keyboard keypress), 'changing'. As you see, the callback is
passed the event as a parameter. This allows you to process the event
more precisely – here I’m just logging some values, e.g., the Event Type, the
Target, etc. I already feel that you have several questions, so let me tackle
them one by one.
Q: How do I know all the Event properties that you’ve used (e.g. .type, .currentTarget, etc.)? A:
You can use either the Reflection interface, or more easily set a Breakpoint in ESTK (add a debugger
statement in the callback, or it won’t work):

Q: How do I know all the available Events I can listen for? A: First, you can match the simplified
handlers ('onClick', 'onChange', etc.) you’ve seen so far. Just remove the on prefix and use lowercase
letters: e.g. 'onClick' becomes 'click', 'onChange' becomes 'change', etc¹⁷. Additionally, ScriptUI
supports all W3C DOM level 3 events, with some exceptions described at page 83 of the JS Tools
Guide. The most widely used ones are:
'show', 'close', 'focus', 'mouseup', 'mousedown', 'mouseover', 'mouseout', 'mousemove', 'keyup',
'click', 'change', 'changing', 'move', 'moving', 'resize', 'resizing', 'enterKey', 'blur',
'keydown'.

Of course, use them in contexts where they make any sense: it’s OK to listen for the Window’s
'close' Event, which would be pointless for, say, an EditText. For details, check the simplified
handlers at page 122 and 147 of the JS Tools Guide.
Q: Can I listen to more than one event in the same component? A: Sure: say that you want to listen
for the 'change' and 'changing' Slider events, you can attach two different callbacks:

¹⁷These are the events you’ll feed .addEventListener() with.


User Interfaces 228

sl.addEventListener('change', aHandler);
sl.addEventListener('changing', anotherHandler);

Alternatively, you can use the same callback, and then check for the Event Type there:

1 function aHandler(evt) {
2 switch (evt.type) {
3 case 'change':
4 // do something
5 break;
6 case 'changing':
7 // do something else
8 break;
9 default:
10 // ...
11 }
12 }
13 sl.addEventListener('change', aHandler);
14 sl.addEventListener('changing', aHandler);

Q: What happens when a listener is attached to a Container, e.g., a Group? A: The Event is captured
for all the elements it contains. Which is quite handy, see the following example.

1 var d = new Window("dialog", "Grouped Events");


2 d.orientation = "column";
3 d.alignChildren = ["fill","top"]
4
5 var pnl = d.add("Panel { text: 'Pick a Font', \
6 alignChildren: ['left','top'], margins:20 }");
7
8 var rb1 = pnl.add("RadioButton { text: 'Source Sans' }");
9 var rb2 = pnl.add("RadioButton { text: 'Myriad Pro' }");
10 var rb3 = pnl.add("RadioButton { text: 'Avenir' }");
11 var rb4 = pnl.add("RadioButton { text: 'Museo', value: true }");
12 var rb5 = pnl.add("RadioButton { text: 'Papyrus' }");
13 var btn = d.add("Button { text: 'Close', properties: { name: 'Cancel' }}")
14
15 pnl.addEventListener('click', function(evt) {
16 for (var i = 0; i < pnl.children.length; i++) {
17 if (pnl.children[i].value == true) break;
18 };
19 $.writeln("Selected: " + pnl.children[i].text);
User Interfaces 229

20 })
21
22 d.center();
23 d.show();

The Dialog contains a Panel, which in turn contains five RadioButtons


with Font names (I’m using the mixed notation because I find it handy
for these short snippets). When housed in a Container, RadioButtons
behave the way you expect them to do: only one can be selected at any
given moment – as with CheckBoxes, the value property defines the
selection status.
The Panel, and not the single RadioButtons, is attached a 'click'
Event Listener: this way, each click that happens inside such Container
triggers the handler.
To log the currently selected Font, I must loop through the available
RadioButtons (as children of the Panel), and look for the value that
it’s true: when it’s found, the loop quits. The index is then used to
extract the text property, that is finally logged in the Console. Instead
of the pnl variable, I could have used this, because in such context it points to the element the
callback it’s attached to (the Panel itself).
Q: Container listeners are great, but how do I deal with them when the scenario is less simple? A:
In fact, it can be tricky! Let’s add one extra feature to our last example:

1 var d = new Window("dialog", "Decoupling Events");


2 d.orientation = "column";
3 d.alignChildren = ["fill","top"]
4
5 var lb = d.add("Listbox { properties: { items: \
6 ['Source Sans', 'Myriad Pro', 'Avenir', 'Museo', 'Papyrus'] } }");
7 var bt = d.add("Button { text: 'Select random Font' }");
8
9 // Listbox handler
10 lb.addEventListener('change', function () {
11 $.writeln("Listbox Change fired: " +
12 this.children[this.selection.index].text);
13 });
14 // Button handler
15 bt.addEventListener('click', function () {
16 $.writeln("Button clicked.");
17 var i = Math.floor( 5 * Math.random());
User Interfaces 230

18 lb.children[i].selected = true;
19 });
20
21 d.center();
22 d.show();

Instead of RadioButtons in a Panel, I’ve used a Listbox to gather


the five items. The button now picks a random one (bt listens for
the click event; the callback computes a random integer between
0 and 4 and uses it as the index for the Listbox selected item).
Alternatively, you can manually pick a Listbox element yourself
just clicking on it (in this case, lb listens for the change event, and
logs the item).
As I’ve mentioned earlier, it’s perfectly fine to use this (line 11):
specifically, I’m using the index of the currently selected item
(this.selection.index) as the index used to browse through the
Listbox’ children collection, in order to get the text property of
the selected item – i.e., the selected Font name.
If you run the script and click few times the button first, then manually select some Fonts on the
Listbox, you’ll see a log like the following one:

Button clicked.
Listbox Change fired: Source Sans
Button clicked.
Button clicked.
Listbox Change fired: Avenir
Button clicked.
Listbox Change fired: Museo
Listbox Change fired: Papyrus
Listbox Change fired: Avenir

Interestingly (you can spot this yourself when trying the code), the first Button click produces two
lines in the Console: one from the Button callback itself, and one line from the Listbox callback. So,
when the Button’s 'click' handler switches the Listbox selected item, as a consequence a Listbox
'change' event is fired too! The event chain is as follows:
User Interfaces 231

Mind you: when two Button clicked strings are logged in a row, it means that the same integer
has been randomly extracted two times in the Button’s callback, hence there’s no real change in the
Listbox. Instead, when you manually pick Listbox elements yourself, you get only Listbox Change
fired logs.

So far so good; what if you want a different logic instead, e.g., the 'change' callback to be fired
only when the user directly clicks on the Listbox, while leaving the 'Select random Font' Button
working the same as before? In other words, is there a way to break the chain of events where each
Button 'click' also triggers the Listbox 'change'?
This is when you need to be more creative. Do you remember when I grouped RadioButtons in a
Panel, attaching the listener to the container? We can borrow the same idea in this scenario too; find
below the refactored code.

1 var d = new Window("dialog", "Decoupling Events");


2 d.orientation = "column";
3 d.alignChildren = ["fill","top"]
4
5 // Adding a Group as a container for the ListBox
6 var gr = d.add("Group { alignChildren: ['fill','top']}");
7 var lb = gr.add("Listbox { properties: { items: \
8 ['Source Sans', 'Myriad Pro', 'Avenir', 'Museo', 'Papyrus'] }}");
9 var bt = d.add("Button { text: 'Select random Font' }");
10
11 // The handler is on the Group,
12 // and it now listens for the 'click' event (not 'change')
13 gr.addEventListener('click', function () {
14 $.writeln("ListBox Change fired: " +
15 lb.children[lb.selection.index].text);
16 });
17
18 // Button handler
19 bt.addEventListener('click', function () {
20 $.writeln("Button clicked.");
21 var i = Math.floor( 5 * Math.random());
User Interfaces 232

22 lb.children[i].selected = true;
23 });
24
25 d.center();
26 d.show();

I have created a Group that acts as a container for the Listbox, and set a 'click' Event handler
in the Group (instead of the 'change' in the Listbox). This is the one triggered by the user when
he directly selects an item in the Listbox. When the Button is clicked, its callback still produces a
'change' in the Listbox, but this time nobody listens to it anymore.

Button clicked.
Button clicked.
Button clicked.
ListBox Change fired: Museo
ListBox Change fired: Papyrus
ListBox Change fired: Avenir

Event Propagation

In order to implement a more granular Event handling, you need to know about how Events
propagate. Try this simple script:

1 var d = new Window("dialog", "Event Phases");


2 var btn = d.add('button', undefined, 'Click me')
3
4 function handler(evt) {
5 $.writeln("Target: " + evt.currentTarget);
6 $.writeln("Phase: " + evt.eventPhase );
7 $.writeln("============================");
8}
9
10 d.addEventListener('click', handler);
11 btn.addEventListener('click', handler);
12
13 d.center();
14 d.show();

It is a bare Dialog, with one Button. A single handler is shared for 'click' events attached to both
Button and the Dialog itself: think about it like a Parent container (the Dialog), and its one Child
(the Button).
User Interfaces 233

Which callback is going to be fired, in your opinion, if you run the script and click the Button: the
Window’s, the Button’s, or both? In case, in which order? To properly understand the answer, you
must be aware of the three different moments where Events can be captured, as illustrated below.

You can find a detailed description on page 84 of the JS Tools Guide, which I’m going to comment
below.

• Capture Phase: When an event occurs in an object hierarchy, it is captured by the topmost
ancestor object at which a handler is registered (in our case the Window). If no handler is
registered for the topmost ancestor, ScriptUI looks for a handler for the next ancestor (say, a
Group), on down through the hierarchy to the direct parent of the actual target. When ScriptUI
finds a handler registered to any ancestor of the target (in our case, the Button), it executes that
handler then proceeds to the next phase. For us here, the topmost ancestor is the Window object,
because there are no other intermediate ancestors (Groups or Panels that contain the Button).
• Target Phase: ScriptUI calls any handlers that are registered with the actual target object. In
our case, the Button is the target, so the Button callback is executed.
• Bubble Phase: The event bubbles back out through the hierarchy; ScriptUI again looks for
handlers registered for the event with ancestor objects, starting with the immediate parent,
and working back up the hierarchy to the topmost ancestor. When ScriptUI finds a handler, it
executes it, and the event propagation is complete.

To sum up, the Event triggers at Capture Phase each parent of the target, provided it listens for it:
from the topmost ancestor (the Window), onwards. Then it triggers the actual Target. Eventually,
it bounces back, from the inside to the outside (Bubble Phase). In case you need to stop the Event
propagation, use the stopPropagation() method in your callback.
In the handler() function of our example, I’ve logged the Event’s eventPhase property, which keeps
track of the Phase at which the callback is fired. Yet, if you expect to see the three of them logged,
you’re going to be disappointed.
User Interfaces 234

Target: [object Button]


Phase: target
============================
Target: [object Window]
Phase: bubble
============================

Where’s the Capture Phase gone? It turns out that objects callback can be triggered either at Capture
Phase or Bubble Phase (the default behavior), not both. To switch the Capture Phase on, and
consequently to switch the Bubble Phase off, you must add true as a third, optional parameter in
the addEventListener() method (by default, when the parameter is missing, it is assumed false).

9 // ...
10 d.addEventListener('click', handler, true); //
11 btn.addEventListener('click', handler);
12 //...

Having enabled the Capture phase in the Dialog, the log finally says:

Target: [object Window]


Phase: capture
============================
Target: [object Button]
Phase: target
============================

As I’ve pointed out, there’s no mention of the Bubble Phase there, for it’s either Capture or Bubble.
You’re free to combine nested containers with listeners targeted to specific Event propagation phases
(also checking the Event’s eventPhase property) to fine-tune Events control in your scripts.

Create and Simulate Events

Being able to dispatch ScriptUI Events may be a useful skill – either for GUI testing purposes or as
a part of the Script logic. There are several ways to do so: run the following script, which I’m going
to comment right after the code.
User Interfaces 235

1 var d = new Window("dialog", "Simulating Events");


2 d.orientation = 'row';
3 d.alignChildren = ['fill', 'fill'];
4
5 // First Panel - addEventListener
6 var p1 = d.add("Panel { text: 'addEventListener ', \
7 alignChildren: ['fill', 'top']}");
8 var cb1 = p1.add("Checkbox { text: 'Clickable'}");
9 var _sp = p1.add("Panel { size: [100,3]}")
10 var bt1 = p1.add("Button { text: '(1) onClick', enabled: false }");
11 var bt2 = p1.add("Button { text: '(2) onClick.call'}");
12 var bt3 = p1.add("Button { text: '(3) notify'}");
13 var bt4 = p1.add("Button { text: '(4) dispatchEvent'}");
14
15 // Second Panel - onClick
16 var p2 = d.add("Panel { text: 'onClick ', \
17 alignChildren: ['fill', 'top']}");
18 var cb2 = p2.add("Checkbox { text: 'Clickable'}");
19 var _sp = p2.add("Panel { size: [100,3]}")
20 var bt5 = p2.add("Button { text: '(5) onClick'}");
21 var bt6 = p2.add("Button { text: '(6) onClick.call', enabled: false}}");
22 var bt7 = p2.add("Button { text: '(7) notify'}");
23 var bt8 = p2.add("Button { text: '(8) dispatchEvent', enabled: false}}");
24
25 // CALLBACK
26 function clickHandler() { alert("Fired!") };
27
28 // LISTENERS
29 cb1.addEventListener('click', clickHandler);
30 cb2.onClick = clickHandler;
31
32 // HANDLERS - addEventListener() Panel
33 bt2.onClick = function() { cb2.onClick.call(cb1) }
34 bt3.onClick = function() { cb1.notify('onClick') }
35 bt4.onClick = function() { cb1.dispatchEvent(new UIEvent('click')) }
36
37 // HANDLERS - onClick() Panel
38 bt5.onClick = function() { cb2.onClick() }
39 bt7.onClick = function() { cb2.notify('onClick') }
40
41 d.center();
42 d.show();
User Interfaces 236

I’ve created a two Panels dialog, with one


Checkbox on each side. Note the use of Panels
3px tall as division lines. Both Checkboxes
are listening for a 'click' Event, but the
cb1 is using cb1.addEventListener(), while
the other cb2.onClick() – as you’re going to
see, this makes a difference. The callback is
shared, just an alert. The buttons are used to
simulate a Checkbox 'click' Event in var-
ious ways: some works for both sides, some
don’t (the ones that I’ve disabled). Let’s look
at them, row by row.
The first row of Buttons doesn’t really simulate a 'click' Event, but the outcome of the Event itself:
the callback. In a way, the net result is the programmatic execution of whatever callback is attached
to the Checkboxes Event. In fact the Button 5 simply calls the cb2.onClick() function:

bt5.onClick = function() { cb2.onClick() }

It works because cb2 has an onClick() function; cb1 hasn’t, it uses addEventListener(), hence I’ve
disabled Button 1. The second row too is focused on the callback only: Button 2 borrows cb2 own
onClick() function and uses it for cb1. The trick is performed by the JavaScript call() function,
which accepts a parameter that is considered as the this value: we’re passing cb1 to it.

bt2.onClick = function() { cb2.onClick.call(cb1) }

The above line translates as: let’s fire the cb2.onClick() function – but onClick() will run as if it
were owned by cb1. The third row (Buttons 3, 7) use a peculiar function: notify(), which accepts a
simplified handler as the parameter (e.g. onClick, onChange, etc. The ones prefixed by on).

bt3.onClick = function() { cb1.notify('onClick') }


bt7.onClick = function() { cb2.notify('onClick') }

Quite surprisingly, it works for both Checkboxes, even for the one (cb1) that hasn’t a proper
onClick() handler.Also note that, this time, the actual Checkbox statuses are toggled: if one
was unchecked, it becomes checked, and vice-versa. This didn’t happen before. The fourth and last
row of Buttons (4) creates a UIEvent object (see page 149 of the JS Tools Guide) and dispatches it:

bt4.onClick = function() { cb1.dispatchEvent(new UIEvent('click')) }


User Interfaces 237

It works only for the Checkbox that listens to the 'click' Event via addEventListener(). Dispatch-
ing an Event does not toggle the status of the Checkbox (if it’s unchecked, it stays the same).
To sum up, you have several ways to simulate the effect of an Event (in this example, the 'click'):
calling directly the onClick() function; borrowing one and changing the owner via the call()
function; using the notify() method with simplified handler names; creating and dispatching a
UIEvent. Only notify() can simulate both the cause and the effect of the Event (it changes the
status of the Component, and triggers the callback).

7.10 Styling
The ScriptUI elements’ look and feel cannot be tweaked but to a small extent (see the JS Tools Guide,
page 78, 155); be also aware that the level of customization also varies from element to element.
Graphics attributes can be accessed through the graphic property of each component, that I suggest
you to store in a variable because the styling syntax is quite repetitive, as you’ll see in a moment. The
easiest property to target is the color: Photoshop wants you to use a so-called ScriptUIBrush when
filling backgroundColor (areas, like a Panel background) and a ScriptUIPen for foregroundColor
(not intuitively: contours, like in a Button, or paths stroke) or for Fonts color. Let’s have a look at an
example or you’ll be lost.

1 var d = new Window("dialog", "BG and FG colors");


2 d.alignChildren = ['fill','fill'];
3 var g = d.add("Group { text: 'One strange panel' ,\
4 margins: 20, orientation: 'column',\
5 alignChildren: ['center','top']}");
6 var b = g.add("Button { text: 'One strange button'}")
7 var t = g.add("StaticText { text: 'One strange text'}")
8
9 var gfx = d.graphics; // used as a shortcut for Pen and Brushes
10 var g_gfx = g.graphics; // The Group graphics
11 var b_gfx = b.graphics; // The Button graphics
12
13 // The Group background color
14 g_gfx.backgroundColor = gfx.newBrush(gfx.BrushType.SOLID_COLOR,
15 [0.3, 0.4, 0.5, 1]);
16 // The Group foreground color, inherited by the Group children
17 g_gfx.foregroundColor = gfx.newPen(gfx.PenType.SOLID_COLOR,
18 [0.9, 0.4, 0.5, 1], 1);
19 // The Button foreground color, which affects the Font color
20 b_gfx.foregroundColor = gfx.newPen(gfx.PenType.SOLID_COLOR,
21 [0.6, 0.8, 1, 1], 1);
22 d.show()
User Interfaces 238

The first part of the script (lines 1-7) creates the usual Dialog, with a
Group, a StaticText and a Button. On line 9 I’m storing the graphics
property of the Window object in the gfx variable: I’m not modifying
any Window feature, but gfx is going to be handy anyway as a utility.
g_gfx and b_gfx reference the graphics property of the Group and
Button, the elements I will directly tweak. Still with me? Great.
To change the background color of the Group, I need to target its backgroundColor (which in turn
is a property of the graphics, now the g_gfx variable). In lines 13-14 I am using the gfx utility to
create a newBrush().

12 // ...
13 // The Group background color
14 g_gfx.backgroundColor = gfx.newBrush(gfx.BrushType.SOLID_COLOR,
15 [0.3, 0.4, 0.5, 1]);

newBrush() accepts as a parameter the Brush Type (a BrushType.SOLID_COLOR constant, expressed


thanks to the gfx variable¹⁸) and the color as an array of [R, G, B, A] values (A stands for Alpha
or Opacity) in the range {0...1}. Definitely wordy. The result is that the Group background has
turned bluish.
A different statement follows, targeting the Group’s foregroundColor:

15 // ...
16 // The Group foreground color, inherited by the Group children
17 g_gfx.foregroundColor = gfx.newPen(gfx.PenType.SOLID_COLOR,
18 [0.9, 0.4, 0.5, 1], 1);

Instead of a Brush, we need a newPen(), of type PenType.SOLID_COLOR, which color is similarly


specified as an array of [R, G, B, A] values, plus the stroke width (in the example, 1), that in this
context is meaningless – it will be used when stroking paths.
A Group doesn’t happen to have any foregroundColor itself though, hence this property gets inherit
by its children: the Button and the StaticText. With StaticText, the foreground color means the font
color, and as a result One strange text is reddish. The Button’s text color is different because I’ve
overridden its color with a dedicated statement, which turned it light blue:

¹⁸Some developers repeat there the original element graphics, like g_gfx.newBrush( g_gfx.BrushType.SOLID_COLOR etc.
User Interfaces 239

18 // ...
19 // The Button foreground color, which affects the Font color
20 b_gfx.foregroundColor = gfx.newPen(gfx.PenType.SOLID_COLOR,
21 [0.6, 0.8, 1, 1], 1);

ScriptUI styling is mostly a trial and error process; for instance, you may expect that a Panel
foregroundColor will target its outline color (it’s not the case), or that setting a Button backgroundColor
will change it (not the case either). Let’s look at Fonts now: I’ve built a dialog that creates a bunch
of StaticText in a loop, and assigns the their graphics property to elements of an array:

1 var d = new Window("dialog", "Fonts");


2 d.alignChildren = ['left','top']; d.spacing = 2;
3 var txt = [];
4 var str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
5 for (var i = 0; i < 14; i++) {
6 txt[i] = d.add('statictext');
7 txt[i].text = "" + i + ". " + str;
8 // assigning the graphics for extra convenience;
9 txt[i] = txt[i].graphics;
10 }
11 // ... Styling goes here...
12 d.show();

I’ve then styled the Fonts of each line using a variety of options, the result is as follows:

See the styling code below.


User Interfaces 240

1 txt[1].font = "dialog:10";
2 // same result as above. The available constants are
3 // REGULAR, BOLD, ITALIC, BOLDITALIC
4 txt[2].font = ScriptUI.newFont('dialog',
5 ScriptUI.FontStyle.REGULAR, 6);
6 // you can use ScriptUI.FontStyle string shortcuts:
7 // "regular", "italic", "bold", "bolditalic"
8 txt[3].font = ScriptUI.newFont('dialog', "regular", 12);
9 // But in the compressed syntax, use must use Capitals:
10 txt[4].font = "dialog-Bold";
11 txt[5].font = "dialog-BoldItalic";
12 txt[6].font = "dialog-Italic:16";
13 // You can also pick an entirely different Font Family
14 txt[7].font = "Menlo:12";
15 // Equivalent syntax for Menlo 12
16 txt[8].font = ScriptUI.newFont('Menlo',
17 ScriptUI.FontStyle.REGULAR, 12);
18 txt[9].font = "Menlo-Bold:12";
19 // Surprisingly, white spaces don't break it!
20 txt[10].font = "American Typewriter-Regular:12";
21 // Equivalent syntax
22 txt[11].font = ScriptUI.newFont('American Typewriter',
23 ScriptUI.FontStyle.REGULAR, 12);
24 // PostScript font names does NOT work...
25 // txt[11].font = ScriptUI.newFont('AvenirNextLTPro-MediumCn',
26 // ScriptUI.FontStyle.REGULAR, 12);
27 // Using an object to pass properties:
28 txt[12].font = ScriptUI.newFont({
29 family: 'Avenir',
30 size: 16,
31 // name: 'AvenirNextCondensed', // DOESN'T WORK
32 // substitute: 'Arial' // DOESN'T WORK
33 });
34 txt[13].foregroundColor = gfx.newPen(gfx.PenType.SOLID_COLOR,
35 [1,0.5,0] ,1);

For each StaticText element, you need to set the graphics.font property (I’ve already embedded
graphics in the array elements). One quick way is to use the String shortcut "fontfamily-style:size",
for instance "dialog-Italic:12" that sets the default font for Windows of type Dialog at size 12
(you can omit the style, like "dialog:12"). This is what I’ve used for StaticText 1, 3, 4, 5, and 6.
The number 2, instead, uses the ScriptUI.newFont() method, passing the same Font, Style (using
the FontStyle constant) and Size parameters. Number 3 uses a String shortcut for the Style – note
that it’s lowercase here.
User Interfaces 241

Number 7, 9, 10 and 11 demonstrate that it’s also possible to use other Font Families (they can have
names with white spaces, while PostScript names such as AvenirNextLTPro don’t work). Also be
aware that fancy styles (e.g., light, condensed, etc.) don’t work either.
Number 12 uses an alternative syntax, where you can pass an object parameter to the newFont()
method. Finally, number 13 changes also the foregroundColor (in this context: the Font color) via
newPen().

Custom elements and onDraw()

Now that you’ve learned about many of the ScriptUI facets, it’s time to deal with one last component:
the Custom Element (described in the JS Tools Guide at page 163). The Custom Element has no
default appearance, it’s up to you to create one in its onDraw() function.

1 var res = "dialog { \


2 text: 'Custom component', \
3 alignChildren: ['fill', 'fill'], \
4 margins:15, \
5 canvas: Custom { \
6 orientation: 'column', \
7 preferredSize: [100, 100] \
8 }\
9 }"
10 var d = new Window(res);
11 d.canvas.onDraw = function() {
12 // See the actual function body in the next code snippet
13 };
14 d.show()

Since Photoshop CC 2015.5, there’s a bug that makes it possible to use Custom components
only when the Window is built via resource string.

1 var d = new Window('dialog');


2 d.add("Custom { }"); // ERROR!
3 d.add("custom "); // ERROR too!

Custom can be optionally assigned a type, depending on what you meant them to work like:

• customBoundedValue: to simulate controls whose value can vary within a range (Progressbar,
Slider, Scrollbar).
User Interfaces 242

• customButton: button-like controls like Button, IconButton, Checkbox, Radiobutton.


• customView: can hold an Image.

To be frank, I’ve mostly used them as a blank canvas for drawing, like in the following example that
fills the previous code.

1 d.canvas.onDraw = function() {
2 gfx = this.graphics;
3 gfx.ellipsePath(10, 10, 80, 80);
4 gfx.fillPath(gfx.newBrush(gfx.BrushType.SOLID_COLOR, [.65,0.15,.17]))
5 gfx.strokePath(gfx.newPen(gfx.PenType.SOLID_COLOR, [0.25,0.25,0.25], 15))
6 gfx.drawString(
7 "V", // the String to draw
8 gfx.newPen(gfx.PenType.SOLID_COLOR, [0.6,0.6,0.6], 1), // Pen
9 15, -8, // x, y
10 ScriptUI.newFont('Minion Pro', ScriptUI.FontStyle.REGULAR, 100)
11 )
12 };

Let’s inspect this onDraw function. I’ve first declared gfx, holding
the element’s graphics property, as a utility variable. I’ve created an
ellipsePath() (an ellipse), setting the x,y coordinate of the top-left point,
and its width and height (line 3). Then, I’ve used the fillPath() method
to fill the ellipse with a newBrush(), passing the required parameters to
create a color, like you’ve seen before. Similarly, strokePath() is used in
conjunction with newPen() to stroke it.
There’s a new, interesting drawString() method. The accepted parameters
are the String to draw (I’ve used a single, V letter, but you can type a
longer, regular String too); the newPen() that defines the Font color; the
x,y coordinate of the top-left starting point; and finally the Font to be used
(very much like we’ve done in the Font example dialog a couple of pages
earlier). Also, note that I’ve been able to use a textual Emoji (a Chess horse
symbol) too – you can experiment various alternatives, but pick fonts that
you supposed are commonly installed if you plan to distribute your script.
Even if it’s not the most comfortable way ever to draw vectors, ScriptUI
allows you a certain degree of creative freedom, as you’ll see in the next section, where I’ll put
together everything I’ve covered so far in a longer, demo script. Before going there, I’d like to point
out that the same onDraw() used to render a Custom Element can be used for regular elements too:
in the following script, I’ve changed the aspect of a Panel.
User Interfaces 243

1 var d = new Window('dialog');


2 d.preferredSize = [200,100];
3 d.alignChildren = ['fill', 'fill'];
4 d.text = "Panel's onDraw()";
5 var p = d.add("Panel { text: 'One Panel'}")
6
7 p.onDraw = function() {
8 var gfx = this.graphics,
9 w = this.size.width, // The Panel's width
10 h = this.size.height; // The Panel's height
11
12 // In Photoshop CC 2018 it is BUGGED >:-/ In ESTK it works fine
13 // It always return the same values regardless of the font size
14 var stringBounds = gfx.measureString(
15 this.text,
16 ScriptUI.newFont('dialog', ScriptUI.FontStyle.REGULAR, 12)
17 );
18 // Storing the Pen for the stroke
19 var pen = gfx.newPen(gfx.PenType.SOLID_COLOR, [0.2,0.4,0.7], 1);
20 // Drawing the Panel's path
21 gfx.newPath();
22 gfx.moveTo(10,10)
23 gfx.lineTo(0,10);
24 gfx.lineTo(1, h - 1);
25 gfx.lineTo(w - 1, h - 1);
26 gfx.lineTo(w - 1, 10);
27 gfx.lineTo(22 + stringBounds.width, 10);
28 gfx.strokePath(pen);
29 // Creating a new Path
30 gfx.newPath();
31 gfx.rectPath(10, 1, stringBounds.width + 12,
32 stringBounds.height + 1);
33 gfx.strokePath(pen);
34 gfx.fillPath(gfx.newBrush(gfx.BrushType.SOLID_COLOR,
35 [0.2,0.4,0.7,0.2]));
36 }
37 d.show();

The onDraw() function sort of suspends the drawing of the Element. For instance with the Panel (I
can’t tell you if it’s a bug or a feature) an empty onDraw() displays the Panel’s text only, not the
User Interfaces 244

Panel’s contour. I’ve used the measureString() method to calculate the Panel’s text bounds¹⁹, and
use these values to define the contour’s starting and end points.

I’ve first created a newPath(). So to speak, think about having


a pencil in your hand: with moveTo() you don’t draw but move
the pencil’s tip to a precise x,y location. With the lineTo()
function, instead, you’re pressing the pencil tip on the paper at
the current location, and draw a segment to the point specified
as the parameter. You can either keep drawing joint segments (as
I’ve done), or move to a different point and start from there. The
path is then strokePath(). A newPath() is created to hold the rectPath() that surrounds the Panel’s
text.

7.11 Putting it all together

To wrap up this long chapter, I’ve created a more complex dialog pretending to build a commercial
script that does some kind of elaborate B/W conversion. The routine is quite simple, but it’s a pretext
to build a robust ScriptUI architecture around it.
¹⁹The function is bugged, in both Photoshop and InDesign it returns the same values regardless of the font size used. I guess they’re the
one of the default size.
User Interfaces 245

The idea behind this script is to provide a B/W version based on one of the available R, G, B Channels
(plus the Luminosity), and add a Brightness/Contrast adjustment on top. If it looks trivial, the
implementation isn’t: first, the Channels choice and the Adjustments are applied live. Hence you
need to walk back into the original History State before applying any routine – otherwise, you’ll
end up running multiple adjustments one on top of the other. Second, I’ve played a bit with ScriptUI
nested containers, styles, and I’ve used a Custom component to plot the Channels histogram. Third,
I’ve implemented a solid Object-Oriented architecture that you can adapt to any other routine of
yours. The Script involves:

• Preflight: before launching the actual Dialog, it checks if the file is in the appropriate
Colorspace (my example needs RGB as the starting point); you can insert here all sorts of
tests, e.g., whether the active layer is visible, it has a layer mask, etc.
• Pre-Processing: any kind of file processing that you need the original file to undergo before
applying the main routine. Here it creates a new layer on top of the stack; then it merges the
visible layers on it.
• Dialog Creation: where the ScriptUI Window is set up (I’ve used a resource string, but other
approaches are fine).
• Dialog Initialization: applying styles, attaching Event Listeners, defining callbacks.
• Dialog Run: showing the dialog.
• Post-Processing: any cleaning routine that may be needed (Layer renaming, etc.).
• Dialog Auto-Run: A utility that is in charge of calling the proper sequence of functions (the
ones I’ve listed above.)

Since the Script is +400 lines of code, I won’t show it here in its entirety. I’ll present you with the
main architecture, and I’ll discuss important parts that are worth mentioning: you can always check
the full code in the Demo.jsx and Demo.jsxinc files (I use to keep the util functions in a separate file
that I #include in the main one). The structure is as follows:

1 #include DemoScript.jsxinc
2 var Converter = (function() {
3
4 // Constructor
5 function Converter() { /* */ }
6
7 // Preflight
8 Converter.prototype.preflight = function() { /* ... */ }
9
10 // Pre-processing
11 Converter.prototype.preProcess = function() { /* ... */ }
12
13 // Create the Dialog
14 Converter.prototype.createDlg = function() { /* ... */ }
User Interfaces 246

15
16 // Initialize the Dialog
17 Converter.prototype.initDlg = function() { /* ... */ }
18
19 // Show the Dialog
20 Converter.prototype.runDlg = function() { /* ... */ }
21
22 // Post process (after the Dialog closes)
23 Converter.prototype.postProcess = function(result) { /* ... */ }
24
25 // Starts the entire workflow
26 Converter.prototype.autoRun = function() { /* ... */ }
27
28 // BW Conversion Routine
29 Converter.prototype.runRoutine = function() { /* ... */ }
30
31 // Histogram updating function
32 Converter.prototype.updateHistogram = function() { /* ... */ }
33
34 return Converter;
35
36 })();
37
38 function main() {
39 var converter = new Converter();
40 /* ... */
41 };
42
43 main();

As you see, I’ve assigned to the Converter variable a so-called Immediately-Invoked Function
Expression (IIFE): a function expression that gets invoked immediately after it is defined. The IIFE
itself has a Converter named function that acts as a constructor, to which prototype I’ve added
functions for all the steps I’ve defined earlier, plus a couple of utility functions I needed. The IIFE
returns the Converter itself so that I can instantiate a new Converter() (line 381 of the script) in
the main(). The autoRun() starts the entire process, as you’ll see.
There indeed are different design patterns for such scripts: I came up with this one years ago when
I used to write CoffeeScript. Way before modern JavaScript, CoffeeScript had the notion of a class,
and the structure you see here is the output of the CoffeeScript to JavaScript compiler with such
input.
User Interfaces 247

1 class Converter
2 constructor: () ->
3 # ...
4 preFlight: () ->
5 # ...
6 preProcess: () ->
7 # ...
8 createDlg: () ->
9 # ...
10 initDlg: () ->
11 # ...
12 runDlg: () ->
13 # ...
14 postProcess: () ->
15 # ...
16 autoRun: () ->
17 # ...
18 runRoutine: () ->
19 # ...
20 updateHistogram: () ->
21 # ...
22
23 main = () ->
24 converter = new Converter();

Let’s have a look at the relevant parts of the script – again, I’ve stripped some details for brevity’s
sake.

1 #include DemoScript.jsxinc
2 var Converter = (function() {
3
4 // PRIVATE VARIABLES
5 var doc = app.activeDocument;
6
7 // Constructor
8 function Converter() {
9 this.histogram = undefined;
10 this.channel = undefined;
11 this.originalStatus = app.activeDocument.activeHistoryState;
12 this.mergedStatus = undefined;
13 this.appliedStatus = undefined;
14 this.result = undefined;
15 this.brightness = 0;
User Interfaces 248

16 this.contrast = 0;
17 }
18 // ...

Converter() works here as a Constructor function, where I’ve defined all the variables that one can
access as properties of the instantiated class. You must use this and a dot in the Constructor if you
want/need to access them in the instance, like:

var converter = new Converter();


converter.brightness // 0;

For this example, I don’t need to access them this exact way (you may, though); they hold important
values that I point to in many parts of the Script: think about them as the core parameters the Script
is based upon. A significant one is the originalStatus, which refers to the current History Status:
where you want to be when disabling the Preview checkbox (to show the unaltered version of the
image), or if the user Cancels the dialog.
The Private Variables (doc) are the ones for internal use only, and out of the reach when the class is
instantiated²⁰. Before checking the other functions, let me start with the autoRun(), so that you can
make sense of the entire workflow.

363 // Starts the workflow


364 Converter.prototype.autoRun = function() {
365 if (!this.preflight()) { return false } else {
366 this.preProcess();
367 this.createDlg();
368 this.initDlg();
369 var result = this.runDlg();
370 // result = 1 if the user clicks OK
371 // result = 2 if the user clicks Cancel
372 this.postProcess(result)
373 }
374 };

The idea is that if preflight() fails (i.e., the current document doesn’t fulfill the script’s require-
ments) there’s no point in going any further. If, instead, everything’s OK, preProcess() prepares the
file, the Dialog is built, initialized, and run: the result being stored in a variable. If the user dismisses
the Dialog (she clicks the Cancel button), the returned value is 2, conversely (OK button click) it’ll be
1. This bit of information is passed along to the postProcess() function, that performs the required
steps to either restore the document to its pristine state or give it a final touch.
Now that you know what to expect let’s focus on (almost) each function. The preflight() does some
initial conditions check, I make sure that the file is RGB. The returned value is a boolean because
this way it’s easier to check if the conditions are met.
²⁰In other words, converter.doc is undefined.
User Interfaces 249

19 // Preflight
20 Converter.prototype.preflight = function() {
21 // A series of conditionals to evaluate before starting the main routine
22 // The document must be RGB
23 if (doc.mode !== DocumentMode.RGB) {
24 alert("Warning!\nOnly RGB documents are supported so far.");
25 return false;
26 }
27 // Feel free to add more constraints!
28 // If everything's OK, return true
29 return true;
30 }

Add an extra if statements if your preflight checklist is longer. The next phase is preProcess(),
where I apply all the transformations needed to prepare the file for the B/W conversion routine.

36 // Pre-processing
37 Converter.prototype.preProcess = function() {
38 mergeOnTop();
39 this.mergedStatus = app.activeDocument.activeHistoryState;
40 };

It’s quite simple: mergeOnTop() is a function defined in Demo.jsxinc that creates a new layer on top
of the Layers stack²¹, merges the visible layers, and then renames it. I also save this.mergeStatus as
the point in the History where you need to get back to, each time a parameter is changed, to avoid
multiple applications of the same B/W routine one on top of the previous.
In the createDlg() function I’ve defined the long resource string used to build the Dialog, which is
eventually assigned to this.dlg.

42 // Create the Dialog


43 Converter.prototype.createDlg = function() {
44 var res = "dialog { \
45 // ~50 lines of Resource String...
46 }";
47 this.dlg = new Window(res);
48 };

As you may have noticed, I’ve forgotten to declare this.dlg in the Constructor: you can add it here
as well, and it’ll be created on the spot. initDlg() is one of the most extended functions because it
is in charge of wiring all the elements that compose the Dialog.
²¹This is useful because the current layer might be within a Layers Set.
User Interfaces 250

96 // Initialize the Dialog


97 Converter.prototype.initDlg = function() {
98 var self = this;
99 var dlg = this.dlg; // saving few keystrokes
100
101 // Shortcuts
102 var channelsGroup = dlg.channelsGroup;
103 // Brightness
104 var brightnessControls = dlg.controlsPanel.brightnessGroup.controlsGroup;
105 brightnessControls.target = "brightness"; // you can add your props too!
106 var brightnessSlider = dlg.controlsPanel.brightnessGroup.controlsGroup.slider;
107 var brightnessInput = dlg.controlsPanel.brightnessGroup.controlsGroup.input;
108 // ...

The self variable, pointing to this, is declared because in the Event Handlers contexts this points
to the Element (the slider, the checkbox, etc.), instead of the Converter itself. I’ve first set a few
shortcut variables to save fingers stamina and type less. Note that brightnessControls points to
a Group: I’ve added a custom target property (that doesn’t exist in ScriptUI specs – yet nothing
prevents you from defining it anyway!) because I share the same callbacks for both Brightness and
Contrast elements. This way I can tell them apart (you’ll see how in a short while).
I’ve then devoted some lines to style the components: I’ve assigned a Logo.png file (which exists
also as @2X and @Dark) to an Image.image, and styled/colored Fonts. I’ve also taken the chance to
define maxvalue, minvalue and value for sliders, which I forgot to do in the Resource String: you
can always target them later:

137 brightnessSlider.maxvalue = 150;


138 brightnessSlider.minvalue = -150;
139 brightnessSlider.value = 0;

Also in the initDlg are Event Listeners:

238 // Channels radiobutton (via Group)


239 channelsGroup.addEventListener('click', channelClickHandler);
240 // Brightness Slider
241 brightnessSlider.addEventListener('changing', sliderChanging)
242 brightnessSlider.addEventListener('change', sliderChange)
243 // Contrast Slider
244 contrastSlider.addEventListener('changing', sliderChanging)
245 contrastSlider.addEventListener('change', sliderChange)
246 // Brightness EditText
247 brightnessInput.addEventListener('keydown', keyboardHandler);
248 brightnessInput.addEventListener('change', textChangeHandler);
User Interfaces 251

249 brightnessInput.addEventListener('blur', textBlurHandler);


250 // Contrast EditText
251 contrastInput.addEventListener('keydown', keyboardHandler);
252 contrastInput.addEventListener('change', textChangeHandler);
253 contrastInput.addEventListener('blur', textBlurHandler);
254 // Preview Checkbox
255 previewCb.addEventListener('click', togglePreview)

I’ve defined a single 'click' handler on the Group that contains the Channels RadioButtons:

149 function channelClickHandler (evt) {


150 for (var i = 0; i < self.dlg.channelsGroup.children.length; i++) {
151 if (self.dlg.channelsGroup.children[i].value) {
152 self.channel = self.dlg.channelsGroup.children[i].text;
153 }
154 }
155 self.runRoutine();
156 }

It loops through the Group’s children (the RadioButtons) and assigns to the self.channel instance
variable the text ("Red", "Green", etc.) of the RadioButton that has value equals to true. Note that
I’m using self (that I’ve declared equals to this in line 98) because – as I’ve mentioned earlier –
within a callback function this points to the element the event refers to, not the Class. When the
Channel is chosen, the routine is fired.
Sliders have two listeners, one for 'change' and one for 'changing':

158 function sliderChange(evt) {


159 if (this.parent.target == 'brightness') {
160 self.brightness = Math.round(Number(this.value))
161 } else {
162 self.contrast = Math.round(Number(this.value))
163 }
164 self.runRoutine()
165 }
166
167 function sliderChanging(evt) {
168 this.parent.input.text = Math.round(this.value);
169 }

The idea is that when a slider is being dragged ('changing'), its value is bound with the sibling
EditText in the same Group, i.e. this.parent.input. Then the Slider gets the last value ('change'), I
check the custom property of the parent Group I’ve set earlier (this.parent.target), to know which
User Interfaces 252

value to update: self.brightness or self.contrast. I could have hardwired the value, writing two
different callbacks for the two sliders, but it’s handier this way – and it scales well when the elements
grow in number. The EditText handlers are quite interesting:

171 function keyboardHandler(evt) {


172 function KeyIsNumeric(evt) {
173 return (evt.keyName >= "0") &&
174 (evt.keyName <= "9") &&
175 !KeyHasModifier(evt);
176 };
177 function KeyIsDelete(evt) {
178 return ((evt.keyName === "Backspace") ||
179 (evt.keyName === "Delete")) && !evt.ctrlKey;
180 };
181 function KeyIsLRArrow(evt) {
182 return ((evt.keyName === "Left") ||
183 (evt.keyName === "Right")) &&
184 !(evt.altKey || evt.metaKey);
185 };
186 function KeyIsTabEnter(evt) {
187 return evt.keyName === "Tab" ||
188 evt.keyName === "Enter";
189 };
190 function KeyIsPlusMinus(evt) {
191 return evt.keyIdentifier === "U+002B" ||
192 evt.keyIdentifier === "U+002D";
193 };
194
195 try {
196 var keyIsOK = (KeyIsNumeric(evt)) ||
197 (KeyIsDelete(evt)) ||
198 (KeyIsLRArrow(evt)) ||
199 (KeyIsTabEnter(evt));
200 if (!keyIsOK) {
201 evt.preventDefault();
202 app.beep();
203 }
204 } catch (error) {
205 e = error;
206 }
207 };

This handler uses a series of utility functions (based on the 'keydown' Event) to limit the characters
User Interfaces 253

the user is allowed to input, e.g. Numbers, Delete and Backspace keys, etc. If "keyIsOK" (line 200),
the preventDefault() methods stops it, and then Photoshop beep(). Two additional listeners have
been added:

209 function textBlurHandler(evt) {


210 if (this.text === null || this.text === '') {
211 this.text = 0;
212 }
213 this.parent.slider.value = Number(this.text);
214 // Used to set contrast and brightness
215 // and to run the routine
216 this.parent.slider.notify('onChange');
217 }
218
219 function textChangeHandler(evt) {
220 if (Number(this.text) > this.parent.slider.maxvalue) {
221 this.text = this.parent.slider.maxvalue
222 }
223 if (Number(this.text) < this.parent.slider.minvalue) {
224 this.text = this.parent.slider.minvalue
225 }
226 }

textBlurHandler() is in charge of checking that the EditText field is never left empty (the 'blur'
Event fires when the element loses focus): when it’s done, it notifies its sibling Slider a 'onChange'
Event, so that the Slider is activating the Routine. textChangeHandler() (associated with the EditText
'change' Event), limits the values in the proper range, borrowing them from the sibling Slider.
Finally, the Preview Checkbox handler:

228 function togglePreview(evt) {


229 if (this.value) {
230 app.activeDocument.activeHistoryState = self.appliedStatus;
231 } else {
232 app.activeDocument.activeHistoryState = self.originalStatus;
233 }
234 app.refresh()
235 }

It toggles between the appliedStatus History status (the one with the B/W effect) and the
originalStatus (the initial status). Please note that app.refresh() is required, to refresh the PS
interface, and show the difference in the proxy view: it’s a well-known fact that it is a time-
consuming call, but we’ve to live with that.
The next important function is runDlg(), which purpose is to show() and return the Dialog.
User Interfaces 254

262 // Show the Dialog


263 Converter.prototype.runDlg = function() {
264 return this.dlg.show();
265 };

It is quite important that the Dialog is in fact returned: as you’ve seen in the autoRun(), the result
of it is going to be passed along, if you don’t return the this.dlg.show() it’ll be impossible to store
the result. The runRoutine() function does the actual B/W conversion’s heavy lifting:

267 Converter.prototype.runRoutine = function() {


268 app.activeDocument.activeHistoryState = this.mergedStatus;
269 // Here, to always get the original histogram;
270 this.updateHistogram();
271 convertToBW(this.channel);
272 // Adjust Brightness and Contrast only if they're not both zero
273 if (this.brightness || this.contrast) {
274 app.activeDocument.activeLayer.adjustBrightnessContrast(this.brightness, this.co\
275 ntrast);
276 }
277 this.appliedStatus = app.activeDocument.activeHistoryState;
278 app.refresh();
279 };

It first brings back the document history to the clean mergedStatus (line 268), then calls a separate
routine to update the Histogram (which may be needed, because runRoutine() can be called after a
Channel switch, hence a different Histogram must be loaded). It then performs a conversion to B/W
(line 271 – the function is defined in the Demo.jsxinc file) and only then, if both brightness and
contrast adjustments are not equal to zero²², it also runs the adjustBrightnessContrast() DOM
method on the active Layer. At this point, I overwrite the appliedStatus (line 276), and refresh the
view.
updateHistogram() deserves to be a function on its own, even if I didn’t mention it when presenting
the Converter class.

²²There’s no point in running a function if its parameters are zeroed.


User Interfaces 255

280 Converter.prototype.updateHistogram = function() {


281
282 var self = this;
283
284 function getHistogram() {
285 // Get the actual Histogram Array
286 var rawHistogram = getRawHistogram(self.channel);
287 // Normalize the values in the range {0...1}
288 self.histogram = normalizeArray(rawHistogram);
289 // $.writeln(self.histogram)
290 }
291
292 getHistogram();
293
294 var dlg = self.dlg;
295 var canvas = dlg.canvas;
296 var gfx = canvas.graphics
297
298 canvas.notify('onDraw')
299
300 canvas.onDraw = function() {
301 // ... lot of code here
302 }
303 };

I first get the Histogram: getHistogram() makes use of a couple of simple functions that you can
find in the Demo.jsxinc – they get and normalize the Histogram so that its values are in the range
{0...1}. At this point, I explicitly call the onDraw() function of the Custom element (line 298): the
only way to do so, is to notify('onDraw'). In there, the Histogram graphic is drawn:

300 canvas.onDraw = function() {


301 // Outer rectangle (Fill Only)
302 gfx.rectPath(1, 1, 258, 101);
303 gfx.fillPath(gfx.newBrush(gfx.BrushType.SOLID_COLOR, [0.4,0.4,0.4]));
304 // gfx.strokePath(gfx.newPen(gfx.PenType.SOLID_COLOR, [0.2,0.2,0.2], 1));
305
306 // Histogram
307 var x = 2,
308 y = 102;
309 gfx.newPath();
310 // Initial location
311 gfx.moveTo(x,y);
312 for (var i = 0; i < self.histogram.length; i++) {
User Interfaces 256

313 x++;
314 y = 102 - ( self.histogram[i]*100 );
315 gfx.lineTo(x,y);
316 }
317 gfx.lineTo(x,102);
318 gfx.closePath();
319
320 var pen = {
321 "Red" : [1,0,0],
322 "Green" : [0,1,0],
323 "Blue" : [0,0,1],
324 "Luminosity" : [1,1,1]
325 }
326
327 var brush = {
328 "Red" : [1,0,0, 0.2],
329 "Green" : [0,1,0, 0.2],
330 "Blue" : [0,0,1, 0.2],
331 "Luminosity" : [1,1,1, 0.3]
332 }
333
334 gfx.fillPath(gfx.newBrush(gfx.BrushType.SOLID_COLOR, brush[self.channel]));
335 gfx.strokePath(gfx.newPen(gfx.PenType.SOLID_COLOR, pen[self.channel], 1));
336
337 // Drawing the 0, 128 and 256 below the histogram
338 var numberPen = gfx.newPen(gfx.PenType.SOLID_COLOR, [0.6,0.6,0.6], 1);
339 var numberFont = ScriptUI.newFont('dialog', ScriptUI.FontStyle.REGULAR, 10)
340 gfx.drawString("0", numberPen, 0, 104, numberFont)
341 gfx.drawString("128", numberPen, 120, 104, numberFont)
342 gfx.drawString("255", numberPen, 240, 104, numberFont)
343
344 // Outer rectangle (Stroke Only)
345 gfx.newPath();
346 gfx.rectPath(1, 1, 258, 101);
347 // gfx.fillPath(gfx.newBrush(gfx.BrushType.SOLID_COLOR, [0.4,0.4,0.4]));
348 gfx.strokePath(gfx.newPen(gfx.PenType.SOLID_COLOR, [0.2,0.2,0.2], 1));
349 }

I’ve drawn the rectangle fill first (lines 302-303) with a rectPath() and fillPath() – it’ll be stroked
in a later pass. For the Histogram itself (lines 306-335) I’ve had to calculate a x,y starting point of
2,102 (bottom-left), with a slight offset of two pixels to account for the outer frame stroke thickness.
The pointer is moveTo() such location, and from there I’ve set a loop through all self.histogram
elements (line 312) and drawn a lineTo() to connect them. In doing so, you have to keep in mind
User Interfaces 257

that the y axis direction goes down, that’s the reason why of 102 minus something (line 314). That
something is the height of each histogram’s bar²³: calculated multiplying the maximum allowed
height (I’ve decided that the graphic is going to be 100px tall) times the Histogram’s current value
– that as you remember is in the range {0...1}. As a result, you get a point in the range {0...100}.
The fillPath() and strokePath() depends on the channel used. I’ve then used the drawString()
function to append the 0, 128, and 255 numbers below the histogram (line 338-342), and finally I’ve
drawn and stroked the rectangular outer frame. That’s the beauty of the rather unknown Custom
element.
The last method of the Class is postProcess():

352 // Post process (after the Dialog closes)


353 Converter.prototype.postProcess = function(result) {
354 if (result == 1) { // Everything's OK
355 app.activeDocument.activeLayer.name = "Simple BW [" +
356 this.channel + "], brightness: " + this.brightness + ", contrast:" + this.contra\
357 st;
358 } else {
359 app.activeDocument.activeHistoryState = this.originalStatus;
360 }
361 };

This method contains all the routine spring cleaning that you must perform before giving back the
steering wheel to the user. In this demo, as an example, I rename the B/W layer if she’s clicked the
OK button (result == 1), or get back to the originalStatus to bring the document to its pristine
History status. In conclusion, let me show you the main() function:

380 function main() {


381 var converter = new Converter();
382 converter.autoRun();
383 // Cleaning the memory
384 converter = null;
385 delete converter;
386 $.gc();
387 };
388
389 main();

Here I’ve created an instance of the Converter class²⁴, and started autoRun(). As a best practice,
when everything’s done with Dialogs, it’s better to explicitly delete any ScriptUI object and perform
garbage collection to free the memory (lines 384-386).
²³I apologize for the confusion: the image’s Histogram is, quite obviously, a graph of type histogram.
²⁴It’s not an actual Class, but it is the best approximation of a Class that we can simulate in ExtendScript.
User Interfaces 258

If this long description has scared you because it looks too complicated to grasp, do not worry: I had
the same feeling myself the first time I’ve opened one of the Scripts bundled with Photoshop – it
is a somewhat normal reaction I would say! Even if this Script is, in fact, a demo, it features many
techniques that I use in my commercial scripts, so you can rest assured that the learning curve is
steep for a good reason. Take your time to review the parts that are more difficult to digest, and refer
to the Demo.jsx and Demo.jsxinc files for the entire source code.

7.12 CEP Panels


I will very briefly cover HTML Panels here – if you’re interested in a Panels coding deep dive,
please get in touch with me for a discount on my Adobe Photoshop HTML Panels development
Course (+300 PDF pages, 3 hours of HD videos, 28 Sample Panels). As I’ve already mentioned, the
CEP technology allows a great deal of extra flexibility and features and might be a better GUI option
if you need your script to communicate with the outer world – say, connect with a remote server
and exchange data. Feel free to review once more the differences between ScriptUI dialogs and CEP
Panels that I’ve described here. I’ve cooked up an elementary example to let you get the feeling of
what coding a Panel is like.

You can find the full source-code in the Chapter 7 subfolder: to run it, copy the entire com.example.cep
folder either in:

• Mac: ∼/Library/Application Support/Adobe/CEP/extensions/²⁵


• Win: C:\Users\<yourUserName>\AppData\Roaming\Adobe\CEP\extensions\

Also, set the Debug Flag on (it saves you from digitally signign a panel in development, preventing
Photoshop from checking the signature). Quoting from the official documentation:

• Mac: In the Terminal, type: defaults write com.adobe.CSXS.9 PlayerDebugMode 1 (The plist
is also located at /Users/<username>/Library/Preferences/com.adobe.CSXS.9.plist)
• Win: regedit > HKEY_CURRENT_USER/Software/Adobe/CSXS.9, then add a new entry PlayerDebugMode
of type "string" with the value of "1".
²⁵The tilde ∼ at the beginning of the path means your User’s Home folder.
User Interfaces 259

Restart Photoshop, and find “CEP Panel Example” in the Windows > Extensions menu. It is a very
simple Lorem Ipsum text generator: with the Slider you define how many words you want to get,
clicking “Generate” you’re connecting with https://lipsum.com/ REST API to get the long String.
Eventually, the Panel hands it to Photoshop, which in turn alerts it and copies it in the Clipboard.
Let’s now overview the Panel structure.

The content shown here is mostly optional, few items are required. The
index.html, which represents the entry point, and has links to all the
JavaScript files that control the Panel’s operations. A CSXS/manifest.xml
file, which contains configuration data – the Panel’s name and id, Menu
name, geometry, CEP version, supported host applications, linked files, etc.
Please note that the CSXS folder name is fixed and should not be changed.
A CSInterface.js file, which is the Common Extensibility Platform API library that you use,
among the rest, to communicate with the Host Application (here, in an optional lib folder). A
photoshop.jsx file (you can change the file name, it’s important that it matches the one defined
in manifest.xml), that contains ExtendScript code.
In the index.html you lay down the GUI elements (buttons, sliders, text) in plain HTML/CSS – I
assume you’re already familiar with these technologies. The code for my example is as follows:

1 <!doctype html>
2 <html>
3 <head>
4 <meta charset="utf-8">
5 <link id="theme" rel="stylesheet" href="" />
6 <link id="hostStyle" rel="stylesheet" href="css/styles.css" />
7 <title></title>
8 </head>
9 <body>
10 <div id="content">
11 <div class="row">
12 <h3 style="">Lorem Ipsum Generator</h3>
13 </div>
14 <div class="row" style="height: 50px;">
15 <h4 class="relative" style="top: 3px; left: 19px; opacity: 0.6;">
16 WORDS
17 </h4>
18 <input type="range" id="words"
19 min="0" max="150" value="50" step="1"
20 class="topcoat-range absolute"
21 style="top: 25px;left: 0px;width: 209px;">
22 <input type="number" id="wordsno"
23 min="0" max="150" step="1" value="50"
User Interfaces 260

24 class="topcoat-text-input absolute"
25 style="top: 18px;left: 217px;width: 43px; padding-left: 7px;"
26 disabled>
27 </div>
28 <div class="row" style="">
29 <button class="topcoat-button absolute"
30 id="reset" style="left: 140px;">Reset</button>
31 <button class="topcoat-button--cta absolute"
32 id="generate" style="left: 144px;">Generate</button>
33 </div>
34 <div class="row">
35 <p class="lipsum" disabled>
36 © Davide Barranca | text API and credits: www.lipsum.com
37 </p>
38 </div>
39 </div>
40 <script src="js/CSInterface.js"></script>
41 <script src="js/themeManager.js"></script>
42 <script src="js/jquery.js"></script>
43 <script src="js/main.js"></script>
44 </body>
45 </html>

I can’t say if it’s less wordy compared to a ScriptUI resource string, but it allows a great deal
of modularity and separation of concerns; it’s nothing but standard HTML/CSS, with absolute
positioning (which I tend to prefer because when the Panel is docked the elements can weirdly
arrange themselves). I’m using Topcoat as the CSS framework to style the Panel and match the PS
dark theme UI, which is loaded and kept in sync with the Photoshop UI in themeManager.js (linked
at line 41). I’ve also linked CSInterface.js and jquery.js. I usually use Vue.js as my JavaScript
framework of choice, but for this demo, it would have been overkill.
The main.js contains the similarly brief code that operates the Panel:

1 (function() {
2 'use strict';
3 themeManager.init();
4
5 $('#words').on('input', function() {
6 $('#wordsno').val(this.value)
7 })
8
9 $('#generate').on('click', generateLorem)
10
User Interfaces 261

11 function generateLorem() {
12 $.get('https://lipsum.com/feed/json',
13 {
14 'what': 'words',
15 'amount': $('#words').val()
16 }).done(function(res) {
17 // the response object is in the form of
18 // {
19 // "feed": {
20 // "lipsum": "Lorem ipsum dolor sit amet, consectetur ...",
21 // "generated": "Generated 1 paragraph, 10 words, 69 bytes...",
22 // "donatelink": "https://www.lipsum.com/donate",
23 // "creditlink": "https://www.lipsum.com/",
24 // "creditname": "James Wilson"
25 // }
26 // }
27 var lipsum = res.feed.lipsum
28 var csInterface = new CSInterface();
29 csInterface.evalScript('alertLipsum("' + lipsum + '")');
30 }).fail(function(res) {
31 console.log("ERROR!", res);
32 })
33 }
34 }());

Everything’s wrapped in an IIFE (Immediately Invoked Function Expression, which you’ve already
met in the ScriptUI example). I’ve set a couple of Event Handlers with JQuery: the first (line 5-8)
listens for the 'input' Event²⁶ of the Slider (the element which id is equal to 'wordsno') and it is
used to bind the Slider value with the text input – so that when you drag the Slider’s handler you
can get immediate feedback on the value.
The second handler (line 10) listens to the Generate Button’s 'click', and calls a generateLorem()
named function that I’ve declared separately (lines 12-34). In there, I make use of JQuery’s $.get()
method²⁷ to send a GET request to the https://lipsum.com/feed/json REST endpoint²⁸, passing
as parameters two key/value pairs: 'what', used to specify whether you want 'words', or maybe
'paras' (paragraphs), 'bytes', etc; and 'amount', which I fill with the slider’s value to get that
number of words.
Since $.get() is asynchronous, the .done() and .fail() methods are called only when the transfer
is completed (or it has failed) – it depends on how much time the Lipsum server takes to answer
your request. The callback function of .done() (lines 17-30) extracts the lipsum property from
²⁶The 'change' event fires only when you release the Slider: to have a true 'changing' event, you must listen for 'input' in the HTML
world.
²⁷Please note that the $ symbol represents two different things in jQuery and ExtendScript.
²⁸I’ve found here a description of the API that https://lipsum.com/ exposes to get Lorem Ipsum text.
User Interfaces 262

the Object the GET request has fetched (you see it contains several other properties), and passes
it to the Photoshop’s ExtendScript engine. It does so first instantiating a new csInterface from
the CSInterface class; it uses its evalScript() method to call an ExtendScript function named
alertLipsum() (declared in the photoshop.jsx file), passing to it the lipsum variable that contains
the Lorem Ipsum text.
As you remember, the panel can’t run ExtendScript commands directly, so it has to hand them as
strings to the Photoshop engine via evalScript(). At this point, the alertLipsum() function is in
charge of copying the text in the clipboard and firing an alert.

1 function s2t (s) { return app.stringIDToTypeID(s) };


2 function alertLipsum(lip) { // Copy the lip in the Clipboard
3 var d = new ActionDescriptor();
4 d.putString( s2t('textData'), lip );
5 executeAction( s2t( "textToClipboard" ), d, DialogModes.NO );
6 // Fire the alert
7 alert("Lorem Ipsum – Copied to the Clipboad\n" + lip);
8 }

As you see, the Copy to Clipboard function


is based on ActionManager. Don’t feel over-
whelmed by this demo Panel – at first, it’s
easy to get lost in the various steps that CEP
requires, but one gets used to them rather
quickly. If you feel inclined, you can try
adding a 'click' handler for the Reset button,
that should bring the Slider and the Text Input
back to 50.
See on the next page a selection from the
Demo Panels included in my other course
Adobe Photoshop HTML Panels Develop-
ment.
User Interfaces 263
8. Working with Metadata
According to Wikipedia, “Metadata is data [information] that provides information about other
data”. In the context of Adobe Photoshop, there’s plenty of metadata to read and write around
images: either directly embedded by the digital camera or added in a later stage in the DAM
(Digital Assets Management) workflow. On the other hand, Scripts themselves may need to save
their metadata: say, to recall default preferences, or as a form of inter-scripts communication when
shared information needs to be passed down the scripting pipeline.

8.1 XMP

Created by Adobe itself, the Extensible Metadata Platform (XMP) is an XML-based format “… for the
creation, processing, and interchange of standardized and custom metadata for digital documents
and datasets”, that became an ISO Standard in 2012. There is a great deal of documentation in the
Adobe XMP Toolkit SDK CC page, so much that it can easily become misleading. Here, we’re mostly
interested in two things: the kind of metadata that also appears in the Photoshop’s File Info Panel:

Similar metadata that is applied on a per Layer basis, a welcome addition to the Scripting API that
dates back to Photoshop CS4, and was mainly driven by a Pixar feature request¹. XMP is documented
¹This is what the former Photoshop Product Manager John Nack wrote in his blog in late 2008.
Working with Metadata 265

in the JS Tools Guide, pages 257-293, with very few examples, mostly based on Adobe Bridge. Bear
with me if I’ll be simplifying to a great extent this otherwise utterly entangled matter.

Schemas and Namespaces

Since XMP is, so to speak, just an XML-based metadata management system, it has nothing to do
with the kind of data it holds: a considerable lot of key/value pairs. Several standard and non-
standard Schemas (groups of properties) are supported, each one being identified by a unique
namespace. An established Schema, for instance, is the Dublin Core, originally created for US
Libraries digital media files: it includes general properties such as Title, Creator, Subject, and
Description – but their number is arbitrary.
As a matter of fact, you can create your own Schema, assign it your custom namespace, stick there
some information and embed it in a .psd, .tiff or .jpg file as part of the XMP metadata. Find in
the table below the most common Schemas, their namespaces and URI².

Some Schemas and their Namespaces

Namespace Value Namespace Constant Description URI


xmp: NS_XMP Basic Schema http://ns.adobe.com/xap/1.0/
dc: NS_DC Dublin Core http://purl.org/dc/elements/1.1/
photoshop: NS_PHOTOSHOP Adobe PS Custom http://ns.adobe.com/photoshop/1.0/
xmpMM: NS_XMP_MM XMP DAM http://ns.adobe.com/xap/1.0/mm/
xmpRights: NS_XMP_RIGHTS Copyright http://ns.adobe.com/xap/1.0/rights/
Iptc4xmpCore: NS_IPTC_CORE IPTC Core http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/
crs: NS_CAMERA_RAW Adobe Camera Raw http://ns.adobe.com/camera-raw-
settings/1.0/
tiff: NS_TIFF Adobe TIFF http://ns.adobe.com/tiff/1.0/
exif: NS_EXIF Adobe EXIF http://ns.adobe.com/exif/1.0/

A couple of caveats: Namespace Constants mean nothing to you now, but they will be useful in a
moment; the URI might resemble an actual URL, but it’s not³, it works just as a unique identifier for
that Schema. All this can appear quite abstract, but if you open the File Info Panel (from the menu
File > File Info) and browse to the Raw Data section, you’ll find that very XML-ish structure:
²Uniform Resource Identifier – a string of characters used to identify a resource.
³The reverse is true, a URL is a URI.
Working with Metadata 266

E.g., a tag such as <dc:creator> defines, in the Dublin Core namespace, the creator metadata.
To sum up: a file can contain an indefinite amount of metadata properties (hierarchically ordered
key/value pairs) stored as XML nodes, and grouped into Schemas, which have a unique namespace
that identifies them. Some Schemas and related namespaces are Standard, but you can add metadata
in a custom namespace of yours too. Let’s now see how it works in practice.
Before dealing with XMP altogether, in each script you need to first load the XMP library as an
ExtendScript ExternalObject:

1 // load the XMP Library


2 if (ExternalObject.AdobeXMPScript == undefined) {
3 ExternalObject.AdobeXMPScript = new ExternalObject('lib:AdobeXMPScript');
4 }
5
6 //... Do your XMP stuff
7
8 // When you're done, unload the Library
9 if( ExternalObject.AdobeXMPScript ) {
10 try{
11 ExternalObject.AdobeXMPScript.unload();
12 ExternalObject.AdobeXMPScript = undefined;
13 }catch (e){
14 alert("Can't unload XMP Script Library");
15 }
Working with Metadata 267

16 }
17 };

XMPFile, XMPMeta, and XMPProperty

You can find in the code bundle a file named cocomeraia.jpg, which I’ll be using throughout the
whole Chapter. It’s a picture of mine, which metadata I have only partially edited, and mixed with
the metadata of the IPTC (International Press Telecommunications Council) demo image, available
for direct download at this link. As a result, all the available XMP slots are filled either with my
metadata or placeholders.

The simplest way to start with XMP is to extract metadata as XML from an existing file on disk:
Working with Metadata 268

1 // Load the XMP Library


2 if (ExternalObject.AdobeXMPScript == undefined) {
3 ExternalObject.AdobeXMPScript = new ExternalObject('lib:AdobeXMPScript');
4 }
5
6 var pic = File(File($.fileName).path +
7 '/resources/cocomeraia.tif');
8
9 // A new XMPFile object
10 var xmpFile = new XMPFile(pic.fsName,
11 XMPConst.FILE_TIFF,
12 XMPConst.OPEN_FOR_READ);
13
14 // A XMPMeta object
15 var xmpMeta = xmpFile.getXMP();
16
17 // Serializing as an XML String
18 var metaString = xmpMeta.serialize();
19 $.writeln(metaString);

After loading the XMP Library, I’ve stored the image file in the pic variable, and then created a
new XMPFile(), passing as a parameter the image’s path, the file kind (one of the available from the
XMPConst object⁴ – when you’re not sure, use XMPConst.UNKNOWN), and another constant related to
the open options (see the entire list in the documentation, page 268). Here, I’m opening a .tif in a
read-only mode.
What you’re returned is a XMPFile instance, from which you extact a XMPMeta object via the
getXMP() method. Finally, this XMPMeta object can be serialized (turned into a long XML string)
via serialize(), for you to inspect:

1 <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 5.6-c140 79.160451, 2017\


2 /05/06-01:08:21 ">
3 <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
4 <rdf:Description rdf:about=""
5 xmlns:xmp="http://ns.adobe.com/xap/1.0/"
6 xmlns:aux="http://ns.adobe.com/exif/1.0/aux/"
7 xmlns:exifEX="http://cipa.jp/exif/1.0/"
8 xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/"
9 xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/"
10 xmlns:stEvt="http://ns.adobe.com/xap/1.0/sType/ResourceEvent#"
11 xmlns:dc="http://purl.org/dc/elements/1.1/"
12 xmlns:tiff="http://ns.adobe.com/tiff/1.0/"
⁴The list of all the constants is found in the JS Tools Guide, pages 264-265.
Working with Metadata 269

13 xmlns:exif="http://ns.adobe.com/exif/1.0/"
14 xmlns:xmpRights="http://ns.adobe.com/xap/1.0/rights/"
15 xmlns:Iptc4xmpCore="http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/"
16 <!-- ... etc. -->

It’s the very same string features in the File Info panel: we’ll use this string to ease our quest for
metadata. Let’s say that we’re interested in knowing which time the Metadata has been modified:

1 <!-- ... stuff before... -->


2 <xmp:MetadataDate>2018-03-29T16:07:29+02:00</xmp:MetadataDate>
3 <!-- ... stuff after... -->

What you want to do to retrieve this property is as follows.

21 // Looking for the "MetadataDate" property


22 if (xmpMeta.doesPropertyExist(XMPConst.NS_XMP, "MetadataDate")) {
23 var metadataDate = xmpMeta.getProperty(XMPConst.NS_XMP, "MetadataDate");
24 // metadataDate is a XMPProperty object
25 $.writeln(metadataDate.value); // 2018-03-29T16:07:29+02:00
26 // Convert it into a XMPDateTime;
27 xmpDate = new XMPDateTime(metadataDate.value);
28 $.writeln("Year: " + xmpDate.year + "; " +
29 "Month: " + xmpDate.month + "; " +
30 "Day: " + xmpDate.day + "; " +
31 "Hour: " + xmpDate.hour + ";")
32 }

The XMPMeta object has a getProperty() method, but it’s safer to test for the property existence in
advance with doesPropertyExist(). Both methods want to know which namespace to look for the
property in (it is http://ns.adobe.com/xap/1.0/, which corresponds to the xmp: prefix, and it’s also
equal to the XMPConst.NS_XMP constant you’ve seen in the Schemas and Namespaces table), and as
a second parameter the actual property ("MetadataDate").
The result is a XMPProperty object, which in turn has a value property that finally corresponds to
the Date ISO String we were looking for (line 24). The XMP Library also provides a XMPDateTime
class⁵: that I’ve instantiated feeding that very ISO String (line 25) to use the much handier properties
year, month, etc. This last step is optional.
⁵See pages 265-267 in the JS Tools Guide.
Working with Metadata 270

It may seem an involved procedure, but it reflects the highly structured nature of such metadata.
I’m afraid that complication escalates pretty quickly even for apparently simple properties, such as
the quite common Dublin Core “Creator” tag, which is found in the serialized string as:

1 <!-- ... stuff before... -->


2 <dc:creator>
3 <rdf:Seq>
4 <rdf:li>Davide Barranca</rdf:li>
5 </rdf:Seq>
6 </dc:creator>
7 <!-- ... stuff after... -->

It turns out that the <rdf> tag is a container of Arrays, that can be either: Unordered (<rdf:Bag>),
Ordered (<rdf:Seq>) and Alternative (<rdf:Alt>)⁶. The <rdf:li> identifies a list element. To get the
"creator", you need to make sure the Array exists first, providing the namespace, the property and
its index: be aware that such Arrays are not zero-based, but always start with 1.

34 if (xmpMeta.doesArrayItemExist(XMPConst.NS_DC, "creator", 1)) {


35 var creatorProp = xmpMeta.getArrayItem(XMPConst.NS_DC,
36 "creator",
37 1);
38 }
39 $.writeln(creatorProp.value); // Davide Barranca

Let’s try to get all the elements in a list, for instance, the "subject", which happens to be an
Unordered array (<rdf:Bag>).

⁶The most common structures are Ordered and Unordered Arrays, the Alternative being used, e.g. for Language Alternatives (translations).
They’re documented in the XMPSpecificationPart1.pdf, found in the Adobe XMP Toolkit SDK CC.
Working with Metadata 271

1 <!-- ... stuff before... -->


2 <dc:subject>
3 <rdf:Bag>
4 <rdf:li>environment</rdf:li>
5 <rdf:li>ecology</rdf:li>
6 <rdf:li>ecosystem</rdf:li>
7 <rdf:li>environmentalism</rdf:li>
8 <rdf:li>scenery</rdf:li>
9 <rdf:li>nature</rdf:li>
10 <!-- ... stuff after... -->

One way to loop through this Bag is to create an Iterator⁷, that provides a convenient next()
method, returning the XMPProperty.

42 // Looking for the Dublin Core "subject" Array


43 var subjectIterator = xmpMeta.iterator(
44 XMPConst.ITERATOR_JUST_LEAFNODES,
45 XMPConst.NS_DC,
46 "subject"
47 );
48
49 while (true) {
50 var subject = subjectIterator.next();
51 if (subject) { $.writeln(subject.value) }
52 else { break; }
53 }
54 // environment
55 // ecology
56 // ecosystem
57 // environmentalism
58 // scenery
59 // ...

The Iterator requires (lines 43-47) a constant that specifies how the iteration is performed (here we’re
interested in ITERATOR_JUST_LEAFNODES only⁸), the Namespace, and the property we’re looking for.
I’ve then set a while loop that logs the property’s value and quits when next() returns null – i.e.,
there are no more items to iterate through.
So far we have used an existing File in the Filesystem as a source of metadata, which is quite handy
because it doesn’t require the document to be opened at all. The following snippet, for instance, tells
you which kind of compression a .tif file is saved with:
⁷See page 272 in the JS Tools Guide.
⁸Refer to the JS Tools Guide, page 281, for the available constants.
Working with Metadata 272

1 // Load the XMP Library


2 if (ExternalObject.AdobeXMPScript == undefined) {
3 ExternalObject.AdobeXMPScript = new ExternalObject('lib:AdobeXMPScript');
4 }
5
6 var pic = File(File($.fileName).path + '/resources/cocomeraia.tif');
7 // A new XMPFile object
8 var xmpFile = new XMPFile(pic.fsName, XMPConst.FILE_TIFF, XMPConst.OPEN_FOR_READ);
9 // A XMPMeta object
10 var xmpMeta = xmpFile.getXMP();
11
12 try {
13 var compMeta = xmpMeta.getProperty(XMPConst.NS_TIFF, "Compression").value;
14 var compression;
15 switch (parseInt(compMeta)) {
16 case 1: compression = "None"; break;
17 case 5: compression = "LZW"; break;
18 case 7: compression = "JPG"; break;
19 case 8: compression = "ZIP"; break;
20 default: compression = "Unknown";
21 }
22 } catch (e) { compression = "Unknown"; }
23
24 $.writeln("TIFF Compression is: " + compression);
25 // TIFF Compression is: ZIP

Very useful indeed. Instead of passing a File reference, nothing prevents you from using the currently
active Document:

63 var xmpMeta = new XMPMeta(app.activeDocument.xmpMetadata.rawData);


64 // You can peek into the xmpMeta now, e.g.
65 if (xmpMeta.doesPropertyExist(XMPConst.NS_XMP, "MetadataDate")) {
66 var metadataDate = xmpMeta.getProperty(XMPConst.NS_XMP, "MetadataDate");
67 $.writeln(metadataDate.value); // 2010-07-16T03:24:01+01:00
68 }

Editing the XMP Metadata

Besides getting data, Files and Documents can be edited as well, i.e., you can add new, or
replace/delete existing namespaced properties. We’ll look at adding your custom namespaces further
on.
Working with Metadata 273

To replace a property, it’s not enough to setProperty(), you also need to apply this change
permanently. The proper way to do so depends on whether you’re acting on a File on disk (via
the XMPFile class), or the activeDocument. Let’s examine the XMPFile way first

1 // Load the XMP Library


2 if (ExternalObject.AdobeXMPScript == undefined) {
3 ExternalObject.AdobeXMPScript = new ExternalObject('lib:AdobeXMPScript');
4 }
5
6 // Using XMPFile
7 var pic = File(File($.fileName).path + '/resources/cocomeraia.tif');
8 // Create a new XMPFile object
9 var xmpFile = new XMPFile(pic.fsName,
10 XMPConst.FILE_TIFF,
11 XMPConst.OPEN_FOR_UPDATE);
12 // Extract the XMPMeta object
13 var xmpMeta = xmpFile.getXMP();
14 // Reading the old Rating
15 if (xmpMeta.doesPropertyExist(XMPConst.NS_XMP, "Rating")) {
16 var oldRating = xmpMeta.getProperty(XMPConst.NS_XMP, "Rating");
17 $.writeln("Old rating: " + oldRating.value);
18 }
19 // Setting a new Rating of 5
20 xmpMeta.setProperty(XMPConst.NS_XMP, "Rating", 5, XMPConst.NUMBER);
21 // Testing if the new xmpMeta fits in the XMP
22 if (xmpFile.canPutXMP(xmpMeta)) {
23 xmpFile.putXMP(xmpMeta);
24 }
25 // Save and close the file
26 xmpFile.closeFile(XMPConst.CLOSE_UPDATE_SAFELY);
27 // Test if it worked:
28 var xmpMeta = new XMPFile(pic.fsName,
29 XMPConst.FILE_TIFF,
30 XMPConst.OPEN_FOR_READ).getXMP();
31 if (xmpMeta.doesPropertyExist(XMPConst.NS_XMP, "Rating")) {
32 var currentRating = xmpMeta.getProperty(XMPConst.NS_XMP, "Rating");
33 $.writeln("Current rating: " + currentRating.value);
34 }

Having loaded the XMP Library, I’ve extracted the XMPMeta object from the XMPFile (lines 6-13).
I’ve got the old "Rating" with getProperty() (lines 15-18), and then used setProperty() specifying
the namespace, the property and its type (line 20). Very importantly, you need to fix the changed
metadata putting xmpMeta in the XMPFile via putXMP() method (line 23). First, it’s better to check
Working with Metadata 274

whether canPutXMP() returns true (it has to do with the size of the XMP we’re about to store).
Eventually, we can closeFile() (line 26). All the rest is reopening the File to test if the result has
stuck.
To apply the same "Rating" change, but with an opened file, the procedure is slightly different:

36 // Using a currently opened Document-----------------------------------------


37 var xmpMeta = new XMPMeta(app.activeDocument.xmpMetadata.rawData);
38 // Reading the old Rating
39 if (xmpMeta.doesPropertyExist(XMPConst.NS_XMP, "Rating")) {
40 var oldRating = xmpMeta.getProperty(XMPConst.NS_XMP, "Rating");
41 $.writeln("Old rating: " + oldRating.value);
42 }
43 // Setting a new Rating of 5
44 xmpMeta.setProperty(XMPConst.NS_XMP, "Rating", 5, XMPConst.NUMBER);
45 // Putting the new XMP in the rawData
46 app.activeDocument.xmpMetadata.rawData = xmpMeta.serialize();
47 // Test if it worked:
48 var xmpMeta = new XMPMeta(app.activeDocument.xmpMetadata.rawData);
49 // Reading the old Rating
50 if (xmpMeta.doesPropertyExist(XMPConst.NS_XMP, "Rating")) {
51 var currentRating = xmpMeta.getProperty(XMPConst.NS_XMP, "Rating");
52 $.writeln("Current rating: " + currentRating.value);
53 }

Here, we’re creating the XMPMeta object from the currently active document. The old "Rating"
is similarly got, and then set, but here the change is made permanent (line 46) by assigning the
serialized xmpMeta back to the rawData property.
Of course, to make the new metadata really permanent, you must save the file.
Working with Metadata 275

Pay attention, because some data may not get permanently written when operating on
the activeDocument. For instance, if you want to set the <xmp:CreateDate> property, the
first method using an XMPFile and then putXMP() does work as expected; instead, setting
back the rawData only apparently works, but further inspection shows that the modification
didn’t stick.

1 var xmpMeta = new XMPMeta(app.activeDocument.xmpMetadata.rawData);


2 // Logging the old "CreateDate"
3 var currentDate = xmpMeta.getProperty(XMPConst.NS_XMP, "CreateDate").value;
4 $.writeln("Current Date: " + currentDate); //
5 // Setting a new "CreateDate"
6 xmpMeta.setProperty(XMPConst.NS_XMP,
7 "CreateDate",
8 new XMPDateTime(new Date(),
9 XMPConst.XMPDATE);
10 // Logging the new "CreateDate"
11 var newDate = xmpMeta.getProperty(XMPConst.NS_XMP, "CreateDate").value;
12 $.writeln("New Date: " + newDate);
13 app.activeDocument.xmpMetadata.rawData = xmpMeta.serialize();
14 // Testing if it worked:
15 var xmpMeta = new XMPMeta(app.activeDocument.xmpMetadata.rawData);
16 var savedDate = xmpMeta.getProperty(XMPConst.NS_XMP, "CreateDate").value;
17 $.writeln("Saved Date: " + savedDate); //
18 $.writeln("Did it work? " + savedDate == newDate);

Setting the date with an ISO String didn’t do the trick, so I would recommend to test it first,
and use the XMPFile technique as a fallback.

Deleting a property is performed via deleteProperty(), with similar parameters. Let’s now try
something slightly more complex, such as updating an Array structure: we’ll first delete the existing
"subject" values, and then we’ll replace them with new values.

1 // Load the XMP Library


2 if (ExternalObject.AdobeXMPScript == undefined) {
3 ExternalObject.AdobeXMPScript = new ExternalObject('lib:AdobeXMPScript');
4 }
5 // Using a currently opened Document
6 var xmpMeta = new XMPMeta(app.activeDocument.xmpMetadata.rawData);
7
8 // Logging the old "subject" array
9 $.writeln("Old subjects [" +
10 xmpMeta.countArrayItems(XMPConst.NS_DC, "subject") +
11 "]\n" + logPropArray(xmpMeta, XMPConst.NS_DC, "subject"));
12 // Old subjects [7]
13 // ["watermelon", "neon", "sign", "sky", "blue", "sunset", "summer"]
Working with Metadata 276

14
15 // Delete the existing property
16 xmpMeta.deleteProperty(XMPConst.NS_DC, "subject");
17 $.writeln("Does 'subject' still esist? " +
18 xmpMeta.doesPropertyExist(XMPConst.NS_DC, "subject"));
19
20 // New Subjects to use
21 var newSubjects = ["food", "Italy", "artificial illumination"];
22
23 // Appending one "subject" at the time – the array is created in the fist cycle
24 for (var i = 0; i < newSubjects.length; i++) {
25 xmpMeta.appendArrayItem(
26 XMPConst.NS_DC, // namespace
27 "subject", // property name
28 newSubjects[i], // Array item
29 0, // Item type (default)
30 XMPConst.ARRAY_IS_UNORDERED); // Array type (e.g. ARRAY_IS_ORDERED)
31 } // Used only when the Array is first created
32
33 // Fixing the changes
34 app.activeDocument.xmpMetadata.rawData = xmpMeta.serialize();
35
36 // Logging the new Subjects
37 $.writeln("New subjects [" +
38 xmpMeta.countArrayItems(XMPConst.NS_DC, "subject") +
39 "]\n" + logPropArray(xmpMeta, XMPConst.NS_DC, "subject"));
40
41 function logPropArray(xmp, namespace, prop) {
42 var iterator = xmpMeta.iterator(
43 XMPConst.ITERATOR_JUST_LEAFNODES, namespace, prop);
44 var res = [];
45 while (true) {
46 var obj = iterator.next();
47 if (obj) { res.push(obj.value) }
48 else { break; }
49 }
50 return res.toSource();
51 }

Lines 1-6 are nothing new; I’ve then logged the old array using countArrayItems(), that returns
the Array’s length, and a custom utility function (lines 41-51) which creates an Iterator (very much
like the previous examples).
I’ve then called deleteProperty() (line 16), and made sure it was really gone thanks to doesPropertyExist().
Working with Metadata 277

The new "subject" keywords have been stored into a (line 21): I’ve then looped through it to add
each one of them via appendArrayItem(). The parameters are the usual namespace, property name,
array item to add, item type⁹, and Array type¹⁰; if the Array is missing – like here: it’s been deleted
alongside with the "subject" property – it is created in the first loop iteration (in which case the
Array type property, otherwise optional, is mandatory).

Custom Namespaces

If you need to store metadata that is not strictly pertinent to any of the available Schema, don’t
pollute existing slots: create your own namespace instead.

1 // Load the XMP Library


2 if (ExternalObject.AdobeXMPScript == undefined) {
3 ExternalObject.AdobeXMPScript = new ExternalObject('lib:AdobeXMPScript');
4 }
5 var xmpMeta = new XMPMeta(app.activeDocument.xmpMetadata.rawData);
6
7 var customNamespace = "http://ns.davidebarranca.com/example/1.0";
8 var customPrefix = "undavide:";
9
10 // Register a new Namespace (use the Class function)
11 XMPMeta.registerNamespace(customNamespace, customPrefix);
12 // Set a new string property (use the instance)
13 xmpMeta.setProperty(customNamespace, "scriptVersion", "1.0.000");
14 // Fix the xmpMeta
15 app.activeDocument.xmpMetadata.rawData = xmpMeta.serialize();
16 $.writeln(xmpMeta.serialize());

I’ve defined my own namespace URI (line 7), an URL-like string, and its custom prefix undavide:
which I’ve then registered via the Class function registerNamespace() (line 11). Let me stress that
it’s the Class method, hence it is a function of XMPMeta, not an instance method like the setProperty
one at line 13, that is a function of the xmpMeta variable.
The property that I’ve added is "scriptVersion", which has a String value of "1.0.000"; I’ve then
fixed the serialized object in the document’s rawData. The logged XML string features the new
namespaces property.

⁹0 is the default, other possible values: XMPConst.PROP_IS_ARRAY and XMPConst.PROP_IS_STRUCT.


¹⁰Either XMPConst.ARRAY_IS_ORDERED, XMPConst.ARRAY_IS_UNORDERED, XMPConst.ARRAY_IS_ALTERNATIVE.
Working with Metadata 278

1 <?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>


2 <x:xmpmeta xmlns:x="adobe:ns:meta/"
3 x:xmptk="Adobe XMP Core 5.6-c140 79.160451, 2017/05/06-01:08:21">
4 <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
5 <rdf:Description rdf:about=""
6 xmlns:xmp="http://ns.adobe.com/xap/1.0/"
7 xmlns:aux="http://ns.adobe.com/exif/1.0/aux/"
8 ...
9 xmlns:undavide="http://ns.davidebarranca.com/example/1.0"
10 ...
11 xmlns:exif="http://ns.adobe.com/exif/1.0/">
12 ...
13 <undavide:scriptVersion>1.0.000</undavide:scriptVersion>

It will never ever happen, but in case you want to check that your URI is not already used by someone
else, you can dump the used Namespaces:

1 $.writeln(XMPMeta.dumpNamespaces());
2 // AEScart: => http://ns.adobe.com/aes/cart/
3 // DICOM: => http://ns.adobe.com/DICOM/
4 // Iptc4xmpCore: => http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/
5 // Iptc4xmpExt: => http://iptc.org/std/Iptc4xmpExt/2008-02-29/
6 // adobe: => http://ns.adobe.com/xmp/aggregate/1.0/
7 // album: => http://ns.adobe.com/album/1.0/
8 // ... and 60 more...

The "scriptVersion" we’ve just entered is a String, of course, we can also use a different type of
values with slightly verbose strings.

17 // Set a boolean property


18 xmpMeta.setProperty(customNamespace, // Namespace
19 "hasBeenProcessed", // Property string
20 "false", // Value string
21 0, // simple-valued property
22 XMPConst.BOOLEAN // Property data type
23 );
24 // Result:
25 // <undavide:hasBeenProcessed>True</undavide:hasBeenProcessed>

Single key/value pairs are fine for most purposes, but why not exploit at least some of the complexity
the ISO Technical Committee has devised for us: let’s see how to append Arrays as metadata.
Working with Metadata 279

27 // Set an Array property


28 var retouchers = ["Magnus", "Judit", "Fabiano"];
29 // Filling the Array
30 for (var i = 0; i < retouchers.length; i++) {
31 xmpMeta.appendArrayItem(customNamespace, // Namespace
32 "retouchers", // Array to fill
33 retouchers[i], // value
34 0, // value type (default)
35 XMPConst.ARRAY_IS_UNORDERED // <rdf:Bag>
36 // XMPConst.ARRAY_IS_ORDERED // <rdf:Seq>
37 // XMPConst.ARRAY_IS_ALTERNATIVE // <rdf:Alt>
38 );
39 }

I’ve defined a retouchers Array of strings; looping the Array’s values I’ve then appendArrayItem(),
passing the custom namespace, the parent property ("retouchers") and the value. The Array type
is required¹¹: depending on the constant value used, you’re going to get unordered ( <rdf:Bag>),
ordered (<rdf:Seq>) or alternative (<rdf:Alt>) Arrays. Don’t forget to put xmpMeta in the rawData
to apply the operation. See below the result:

1 <undavide:retouchers>
2 <rdf:Bag>
3 <rdf:li>Magnus</rdf:li>
4 <rdf:li>Judit</rdf:li>
5 <rdf:li>Fabiano</rdf:li>
6 </rdf:Bag>
7 </undavide:retouchers>

There’s one last metadata type I want to cover here, which resembles an Object and is defined in
XMP papers as Structures. It is a container of named properties, let’s check it.

74 // Set a Structure property


75 var dimensions = {
76 width: 8.5,
77 height: 11,
78 units: "inches"
79 }
80
81 for (var key in dimensions) {
82 xmpMeta.setStructField(customNamespace, // Namespace
83 "dimensions", // Structure name
¹¹Actually, it is required only in the first iteration, when the <rdf> is first created.
Working with Metadata 280

84 customNamespace, // Field type namespace


85 key, // Key
86 dimensions[key], // Value
87 0 // default type value
88 // use it when nesting other structures
89 // XMPConst.PROP_IS_ARRAY
90 // XMPConst.PROP_IS_STRUCT
91 );
92 }

This time the method is setStructField(), and like Array’s, it creates one if the Structure is not there
yet. You need to pass it the Namespace (twice, I’ve never really understood why), the Structure name
("dimensions"), the key and its value, and the default type value. I’ve done that looping through the
dimensions object keys, and the result in the XMP is:

1 <undavide:dimensions rdf:parseType="Resource">
2 <undavide:width>8.5</undavide:width>
3 <undavide:height>11</undavide:height>
4 <undavide:units>inches</undavide:units>
5 </undavide:dimensions>

Since we’ve never encountered Structures yet, this is how you would read them:

102 $.writeln(xmpMeta.getStructField(
103 customNamespace, // Namespace
104 "dimensions", // Structure name
105 customNamespace, // Field type namespace
106 "width" // Key to get
107 ));
108 // 8.5

Please note that both Arrays and Structures can be nested, and host other Arrays and Structures, if
needed. Also, you can further explore the complexity of XMP, implementing Property Qualifiers in
Structures (sort-of properties of properties). All the XMP Classes information, and object methods are
listed in the JS Tools Guide (pages 257-293), but for I’d say 95% of the users, what’s been demonstrated
here so far is enough to consider your XMP knowledge well above-average.

Per Layer Metadata

The XMP metadata you’ve seen so far can be embedded in single Layers too, starting from Photoshop
CS4. Nothing has to change in the way you set and get properties, Arrays and Structures, the only
difference is that instead of targeting the Document’s rawData, you’re acting upon the Layer’s.
Working with Metadata 281

1 if (app.activeDocument.activeLayer.isBackgroundLayer) {
2 throw new Error("Can't operate on the Background Layer.");
3 }
4 // Load the XMP Library
5 if (ExternalObject.AdobeXMPScript == undefined) {
6 ExternalObject.AdobeXMPScript = new ExternalObject('lib:AdobeXMPScript');
7 }
8
9 // Getting the layer's rawData
10 var xmpMeta = undefined;
11 try {
12 // Grab the existing metadata, if any...
13 xmpMeta = new XMPMeta(app.activeDocument.activeLayer.xmpMetadata.rawData);
14 $.writeln("Found existing metadata.")
15 } catch (e) {
16 // ... or create an empty one
17 xmpMeta = new XMPMeta();
18 $.writeln("No metadata found.")
19 }

Mind you, the Background layer cannot have XMP metadata, hence the check on line 1. Also note
that getting the XMPMeta object from the layer’s rawData throws “Error: ‘value’ property is missing”
if there’s nothing stored yet, that’s the reason of the try/catch block, and the creation of a brand
new, empty xmpMeta on line 17.
From this point on, every Document XMP example shown so far holds for Layers as well – as an
example let’s write some data (refer to the previous sections for detailed explanations on setting
data).

21 // Register a new Namespace


22 var customNamespace = "http://ns.davidebarranca.com/example/1.0";
23 var customPrefix = "undavide:";
24 XMPMeta.registerNamespace(customNamespace, customPrefix);
25
26 // Setting a String
27 xmpMeta.setProperty(customNamespace, "retoucher", "Ding");
28
29 // Setting a Boolean
30 xmpMeta.setProperty(customNamespace, // Namespace
31 "hasBeenProcessed", // Property string
32 "true", // Value string
33 0, // simple-valued property
34 XMPConst.BOOLEAN // Property data type
Working with Metadata 282

35 );
36
37 // Setting a Date
38 xmpMeta.setProperty(customNamespace, // Namespace
39 "lastExportationDate", // Property string
40 new XMPDateTime(new Date()), // Value
41 0, // simple-valued property
42 XMPConst.XMPDATE // Property data type
43 );
44
45 // Setting an Array
46 var filters = ["Curves", "Hue/Sat", "USM"];
47 // Filling the Array
48 for (var i = 0; i < filters.length; i++) {
49 xmpMeta.appendArrayItem(customNamespace, // Namespace
50 "filters", // Array to fill
51 filters[i], // value
52 0, //value type (default)
53 XMPConst.ARRAY_IS_ORDERED // <rdf:Seq>
54 );
55 }
56
57 // Setting a Structure
58 var placedGraphic = {
59 x: 102,
60 y: 133,
61 rotation: 45,
62 origin: "center"
63 }
64 for (var key in placedGraphic) {
65 xmpMeta.setStructField(customNamespace, // Namespace
66 "placedGraphic", // Structure name
67 customNamespace, // Field type namespace
68 key, // Key
69 placedGraphic[key], // Value
70 0 // default type value
71 );
72 }
73
74 // Fix the changes
75 app.activeDocument.activeLayer.xmpMetadata.rawData = xmpMeta.serialize();
76 // Then remember to save the file
Working with Metadata 283

The result is as expected:

1 <rdf:Description rdf:about=""
2 xmlns:undavide="http://ns.davidebarranca.com/example/1.0">
3 <undavide:retoucher>Ding</undavide:retoucher>
4 <undavide:hasBeenProcessed>True</undavide:hasBeenProcessed>
5 <undavide:lastExportationDate>
6 2018-03-30T13:04:20.307+02:00
7 </undavide:lastExportationDate>
8 <undavide:filters>
9 <rdf:Seq>
10 <rdf:li>Curves</rdf:li>
11 <rdf:li>Hue/Sat</rdf:li>
12 <rdf:li>USM</rdf:li>
13 </rdf:Seq>
14 </undavide:filters>
15 <undavide:placedGraphic rdf:parseType="Resource">
16 <undavide:x>102</undavide:x>
17 <undavide:y>133</undavide:y>
18 <undavide:rotation>45</undavide:rotation>
19 <undavide:origin>center</undavide:origin>
20 </undavide:placedGraphic>
21 </rdf:Description>

8.2 Generator Metadata


A different mechanism of data storage, both at the Document and Layer level, is implemented via
the Generator API: because Adobe Generator is a vast topic, I would ask you to jump to Chapter
10 where it is covered in great depth, to follow the discussion on metadata in the context of this
particular technology.

8.3 Photoshop Registry


Metadata can also exist outside the XMP realm, and act on a higher level: not stored in a Document
Layer, nor a File on disk, but within the Application itself. Photoshop has, in fact, a Registry that
you’re allowed to save information to. Such data can persist from session to session – you may
restart the application, and still be able to access it – however, it gets flushed out when the user
resets the Preferences. You should keep this in mind when deciding what kind of data you want
to store this way.
Working with Metadata 284

On the other hand, the Registry can’t be easily inspected as, say, XMP metadata (that by design
is exposed to anyone’s eyes in the File Info panel), but I can’t assure you it’s 100% safe too. If you
have skipped Chapter 6 (about ActionManager), you should visit it now, because Registry operations
involve a small dose of AM code.
Whatever the Registry actually is (or where it is stored) it’s not really important¹²; what matters is
that we’re given a System that allows the association of key/value pairs, where the key is always a
String, and the value is always an ActionDescriptor. In the ActionDescriptor itself, you can store
all kind of stuff.

1 function s2t(s) {return stringIDToTypeID(s)};


2
3 // Create an empty ActionDescriptor
4 var d = new ActionDescriptor();
5 // Populate the AD with a String
6 d.putString(s2t('country'), 'Italy');
7 // Populate the AD with an Integer
8 d.putInteger(s2t('age'), 42);
9
10 // Save the `d` ActionDescriptor in the Registry under the "unDavide" key
11 app.putCustomOptions("unDavide", d);

Besides the s2t() utility function, you see that (line 11) I’ve used the putCustomOptions() method of
the app object to associate and store the "unDavide" key with a value that is the d ActionDescriptor. In
Photoshop jargon, the CustomOptions are synonymous of Registry. In turn, the d ActionDescriptor
is a container of key and value pairs, defined using the various put-something methods; they depend
on the value type, putInteger() associates a key with an Integer value, putString() associates a
key with a String value, etc.

Save a Key/Value pair in the Photoshop Registry

¹²The actual data is saved in the .psp file with the Application Preferences.
Working with Metadata 285

I would suggest looking at the above illustration from right to left. You have stored two key/value
pairs into an ActionDescriptor, which in turn is stored as the value of the "unDavide" key in the
Registry. You may be slightly suspicious about those s2t() functions: why are they needed, and
what’s the typeID of the 'country' String after all?
Each key/value pair, by definition, needs a key: so if you’re storing 'Italy' and 42 as values, some
keys must be provided as well; especially to allow you to retrieve the values back, which you’ll see
in a moment. On the other hand (as I’ve discussed in Chapter 6), the stringIDToTypeID() function
(shortened in s2t()) accepts all kind of Strings, and returns a number¹³ – which is exactly what all
the ActionDescriptor put-something methods do require as keys.
How do you query the Registry for saved CustomOptions?

13 // Get back the CustomOptions


14 var d = app.getCustomOptions("unDavide");
15 var country = d.getString(s2t('country'));
16 var age = d.getInteger(s2t('age'));
17 alert("Custom Options for the key: 'unDavide'" +
18 "\ncountry: " + country +
19 "\nage: " + age);

You can remove the CustomOptions via eraseCustomOptions()¹⁴, passing the key String.

21 // Delete CustomOptions
22 app.eraseCustomOptions("unDavide");

As stated by the JS Scripting Reference (pages 48-50), putCustomOptions() accepts a third, optional
parameter: a persistent boolean that “indicates whether the object should persist once the script has
finished”. According to my tests, it does not work: the data persists no matter if it’s true or false.
I have also provided a couple of useful utility functions, one called objectToDescriptor() and
one descriptorToObject() within the DescriptorUtils.jsx file. These are found in some of the
Photoshop’s Scripts that do “create an ActionDescriptor from a JavaScript Object”, and do “update
a JavaScript Object from an ActionDescriptor”. They support only Boolean, String, Number, and
UnitValue types; you may find them useful in this context.

8.4 External Files


When it comes to Script metadata – the kind of information that a Script might need to preserve
from launch to launch, or as a default parameters set to load when it’s run – it’s not uncommon at
¹³'country' turns out to correspond to 4634 on my machine right now (your result may differ).
¹⁴Some methods are called delete-something, others remove-something, this one is erase.
Working with Metadata 286

all for developers to dump data in a File on disk. ExtendScript, as you recall from Chapter 5, has
full-fledged tools for Filesystem I/O.
Before tackling the what, let me mention the where: depending on the purpose of your data you may
decide to save it alongside the main script or hide it in some remote sub-subfolder. Review this table
for a list of Folder tokens that point to specific System directories; also, remember that you can set
the hidden property of a File and conceal it from prying eyes.
Speaking of the kind of data format to use, I’ve seen either plain text files (usually with a .ini
extension), or more structured .xml and .json files. I would suggest these two because I find them
better suited for the purpose.

JSON Example: saving the GUI status

To demonstrate how to read and write data on external files, let’s say we have a ScriptUI Window
with some predefined elements, which status can be saved when the Dialog closes and loaded when it
opens. There’s a pre-filled Listbox: you can add extra elements to it typing something in the EditText
field, and clicking the Add button below. I’ve added three RadioButtons and a Slider, which values
you should try to change as well – to verify they stick at the next launch.
The script reads and writes data as JSON – hence the .jsx needs to include the json2.js code to
implement both JSON.stringify() and JSON.parse() methods. Everything boils down to saving
data as a stringified object on a file, and parsing the file content into an object again.

When the Script is first launched, readJSON() looks for the .json file: if it’s there, it reads its content
(which is a JSON string), it JSON.parse() it, and returns an Object to the setGUIWithData() function.
Which in turn fills the Listbox items, checks the RadioButton, set the Slider’s value, etc. In case
the .json file doesn’t exist, setGUIWithData() uses a defaults object to fill the GUI with default
parameters.
When the “Save and Close” button is clicked, before closing the Dialog, the script uses the
getDataFromGUI() function to build a preferences Object, that is handed to saveJSON(): the object
is then JSON.stringify() (converted into a String), that is eventually written in the .json file on
Disk.
Working with Metadata 287

The entire source-code is found in the Chapter’s folder – some relevant parts are discussed here. I’ve
defined the usual shortcut variables (pointing to GUI elements – the dialog has already been created
via resource string), and the defaults object; also I’ve created a reference to the .json file¹⁵.

37 // Using the resource string to build the dialog


38 var d = new Window(res);
39 // Shortcuts
40 var list = d.firstRow.list,
41 newItem = d.firstRow.rightGroup.newItem,
42 addButton = d.firstRow.rightGroup.addButton,
43 radiobuttons = d.firstRow.rightGroup.rbGroup,
44 slider = d.range,
45 cancel = d.secondRow.cancelButton,
46 saveClose = d.secondRow.okButton;
47 deleteJSON = d.secondRow.deleteJSONButton;
48
49 // Defaults values to be loaded
50 var defaults = {
51 items: ['Button', 'Dropdown menu', 'Call to Action', 'Text Area'],
52 listSelectionIndex: 0,
53 radiobuttonSelectionIndex: 0,
54 newItemText: 'Type here, and click Add',
55 sliderValue: 42
56 };
57 // Create a pointer to the JSONFile
58 var JSONFile = File(File($.fileName).path + '/ExternalFileJSON.json');

Skipping the Buttons handlers, the Script continues with the line that fills the GUI with Data.

77 // Fill the GUI with either JSON data, if it's been saved,
78 // or with defaults if it's the first run
79 setGUIWithData(readJSON());

For convenience, I’ve built two separate functions, one that gets the Data, the other that uses it to
fill the GUI:

¹⁵As you remember, an ExtendScript File object can exist even if it points to a File that doesn’t exist in the Filesystem.
Working with Metadata 288

81 // Return an Object, or undefined


82 function readJSON() {
83 var str;
84 try {
85 JSONFile.open('r');
86 str = JSONFile.read();
87 JSONFile.close();
88 } catch (e) { alert("I/O Error reading the JSON file") }
89 if (str) { return JSON.parse(str) } else { return undefined };
90 }
91
92 // Set GUI with data, either from the JSON or defaults
93 function setGUIWithData(obj) {
94 // The data has been successfully read from JSON
95 if (obj != undefined) {
96 newItem.textselection = obj.newItemText;
97 newItem.active = true;
98 slider.value = obj.sliderValue;
99 // List
100 for (var i = 0; i < obj.items.length; i++) {
101 list.add('item', obj.items[i])
102 }
103 list.selection = obj.listSelectionIndex;
104 // Radiobuttons
105 for (var j = 0; j < radiobuttons.children.length; j++) {
106 radiobuttons.children[j].value =
107 (obj.radiobuttonSelectionIndex == j) ? true : false;
108 }
109 } else {
110 // Fill it with defaults
111 newItem.textselection = defaults.newItemText;
112 newItem.active = true;
113 slider.value = defaults.sliderValue;
114 // List
115 for (var i = 0; i < defaults.items.length; i++) {
116 list.add('item', defaults.items[i])
117 }
118 list.selection = defaults.listSelectionIndex;
119 // Radiobuttons
120 for (var j = 0; j < radiobuttons.children.length; j++) {
121 radiobuttons.children[j].value =
122 (defaults.radiobuttonSelectionIndex == j) ? true : false;
123 }
Working with Metadata 289

124 }
125 }

As you see, setGUIWithData() has one main if statements, that either make the function use the
provided obj parameter to fill the GUI or the defaults in case obj is undefined. The rest of the code
should already be clear if you’ve survived Chapter 7 – it’s a matter of adding items to the Listbox,
value and text properties for RadioButtons, Sliders, and EditText, etc.

The “Save and Close” button onClick handler triggers the reverse process:

66 saveClose.onClick = function() {
67 saveJSON(getDataFromGUI());
68 alert("GUI Data successfully saved in the JSON");
69 d.close();
70 }

See below the code for the two functions:

127 // Gathers GUI params in an object and returns it.


128 function getDataFromGUI() {
129 var obj = {};
130 // Collect List items
131 var itemsArray = []
132 for (var i = 0; i < list.items.length; i++) {
133 itemsArray.push(list.items[i].text);
134 }
135 obj.items = itemsArray;
136 // Selected element in the List;
137 obj.listSelectionIndex = list.selection.index;
138 // Selected Radiobutton;
139 for (var j = 0; j < radiobuttons.children.length; j++) {
140 if (radiobuttons.children[j].value == true) {
141 obj.radiobuttonSelectionIndex = j;
142 }
143 }
144 obj.newItemText = newItem.text;
145 obj.sliderValue = slider.value;
146 return obj;
147 }
148
149 // Save JSONFile
150 function saveJSON(obj) {
151 var str = JSON.stringify(obj);
Working with Metadata 290

152 try {
153 JSONFile.open('w');
154 JSONFile.write(str);
155 JSONFile.close();
156 } catch (e) { throw new Error("I/O Error writing the JSON file") }
157 }

getDataFromGUI() builds an object from the GUI, and saveJSON() stringifies and saves it into the
.json file.

XML Example: a complete Preset system

I’ve created another ScriptUI Dialog to demonstrate


how to save and retrieve XML data: this one pretends
to be an alternative UI for the UnSharpMask Filter, with
the usual three sliders. I’m using XML to store Presets,
each one of which recalls a different set of sliders’
positions/values. Presets belong to a DropDown List,
and besides the built-in ones, you’re allowed to create
additional Presets clicking the “New” button. You can
also “Remove” them, but – as an extra feature – you
can’t get rid of the three Presets that come by default:
they are somehow tagged as permanent.
I will discuss in detail the most salient elements. The
default presets are internally defined as an XML literal,
which is an ExtendScript peculiarity – in other words,
it breaks traditional JS obfuscation.

72 var presetFile = new File("" + resPath + "/presets.xml");


73 var defaultXML = <presets>
74 <preset default="true">
75 <name>select...</name>
76 <amount></amount>
77 <radius></radius>
78 <threshold></threshold>
79 </preset>
80 <preset default="true">
81 <name>Default value</name>
82 <amount>300</amount>
83 <radius>2</radius>
84 <threshold>0</threshold>
Working with Metadata 291

85 </preset>
86 ...
87 <presets>

You may remember the content of the defaultXML variable from Chapter 5. The root element
<presets> have few <preset> children, each one of them with four tags (the three sliders plus a
name), and one attribute (the default boolean) controlling the permanence: all built-in presets have
default="true", while user-created ones will be equal to false – hence, they can be deleted.

The script logic, less complex than the diagram below would suggest, will be discussed right away:

The idea is first to initialize the DropDown List (DDL for simplicity) looking for an xml file (line 72
in the previous snippet); if it does not exist – for any reason – one will be created with the content
from defaultXML, and initDDL() recursively called.
Working with Metadata 292

144 // Initialize DropDownList


145 function initDDL() {
146 // if file doesn't exist...
147 if (!presetFile.exists) {
148 // Create the default XML
149 createDefaultXML();
150 // Recursive call to itself
151 initDDL();
152 }
153 // ...

Where the function to create the default xml file is quite simple:

113 // Create a Default XML file


114 function createDefaultXML() {
115 if (!presetFile.exists) { writeXML(defaultXML) } else {
116 presetFile.remove();
117 createDefaultXML();
118 }
119 return true;
120 };
121
122 // Write an XML object to a file
123 function writeXML(xml, file) {
124 if (file == null) { file = presetFile }
125 try {
126 file.open("w");
127 file.write(xml);
128 file.close();
129 } catch (e) { alert(e.message + "\nThere are problems writing the XML file!")}
130 return true;
131 };

In case the xml file does exist, it is read via the following function, which returns a proper XML
Object:
Working with Metadata 293

100 // Read a file (default = presets.xml) and returns an XML object


101 function readXML(file) {
102 var content;
103 if (file == null) { file = presetFile }
104 try {
105 file.open('r');
106 content = file.read();
107 file.close();
108 return new XML(content);
109 } catch (e) { alert(e.message + "\nThere are problems reading the XML file!")}
110 return true;
111 };

The content of the XML is used to populate the DDL.

153 // file exists


154 xmlData = readXML();
155 // clean DropDownList
156 if (presetList.items.length !== 0) { presetList.removeAll() }
157 // how many presets?
158 var nameListLength = xmlData.preset.name.length();
159 // empty the names array
160 presetNamesArray.length = 0;
161 // fills the DDL and the presetNamesArray
162 var i = 0;
163 while (i < nameListLength) {
164 // .toString() otherwise its an array of XML objects!
165 presetNamesArray.push(xmlData.preset.name[i].toString());
166 presetList.add("item", xmlData.preset.name[i]);
167 i++;
168 }
169 presetList.selection = presetList.items[0];
170 };

First, the DDL is emptied (line 156), and an xmlData variable used to store the existing XML. Then,
the presetNamesArray variable is used to store the name strings (something that will turn out to
be useful when adding/removing user presets too: be aware that you need to toString() them,
otherwise you’ll store XML nodes instead), and eventually the names are pushed into the DDL as
new 'items' (line 166).
Saving a new Preset (as an onClick handler) means to populate a <preset> template, and append it
to the existing XML Data.
Working with Metadata 294

205 // Save New Preset Button


206 saveNewPresetButton.onClick = function () {
207 var presetName = prompt("Give your preset a name!" +
208 "\nYou'll find it in the preset list.", "User Preset", "Save new Preset");
209 if (presetName == null) { return }
210 // if presetName exists in the presetNamesArray
211 if ( indexOf.call(presetNamesArray, presetName) >= 0) {
212 alert("Duplicate name!\nPlease find another one.");
213 // recursion again
214 saveNewPresetButton.onClick.call();
215 }
216 // create a <preset> xml child
217 var child = createPresetChild(presetName,
218 amountText.text,
219 radiusText.text,
220 thresholdText.text);
221 // append the child to the xmlData
222 xmlData.appendChild(child);
223 writeXML(xmlData);
224 // re-initialize the DDL in order to make changes appear
225 initDDL();
226 // select the last preset
227 presetList.selection = presetList.items[presetList.items.length-1];
228 };

I first check if the new preset name already exists in the current presets list (the indexOf is a shim
for the JavaScript Array’s own indexOf, see the function body in the source-code), then I delegate
the creation of the <preset> child to a dedicated function (line 217), and make use of the built-in
appendChild() method to add the child to the xmlData. At this point (line 225), the entire DDL must
be re-initialized, to be up-to-date with the current presets XML content; as the last step, (line 227)
the DDL selects the newly created preset. The function to create the <preset> child is as follows.

133 // Create and returns an XML <preset> element


134 function createPresetChild(name, amount, radius, threshold) {
135 var child = <preset default="false">
136 <name>{name}</name>
137 <amount>{amount}</amount>
138 <radius>{radius}</radius>
139 <threshold>{threshold}</threshold>
140 </preset>;
141 return child;
142 };
Working with Metadata 295

Note the syntax with curly braces. Deleting a preset is performed with this function, that checks the
.@default attribute. Remember to use toString() and compare it against the "true" string (not a
boolean).

230 // Delete Preset Button


231 deletePresetButton.onClick = function () {
232 // "true" is a string in the XML, not a boolean!
233 if (xmlData.preset[presetList.selection.index].@default.toString() === "true") {
234 alert("Can't delete \"" +
235 xmlData.preset[presetList.selection.index].name +
236 "\"\nIt's part of the default set of Presets");
237 return;
238 }
239 if (confirm("Are you sure you want to delete \"" +
240 xmlData.preset[presetList.selection.index].name +
241 "\" preset?\nYou can't undo this.")) {
242 delete xmlData.preset[presetList.selection.index];
243 }
244 writeXML(xmlData);
245 // re-initialize the DDL in order to make changes appear
246 initDDL();
247 };

I won’t show here the code for resetting the Presets list – it’s just a matter of creating a default XML,
like if the file on disk wasn’t found – please refer to the full source code.
With this last example, I conclude the Chapter on Metadata – you’ve seen how to embed
standardized and customized XMP metadata, exploit the Photoshop Registry, or save text files (either
as json or xml) with meaningful and structured information: another important item in your scripter
toolbox.
9. Events
In Chapter 6, we’ve seen how ActionManager is based upon an underlying net of Events, that are
fired when actions are performed and have associated Descriptors. There is one other technique that
allows us to exploit this system, and respond to those Events in a precise way.

9.1 Script Events Manager


Little known to the majority of users, in the “File > Scripts” menu there’s an item called “Script
Events Manager”, which opens the following dialog.

The GUI is slightly confusing, but the general idea is that we’re allowed to define Event Listeners: if
something happens in Photoshop, then something else (either a Script or an Action) is automatically
triggered. There are subtleties that I’m going to discuss soon, but in general terms, it is a potent
technique: either for automation purposes or workflow control in complex image processing
pipelines when multiple operators are involved. For instance, you may want that:

• when an image is saved, a .jpg thumbnail with a timestamp in the filename is also saved in a
predefined folder;
• if a CMYK image with missing ICC profile is opened, a popup alerts the user about few
alternatives for Profile Assignation;
Events 297

• when a file is saved and closed, all guides and alpha channels are removed, and information
about the Photoshop operator and the current session are saved as XMP metadata;

These are just suggestions, the possibilities are endless – your actual needs will surely represent
better examples than mine. The Script Events Manager is one of the doors that let you enter this
world, so let’s step in.
First, make sure the “Enable Events to Run Script/Actions” checkbox is active. Then, in the
“Photoshop Event” dropdown list pick up “New Document”; if it’s not already selected, click on
the “Script” radiobutton and select “Welcome” – the string “Show a simple alert when Photoshop
starts.” appears in the info box below. You’ve now linked the Event (each time a New Document is
created…) to the handler (… a welcome alert pops up); it’s not really active until you click the “Add”
button, however! So click it¹.
The final status of the dialog should appear as follows.

Let’s test it: create a new document, and you should see an alert saying: “You have successfully
configured an event triggering a JavaScript.” Mildly exciting, as any Hello World-ish experiments
are, but this Welcome dialog is quite helpful when debugging/looking for the proper Event to listen
to.
¹When I’ve encountered unexpected errors testing Script Events Manager, ten out of ten times I had just forgotten to click the Add button.
Events 298

The “Photoshop Event” list comes pre-populated (Start Application, New Document, Open Docu-
ment, etc. plus an interesting Everything item, which you should use with prudence). Of course,
they’re just a few examples: you can add the Events you want, defined either via their charID or
stringID². Please refer to this section in Chapter 6 for a quick reminder: in summary, pick the
charID/stringID that appears as the first parameter in the executeAction() call, at the end of an
ActionManager chunk of code. For instance, this is the ScriptListener log for selecting the Hand Tool
(refactored for clarity):

1 var d1 = new ActionDescriptor();


2 var r1 = new ActionReference();
3
4 r1.putClass(s2t('handTool'));
5 d1.putReference(s2t('target'), r1);
6 d1.putBoolean(s2t('dontRecord'), true);
7 d1.putBoolean(s2t('forceNotify'), true);
8 executeAction(s2t('select'), d1, DialogModes.NO);

You see the 'select' stringID in the last line, do you? This is what you’re after when identifying
Events. In the dialog that shows up clicking “Add an Event…” in the “Photoshop Event” dropdown,
the Select event can be either added as 'Slct' or 'select' (without quotes).

It is quite a deceptive dialog though, because the Descriptive Label and Event Name fields are
reversed: you must put the charID/stringID in the Label’s, and the Label (i.e., whatever arbitrary
String you may want to associate to the Event), in the Event’s. This covers the first part, the Event
to listen to; what about the handler?
Similarly, the Script dropdown list (the one outlined in Magenta in the previous screenshot) is pre-
populated with some items, but you can add your own. Click “Browse…” and in the following dialog
that opens, select a .jsx script from the Filesystem. In other words, while Event Listeners, as we’ve
encountered them in Chapter 7, use a callback function as the handler, the Script Events Manager
²There are some caveats, that will be considered thereafter.
Events 299

requires a File; the code within that File is going to be executed straight away as a response to the
fired Event. The Welcome item we’ve used earlier is a plain alert() call, wrapped with some extra
utility code – you can check yourself browsing to the Photoshop’s /Presets/Scripts/Event Scripts
Only/ folder, where you can find Welcome.jsx alongside a few other scripts.

Linking the 'select' event to the Welcome handler works (remember to click the Add button!), as
you can test selecting different tools (Hand, Brush, etc.) in the Tools palette, and watching the alert
pop up.

To sum up what we’ve learned so far: you can set up Event Listeners for Photoshop Events,
that use charID/stringID as the Event identifiers, and .jsx files (or Actions as well) as the
callbacks. The Script Events Manager is the dedicated, built-in GUI that lets you make/revoke
this association – i.e., add/remove such Event Listeners.

This is a perfectly working, yet superficial description: in fact, the Script Events Manager window
is a ScriptUI dialog itself, which code you can peek in the Script Events Manager.jsx file found
within the /Presets/Scripts/ folder. It turns out that the Events list is populated reading from this
file:

~/Library/Preferences/Adobe Photoshop CC 2018 Settings/Script Events Manager.xml

Which contains XML code along these lines:

1 <ScriptEventsManager>
2 <events>
3 <0>
4 <name>Start Application</name>
5 <value>Ntfy</value>
6 <valueClass></valueClass>
7 </0>
8 <1>
9 <name>New Document</name>
10 <value>Mk </value>
11 <valueClass>Dcmn</valueClass>
12 </1>
13 <!-- ... etc. ... -->
14 </events>
15 </ScriptEventsManager>

The <name> corresponds to the “Event Name” field, and <value> to the Descriptive Label; please
note that charIDs are always made of 4 chars, so whitespaces must be added when needed. You
see that there’s also a <valueClass> tag, which may or may not be used. For instance, the “New
Events 300

Document” item is built with a value of 'Mk ', and a valueClass of 'Dcmn'. Which, if you know
your ActionManager, is equivalent to 'make', and 'document'.
This leads us to a more appropriate definition of Event Management, which also involves a change
in nomenclature.

9.2 Notifiers
Adobe Photoshop doesn’t have the notion of Event Listeners, but uses Notifiers instead – it’s the
very same concept; besides, you don’t need the Script Events Manager dialog either. In fact, the
Notifiers collection is a property of the Application, and can be used independently (see Photoshop
JS Reference, page 138). The Script Events Manager dialog is nothing but a visual tool that helps
adding, and removing, simple Notifiers – the reason why I call them simple will be evident in a
short while.
Listening to the same 'select' event is performed with code this way:

1 // First make sure that Notifiers are enabled


2 app.notifiersEnabled = true;
3 // Pointer to the JSX callback file
4 var handlerFile = File(File($.fileName).path + '/resources/alert.jsx');
5 // Adding a notifier
6 app.notifiers.add('select', handlerFile);

The notifiersEnabled property is the equivalent of the “Enable Events to Run Script/Actions”
checkbox; the notifiers collection has the add() method, that accepts the Event charID/stringID
(here 'select'), a pointer to the .jsx File callback, and the Event Class specifier (here 'document').
Run this code, then select a different tool and the alert(), from the alert.jsx handler file will pop
up.

Caveats
Tom Ruark explained in a Forum post that we should preferably use charIDs over StringIDs.
After some testing, I’ve found that on the one side it’s true that we can directly use either
stringIDs or charIDs (e.g. 'select' and 'Slct') as parameters of the notifiers.add()
function; on the other side, this duality doesn’t apply, say, for 'make' and'Mk ', or 'open'
and 'Opn ': only the latter works. Moreover, hashed typeIDs values would also sometimes
work, e.g.

app.notifiers.add(charIDToTypeID('Slct'), handlerFile);

For these reasons, contrary to my habit, I will use charIDs throughout this Chapter.
Events 301

To be useful, Notifiers must be precise – which is not the case if they’re limited to one parameter
(Event) only. Take as an example the creation of a New Document and a New Layer – as a reminder,
the ActionManager code as follows:

1 function s2t(s) { return app.stringIDToTypeID(s) };


2
3 // Create a new Document
4 var d1 = new ActionDescriptor();
5 var d2 = new ActionDescriptor();
6 var l1 = new ActionList();
7
8 d2.putBoolean( s2t('artboard'), false );
9 d2.putClass( s2t('mode'), s2t('RGBColorMode'));
10 d2.putUnitDouble( s2t('width'), s2t('distanceUnit'), 420 );
11 d2.putUnitDouble( s2t('height'), s2t('distanceUnit'), 151 );
12 d2.putUnitDouble( s2t('resolution'), s2t('densityUnit'), 144 );
13 d2.putDouble( s2t('pixelScaleFactor'), 1 );
14 d2.putEnumerated( s2t('fill'), s2t('fill'), s2t('white'));
15 d2.putInteger( s2t('depth'), 8 );
16 d2.putString( s2t('profile'), 'Display' );
17 d2.putList( s2t('guides'), l1 );
18 d1.putObject( s2t('new'), s2t('document'), d2 );
19 d1.putInteger( s2t('documentID'), 508 );
20 executeAction( s2t('make'), d1, DialogModes.NO );
21
22 // Create a new Layer
23 var d1 = new ActionDescriptor();
24 var r1 = new ActionReference();
25 r1.putClass( s2t('layer'));
26 d1.putReference( s2t('target'), r1 );
27 executeAction( s2t('make'), d1, DialogModes.NO );

Both use the 'make' Event, which is the one that appears first in the list of executeAction()
parameters (line 20 and line 28). If you were just listening for such 'make' Event, how could you
tell the two apart? You could not. An additional (optional, but in this case very much needed) Event
Class parameter is available so that you can write the notifier this way.
Events 302

1 function c2t(c) { return charIDToTypeID(c) }


2 app.notifiersEnabled = true;
3 var handlerFile = File(File($.fileName).path + '/resources/alert.jsx');
4 // .add(event, eventFile, eventClass)
5 app.notifiers.add('Mk ', handlerFile, 'Dcmn'); // 'make', 'document'

This notifier precisely targets the making of a new document, thanks to the extra Event Class
parameter. At this point it should be clear that you need to get your hands dirty with at least some
ActionManager: to recap, the Event is the one in the executeAction() call, while the auxiliary Class
should be found (with a bit of ingenuity) at the root level of the ActionDescriptor ( 'document', in
the previous snippet). Remember to use them as charIDs, or hashed typeIDs – and make sure to test
them.
Before getting into more advanced facets of Notifiers, let me mention few other simple facts about
them: you are allowed to removeAll() Notifiers in the collection:

app.notifiers.removeAll();

In order to target a single Notifier, either for extracting properties or removing it, you can use the
Array index notation:

app.notifiers[0].event; // 'Mk '


app.notifiers[0].eventClass; // 'Dcmn'
app.notifiers[0].eventFile; // ~/Long/Path/to/handler.jsx
// you can directly remove the single notifier too:
app.notifiers[0].remove();

Arguments

Compared to regular Event Listener’s Callbacks, you may think to yourself that something is
missing. A handler is passed the Event itself as an argument, e.g.

1 someElement.addEventListener('click', function(event) { /* ... */ });

Can you do the same with Notifiers? It turns out that, with the help of a wacky .jsx File handler,
you can discover it yourself. This is the usual notifier, listening to the new document:
Events 303

1 function c2t(c) { return charIDToTypeID(c) }


2 app.notifiersEnabled = true;
3 var handlerFile = File(File($.fileName).path + '/resources/debugger.jsx');
4 app.notifiers.add('Mk ', handlerFile, 'Dcmn');

And this is the content of the debugger.jsx file:

1 $.level = 2;
2 alert("About to debug it!");
3 debugger;

The $.level is the current debugging level (0 is no debugging; 1 breaks on runtime errors; 2 is full
debug mode). Open the ESTK and connect it with Photoshop, launch the notifier code; now create
a new document, and get back to ESTK when it hits the debugger; line.

As you see, I’ve typed some code in the Console: similarly to traditional JavaScript, the particular
arguments Array-like³ object that usually groups the arguments (parameters) in a function body, is
available in this context as well⁴!
I’ve attempted to explore this object: its length is equal to 2. The first of its elements arguments[0]
appears to be an ActionDescriptor, while the second, arguments[1] is a long integer. This should
immediately trigger your ActionManager instinct, and let you perform a quick typeIDToStringID()
conversion: it results as 'make'. So, what’s up here?
It happens that in the .jsx File handler, an arguments object is available, and it usually contains two
or more things:

• an ActionDescriptor that is the result of the Event;


• the Event’s typeID;
³It’s defined this way because it doesn’t have any Array properties except length.
⁴See this page for more information on arguments.
Events 304

• additional information.

If you pimp up the debugger.jsx with some brief ActionManager inspector code like this:

1 $.level = 2;
2 alert("About to debug it!");
3 debugger;
4
5 function s2t(s) { return stringIDToTypeID(s) }
6
7 var d = new ActionDescriptor();
8 d.putObject( s2t("object"), s2t("object"), arguments[0] );
9 var jsonDesc = executeAction( s2t("convertJSONdescriptor"),
10 d, DialogModes.NO );
11
12 $.writeln(jsonDesc.getString(s2t("json")));

You’ll see interesting things logged… I’m inspecting the arguments[0] ActionDescriptor, which
results to be:

{
"_obj": "object",
"documentID": 487.0,
"new": {
"_obj": "document",
"artboard": false,
"depth": 8.0,
"fill": {
"_enum": "fill",
"_value": "white"
},
"guides": [],
"height": {
"_unit": "distanceUnit",
"_value": 512.0
},
"mode": {
"_class": "RGBColorMode"
},
"pixelScaleFactor": 1.0,
"profile": "sRGB IEC61966-2.1",
"resolution": {
Events 305

"_unit": "densityUnit",
"_value": 144.0
},
"width": {
"_unit": "distanceUnit",
"_value": 512.0
}
}
}

The result of the Event is the newly created Document, for which the ActionDescriptor is shown
above⁵. The purpose of this inspection is to provide even more granular control in the Notifier
process.
For instance, you may want to listen for the Event related to the application of a Filter (say, Gaussian
Blur), and react in a certain way depending on the blurring radius used. You already knew how to
set a Notifier, now you’re able to precisely target the handler .jsx File as well. The notifier is simply:

1 app.notifiersEnabled = true;
2 var handlerFile = File(File($.fileName).path + '/resources/gBlur.jsx');
3 app.notifiers.add('GsnB', handlerFile);

I’ve used the same debugger; statement in the handler, applied a Gaussian Blur filter to a dummy
image, and this time manually explored the arguments[0] descriptor:

You can see in the Console that I’ve typed few commands to get to the radius value (40 pixels) –
if you’re puzzled by these ones, please review Chapter 6. This is a one-time exploration, that I have
commented below:
⁵Each time you run an executeAction() call, the ActionDescriptor used is also returned; apparently, this is also the case with Events.
Events 306

// how many items in the argument object


arguments.length
Result: 2
// The Event
typeIDToStringID(arguments[1])
Result: gaussianBlur
// How many keys in the ActionDescriptor
arguments[0].count
Result: 1
// Getting the one available Key
arguments[0].getKey(0)
Result: 1382314784
// Forgetting that it would return a typeID; converting to stringID
typeIDToStringID(arguments[0].getKey(0))
Result: radius
// Finding the Type of such Key
arguments[0].getType(arguments[0].getKey(0))
Result: DescValueType.UNITDOUBLE
// Getting the Unit Type
arguments[0].getUnitDoubleType(arguments[0].getKey(0))
Result: 592476268
// Forgetting again that I should have turned it to a stringID
typeIDToStringID(arguments[0].getUnitDoubleType(arguments[0].getKey(0)))
Result: pixelsUnit
// Finally Getting the pixels value
arguments[0].getUnitDoubleValue(arguments[0].getKey(0))
Result: 40

As a result, the .jsx File handler can now properly detect the Radius used, and react accordingly:

1 var desc = arguments[0];


2 var key = desc.getKey(0)
3 var radius = desc.getUnitDoubleValue(key);
4 // checking the radius
5 if (radius >= 40) {
6 alert("Make sure to avoid posterization when converting to CMYK!")
7 }

In case you want to explore Events further, I would suggest you try the code from this forum
post, posted by the user habaki1. It attaches to all Events, and fires a very informative popup
of the arguments content, ActionDescriptor included.
10. Adobe Generator
Real-time Image Asset Generation is a feature that Photoshop enthusiastically introduced with the
first point update of the CC era (version 14.1, in late 2013), to suit the needs of web/game/UI
designers: it allows users to export Layers/LayerSets into .jpg and .png files – automatically, and
in the background.

A lesser known detail to the general audience, but a remarkably interesting one for us developers,
is that Assets Generation relies on a solid technology that we’re allowed to take advantage of for a
variety of other purposes.
In fact, Photoshop embeds a Node.js server – if you’ve never heard of Node, it’s a popular JavaScript
runtime built on Chrome’s V8 JavaScript engine; basically, a JS engine transplanted out of the
Browser (and in our case, directly into Photoshop)¹.

Especially if you are familiar with HTML Panels (which, too, can use Node.js), you must
be aware that CEP’s Node.js and the one I’ll discuss here are two separate things. They’re
both proper Node.js environments, but their version numbers differ – I can only speculate
about the reason why². At the time of this writing, the node executable in Photoshop 19.1.3
is v8.10.0, whereas CEP’s node is v7.7.4.
¹We’ve had the ExtendScript engine built-in Photoshop for many years, so I’ve not felt the excitement that took the world by storm when
Node.js got into the limelight.
²I’d say that they’re just two separate engineering teams, following two separate development cycles. At all events, Photoshop CC sports
not one but two Node.js runtimes.
Adobe Generator 308

Even though it’s Node.js at its heart, the Asset Generation technology and the glue that creates an
entirely new branch of scripting development is usually referred to as Adobe Generator. The project
has been open sourced from the early days and can be found in Adobe’s official generator-core
repository. This Chapter is basically a commented walkthrough the available documentation – that
in Adobe’s style is halfway for internal use, halfway for the general public, hence not very friendly
– a series of now offline blog posts by the former Adobe Product Manager Tom Krcha, plus a good
deal of personal experimentation.
Developers have four different options to choose from, in the Photoshop extensibility layer.

• ExtendScript (i.e. pure scripting).


• CEP (HTML) Panels.
• The Photoshop Connection SDK.
• Adobe Generator.

I mention the Connection SDK (included in the Photoshop SDK) because we’ll need it in a moment. If
you’ve not heard of the Connection SDK – also known as Kevlar, or KVLR – it is a piece of technology
supported since Photoshop CS5: you can set up Photoshop as a Server, accepting incoming TCP/IP
connections, and exchange ExtendScript strings, Images, Events or arbitrary data.

The Photoshop Connection SDK

The idea being that developers can build multi-platform applications for any kind of device (please
note that AIR was still trendy back then), which are then able to communicate with Photoshop via
Socket connections³.
The Photoshop server is enabled in the Plug-Ins tab of the Photoshop Preferences dialog, with a
default password of password.
³Adobe did release some mobile applications relying upon the Connect SDK, one of which let you group a subset of items that belong to
the Photoshop Tools Palette, that were displayed on the iPad for you to tap and select.
Adobe Generator 309

Mind you: the Connection SDK implies that Photoshop acts as a server to receive incoming
connections, and to respond accordingly; whereas Adobe Generator is mostly used as a
source of an outgoing stream of data. Being based on Node.js though, nothing prevents
you from setting up a socket server in Generator too, and accept incoming connections.

10.1 Plug-In development


Building Plug-Ins is what we do to exploit the Adobe Generator technology. To properly set up the
development environment, we’re suggested to disable the built-in Generator, unchecking “Enable
Generator” in the above Preferences dialog. Why? We will use an external version of Generator,
instead: according to the wiki, it is “easier to run your own, separate NodeJS interpreter, and to
point Photoshop to that, instead of its built-in NodeJS interpreter”.⁴
To further clarify this point: Photoshop is equipped with its Generator, yet you can plug a different
Generator version, and run it not with Photoshop’s own Node.js, but your local Node.js runtime.
This implies a Socket connection (i.e., the Connection SDK) hence you must check “Enable Remote
Connections” in the same Preferences tab – leaving Photoshop Server as the Service Name, and
password as Password. Also, make sure you’ve installed a relatively recent version of Node.js.

To deploy your Generator plug-in for production, move the plug-in folder within Photoshop’s own
Plug-ins/Generator/ folder.
⁴I admit that I don’t entirely get the reason why it should be easier, but let’s assume it really is.
Adobe Generator 310

Generator Getting Started

What I’ve called “a different Generator version” in the previous paragraph, is, in fact, the source code
from generator-core: the Node.js library that communicates with Photoshop over the Connect SDK
(aka Kevlar), and exposes the event-based API to Generator plug-ins. Download it from GitHub, and
unzip it in an empty folder (the base folder from now on); then open the Terminal, cd the unzipped
generator-core-master and run npm install to install the required dependencies.

Our first plug-in is going to be Tom Krcha’s Generator Getting Started, that I’ve forked here,
bumping the Generator Core version in the package.json to ∼3 to support the currently available
Generator.

In the base folder, create a new plugin/ folder and unzip the
generator-getting-started-master there (see the screenshot).
There’s nothing to npm install in here this time.
Launch Photoshop and open an image. In the Terminal, cd into the generator-core-master (not the
plugin) and type:

1 node app -f ../plugins

You are starting the external Generator Core application with the -f flag. A list of flags is found in
the Core’s app.js file:

63 "p": "Photoshop server port",


64 "h": "Photoshop server host",
65 "P": "Photoshop server password",
66 "i": "file descriptor of input pipe",
67 "o": "file descriptor of output pipe",
68 "f": "folder to search for plugins (can be used multiple times)",
69 "v": "include verbose generator logging in stdout",
70 "photoshopVersion": "tell Generator PS's version so it isn't queried \
71 at startup (optional)",
72 "photoshopPath": "tell Generator PS's path so it isn't queried \
73 at startup (optional)",
74 "photoshopBinaryPath": "tell Generator PS's binary location so \
75 it isn't queried at start(optional)",
Adobe Generator 311

76 "whiteListedPlugins": "A comma seperated list of plugin names \


77 that are ok to run (optional)",
78 "help": "display help message"

So -f just tells Generator where to look for plugins (i.e., in the sibling plugins directory). You should
then see something like this being logged in the Terminal:

Many things are going on here! Some more will be logged if you add the -v (verbose) flag. First,
each plugin must be contained in one folder, and it also must have a package.json in which the
Generator version it targets is specified (see line 6 below):

1 {
2 "name": "generator-getting-started",
3 "version": "1.0.0",
4 "description": "Getting started Adobe Generator",
5 "main": "main.js",
6 "generator-core-version": "~3",
7 "repository": {
8 "type": "git",
9 "url": "https://github.com/tomkrcha/generator-getting-started"
10 },
11 "license": "Public Domain",
12 "readmeFilename": "README.md",
13 "scripts": {
14 "test": "grunt test"
15 },
16 "dependencies": {},
17 "devDependencies": {}
18 }
Adobe Generator 312

The tilde in "∼3" locks the major version (in a semantic versioning scheme fashion, i.e., ma-
jor.minor.patch), specifying it requires Generator Core version 3. Also, the plugin entry point must
be defined in the "main" property, here main.js – where the actual plugin code is. The structure is:

1 (function () {
2 "use strict";
3
4 var PLUGIN_ID = require("./package.json").name,
5 MENU_ID = "tutorial",
6 MENU_LABEL = "$$$/JavaScripts/Generator/Tutorial/Menu=Tutorial";
7 var _generator = null,
8 _currentDocumentId = null,
9 _config = null;
10
11 // INIT
12
13 function init(generator, config) { /* ... */ }
14
15 // EVENTS
16
17 function handleCurrentDocumentChanged(id) { /* ... */ }
18 function handleImageChanged(document) { /* ... */ }
19 function handleToolChanged(document) { /* ... */ }
20 function handleGeneratorMenuClicked(event) { /* ... */ }
21
22 // CALLS
23
24 function requestEntireDocument(documentId) { /* ... */ }
25 function updateMenuState(enabled) { /* ... */ }
26
27 // HELPERS
28
29 function sendJavascript(str){ /* ... */ }
30 function setCurrentDocumentId(id) { /* ... */ }
31 function stringify(object) { /* ... */ }
32
33 // EXPORTS
34 exports.init = init;
35
36 }());

It’s an immediately invoked function expression (IIFE), exporting an init function. In the Events
section you see few handlers, which suggest that this Generator plugin is going to listen for several
Adobe Generator 313

events (e.g., when the document, image, and tool are changed, etc.): in fact, if you try operating
Photoshop, you’ll find several new logged lines. But let’s dig into the main init function.

39 function init(generator, config) {


40 _generator = generator;
41 _config = config;
42
43 console.log("initializing generator getting started tutorial with config %j",
44 _config);
45
46 _generator.addMenuItem(MENU_ID, MENU_LABEL, true, false).then(
47 function () {
48 console.log("Menu created", MENU_ID);
49 }, function () {
50 console.error("Menu creation failed", MENU_ID);
51 }
52 );
53 _generator.onPhotoshopEvent("generatorMenuChanged",
54 handleGeneratorMenuClicked);
55
56 function initLater() {
57 // Flip foreground color
58 var flipColorsExtendScript = "var color = app.foregroundColor; " +
59 "color.rgb.red = 255 - color.rgb.red; " +
60 "color.rgb.green = 255 - color.rgb.green; " +
61 "color.rgb.blue = 255 - color.rgb.blue; " +
62 "app.foregroundColor = color;";
63 sendJavascript(flipColorsExtendScript);
64
65 _generator.onPhotoshopEvent("currentDocumentChanged",
66 handleCurrentDocumentChanged);
67 _generator.onPhotoshopEvent("imageChanged", handleImageChanged);
68 _generator.onPhotoshopEvent("toolChanged", handleToolChanged);
69 requestEntireDocument();
70 }
71
72 process.nextTick(initLater);
73
74 }

When called, init is passed the generator object (defined in the generator-core, and providing all
the main Generator API), and a config object that turns out to be empty. In line 46 a new menu
Adobe Generator 314

item is created (find a new “Tutorial” item in the File > Generate submenu) via addMenuItem(), using
constants defined earlier.

The addMenuItem() function accepts four parameters: a String for the Menu ID, a String for
the Menu Display Name, a Boolean that controls whether the Menu is Enabled or Disabled,
and a Boolean that controls whether the menu is Checked or Unchecked.
Other Menu related functions are toggleMenu() (that accepts the parameters in a different
order: ID, Enabled, Checked, Display Name), and getMenuState(), that needs only the ID.

Generators’ functions make use of Promises so they are asynchronous: then() is called when the
promise is resolved (it succeeds) or rejected (it fails), and it’s passed two functions in this exact
order (one for dealing with the success result, one for dealing with the error result). You’re allowed
to chain multiple .then() calls, and
On line 53 you’re subscribing to the "generatorMenuChanged" event (i.e. when the user clicks the File
> Generate > Tutorial menu item), via the onPhotoshopEvent() method – the Generator equivalent of
addEventListener() – passing handleGeneratorMenuClicked as the callback, which will be defined
in the Events section and just logs a message.
On line 72 you find process.nextTick(), which is a method of Node’s exclusive process global
variable. Node.js is an “always-on” event-based environment which relies upon a single threaded
JavaScript engine, it keeps cycling through the so-called Event Loop. nextTick() ensures that the
function passed as the parameter will be executed on the next iteration (a tick, in Node’s jargon) of
the loop⁵.
The function that is called in the following tick in our case is initLater(), which is defined on lines
56-69. Besides adding three more event listeners (lines 65-67), it does a couple of remarkable things:
on the one side, it sends a String of ExtendScript code to the ExtendScript interpreter (line 63). Very
much like a CEP Panel, Generator’s Node cannot directly run any ExtendScript; it will hand it to
the JSX engine for evaluation. The sendJavascript() function body is declared later on, here it is
for your convenience.

126 function sendJavascript(str){


127 _generator.evaluateJSXString(str).then(
128 function(result){
129 console.log(result);
130 },
131 function(err){
132 console.log(err);
133 });
134 }
⁵See the official Node.js documentation about Event Loop and nextTick() here.
Adobe Generator 315

As you see, it’s just a wrapper to the native evaluateJSXString() method, plus some logging. On
the other side, initLater() also calls the requestEntireDocument() function that is in charge of
logging a lot of information related to the current document – here’s the function body:

103 function requestEntireDocument(documentId) {


104 if (!documentId) {
105 console.log("Determining the current document ID");
106 }
107
108 _generator.getDocumentInfo(documentId).then(
109 function (document) {
110 console.log("Received complete document:", stringify(document));
111 },
112 function (err) {
113 console.error("[Tutorial] Error in getDocumentInfo:", err);
114 }
115 ).done();
116 }

No documentId is given (and in fact "Determining the current document ID" is logged); hence
getDocumentInfo() is passed null, and as a result it will operate on the currently active document.
The result is a JSON.stringify() version of the collected document data, e.g.

Received complete document: {


"version": "1.5.0",
"timeStamp": 1524128117.421,
"count": 5,
"id": 489,
"file": "Untitled-1",
"bounds": {
"top": 0,
"left": 0,
"bottom": 938,
"right": 2396
},
"selection": [ 1 ],
"resolution": 144,
"globalLight": {
"angle": 90,
"altitude": 30
},
"generatorSettings": false,
"profile": "Display",
Adobe Generator 316

"mode": "RGBColor",
"depth": 8,
"layers": [
{
"id": 2,
"index": 1,
"type": "layer",
"name": "Layer 1",
"bounds": {
"top": 0,
"left": 0,
"bottom": 938,
"right": 2396
},
"visible": true,
"clipped": false,
"pixels": true,
"generatorSettings": false
}
]
}

If you trace the getDocumentInfo() call in the Generator Core source code, you’ll find out that
it ultimately runs some ActionManager code, i.e. it calls executeAction() with the stringID
"sendDocumentInfoToNetworkClient".

Adobe Generator exploits the Connection SDK to send ExtendScript commands to Photo-
shop via Socket. A list of these Photoshop Kevlar API Additions for Generator is found
in the Generator Core Wiki.

The plug-in lines of code that I haven’t reviewed here (e.g., the handlers), are mostly logging plus
some utilities, that you can check yourself. To recap:

• A Generator plugin is a Node.js module that sits in a dedicated folder, and has a package.json
file specifying the Generator version it targets, plus its .js entry point.
• The Getting Started plug-in we’ve seen is made of an IIFE that exposes an init() function,
that in turn:
– adds a new menu item in the File > Generate submenu;
– listens for the click event of such menu item;
– executes a initLater() function on the nextTick(), which does three things:
* adds three more Event listeners;
* sends ExtendScript code for evaluation to the JSX engine via evaluateJSXString();
* calls requestEntireDocument() via Generator’s own getDocumentInfo(), that is a
wrapper on Photoshop Kevlar API additions, i.e. dedicated ActionManager code.
Adobe Generator 317

Document Info Options

As a reference, please find below the list of the available, optional flags that you can request
Document Information with.
Param [Type] Description
documentId [integer] Optional document ID
flags [Object] Optional override of default flags for document info request.
The optional flags and their default values are:
flags.compInfo [boolean] True.
flags.imageInfo [boolean] True.
flags.layerInfo [boolean] True. Specifies which info to send (image-specific,
layer-specific, comp-specific). If none of these is specified, all
three default to true, otherwise it just returns the true values
flags.expandSmartObjects [boolean] False. Recurse into smart object (placed) documents
flags.getTextStyles [boolean] True. Get limited text/style info for text layers. Returned in the
“text” property of layer info
flags.getFullTextStyles [boolean] False. Get all text/style info for text layers. Returned in the
“text” property of layer info, can be rather verbose
flags.selectedLayers [boolean] False. If true, only return details on the layers that the user has
selected. If false, all layers are returned
flags.getCompLayerSettings [boolean] True. If true, send actual layer settings in comps (not just the
comp ids, useVisibility, usePosition, and useAppearance)
flags.getDefaultLayerFX [boolean] False. If true, send all fx settings for enabled fx, even if they
match the defaults. If false, layer fx settings will only be sent if
they are different from default settings.
flags.getPathData [boolean] False. If true, shape layers will include detailed path data (in
the same format as generator.getLayerShape)

10.2 Network Events


The Getting Started plug-in has attached some event listeners via onPhotoshopEvent(), e.g.

_generator.onPhotoshopEvent("imageChanged", handleImageChanged);
_generator.onPhotoshopEvent("toolChanged", handleToolChanged);
// ...

You may wonder what the other available so-called Network Events that you can listen for are – see
the list below.
Adobe Generator 318

Network Event String Event Description


"documentChanged" A document becomes dirty⁶
"currentDocumentChanged" A different document becomes active
"closedDocument" A document is closed
"imageChanged" Any changes to image documents
"toolChanged" A new tool is selected
"generatorMenuChanged" Selection of Generator menu items
"foregroundColorChanged" App foreground color changes
"backgroundColorChanged" App Background color changes
"activeViewChanged" A different view becomes active
"newDocumentViewCreated" A new view is created
"colorSettingsChanged" Color management preferences changes
"keyboardShortcutsChanged" Keyboard shortcut preferences changes
"quickMaskStateChanged" Quick mask mode is entered/exited
"workspaceChanged" A new workspace is selected
"generatorDocActivated" Any activate of documents with generator metadata

Each event may bring its own payload to the callback, please refer to the documentation for details.
In order to remove the Event listener, the syntax is different:

// Add Event Listener


_generator.onPhotoshopEvent("toolChanged", handleToolChanged);
// Remove Event Listener
_generator.removePhotoshopEventListener("toolChanged", handleToolChanged);

Please note that listening to some these events (e.g. "imageChanged") can be expensive, so you should
consider the performance implications before deciding to do so.

10.3 Debugging
To debug your Generator plugin, you can connect to it a Chrome Developer Tool session: you must
run the node executable with a --inspect flag.

node --inspect app -f ../plugins -v

Mind the order of the flags: --inspect comes right after node, -f must be followed by the plugins
folder path (relative to the current position), and -v stands for “verbose”. Now open Google Chrome
and point it to chrome://inspect/ (it will automatically add #devices in the URL).
⁶I.e., it has been edited since the last save.
Adobe Generator 319

Then click the inspect link in the Target section, and the Chrome DevTool will open:

If you’re not familiar with using Chrome DevTools (e.g., from a previous CEP development
experience), please refer to the official documentation. Adobe Generator’s Logs are found in the
following folders

• Mac: /Users/<you>/Library/Logs/Adobe/Adobe Photoshop CC 2018/Generator


• Win: C:\Users\<you>\AppData\Roaming\Adobe\Adobe Photoshop CC 2017\Generator\logs

10.4 Generator to JSX Communication


We’ve seen that it’s possible to run an arbitrary amount of ExtendScript code from Generator via
evaluateJSXString() – wrapped with a utility function.
Adobe Generator 320

126 function sendJavascript(str){


127 _generator.evaluateJSXString(str).then(
128 function(result){
129 console.log(result);
130 },
131 function(err){
132 console.log(err);
133 });
134 }

It turns out that you can also get some data back from JSX to Generator, which can be quite useful.
Data is returned only as a String, though. Let’s try to understand how it works, using as an example
these two lines of code:

app.preferences.rulerUnits = Units.PIXELS;
app.activeDocument.width;

If you run this in ESTK, you’ll get in the console whatever the current document’s width is, in pixel
units. That is to say, outside of a function, the last statement is the return value⁷.
In case you want both width and height back, an ugly hack is to combine them into a string:

app.preferences.rulerUnits = Units.PIXELS;
app.activeDocument.width + "," + app.activeDocument.height;
// Result: 3000 px,2000 px

You can use the same principle to get data back from from ExtendScript within a Generator plugin:

1 function getActiveDocumentSize(){
2 // Same lines, wrapped in single quotes (mind the use of double quotes for the com\
3 ma)
4 var str = 'app.preferences.rulerUnits = Units.PIXELS;'+
5 'app.activeDocument.width + "," +app.activeDocument.height;';
6
7 _generator.evaluateJSXString(str).then(
8 function(result){
9 // splitting the string into an array
10 var obj = result.split(",");
11 var width = parseInt(obj[0]);
12 var height = parseInt(obj[1]);
13
⁷If you wrote var w = app.activeDocument.width as the last line, you wouldn’t get the width returned in the Console, though.
Adobe Generator 321

14 console.log("width: " + width + ", height: " + height);


15 },
16 function(err){
17 console.log(err);
18 });
19 }

In the .then() function, the result (a string) is split into an array for convenience, and then used for
the logging. To test it, add getActiveDocumentSize() within the initLater() function, and check
the Chrome console. While this approach works, you’re not expected to combine and run all the
ExtendScript code as a long string: as usual, you’re allowed to read existing .jsx files.

Using external .jsx files

The Generator API exposes an evaluateJSXFileSharedSafe() method, that accepts a path and loads
and executes an existing .jsx file – it’s not documented in the Generator Wiki page AFAIK, but if
you dig into the generator.js code around line 409, you’ll find that it is a promise-friendly way to
do just that.

var path = require("path");


var jsonFilePath = path.join( dirname, "jsx", "json.jsx");
_generator.evaluateJSXFileSharedSafe(jsonFilePath);

In this case, I plan to make use of JSON in the ExtendScript side, hence I need to evaluate the
json.jsx file which sits in the jsx folder. If you were using a relative path straight away, such
as ./jsx/json.jsx it would have failed: the reason seems to be that ./ is the Generator Core root,
not the Plugin’s. Above I’ve built the path via Node’s "path" utility. I’ve not been able to make
use of #include directives within .jsx files, unless you build them with absolute paths, hence the
convenience of using the evaluateJSXFileSharedSafe() function.
Speaking of which, it also accepts an optional second parameter: one object that groups all the
parameters that you need to pass to the ExtendScript code. In fact, if you think about it, in the
simplest case a .jsx file is just a handy way to keep the code in separate modules; but what if you
need to execute functions, passing also parameters? In this scenario, each file can be handed params
via evaluateJSXFileSharedSafe(), and accessed (in the .jsx) via the params variable. For instance,
if in your Generator plugin you have this line:

var greetFilePath = path.join( dirname, "jsx", "greet.jsx");


_generator.evaluateJSXFileSharedSafe(greetFilePath, { message: "Ciao" });

And greet.jsx file contains this code:


Adobe Generator 322

var mess = (params.message != "") ? params.message : "Hello World!";


alert(mess);

Then you’re going to get an Alert box popping up in Photoshop saying "Ciao". Mind you, in the
.jsx file you must refer to the params object, not anything with a different name – e.g., if it were
param.message, it would have returned an “Unknown JavaScript error”.

In case you’re wondering how I get to this, in the generator.js is defined an alert() function:

_generator.alert("A Message from Generator");

That line fires an ExtendScript popup in Photoshop. If you look at the code that is used in
generator.js to construct the Alert function:

443 /**
444 * Simple window alert
445 *
446 * @param {string} message
447 * @param {string} stringReplacements
448 */
449 Generator.prototype.alert = function (message, stringReplacements) {
450 this.evaluateJSXFileSharedSafe("./jsx/alert.jsx", { message: message, replacemen\
451 ts: stringReplacements });
452 };

It turns out that it uses the very mechanism I’ve described above – it hands the alert.jsx file an
object parameter, which is then referred to as params. I’m afraid that reading the source code still
proves to be important.

10.5 Bitmaps and Pixmaps


Since Adobe Generator has been introduced in Photoshop for automatic asset exportation, let’s now
look at how it deals with images. First, at the very core, there’s ImageMagick, an open-source set
of utilities that is available either as a multi-platform command line executable or as a library in a
variety of programming languages.
The Assets Generation feature is implemented in Photoshop as a Generator plug-in, and quite a
complex one. While I won’t analyze it here – if you’re interested, the source code is freely available
in this repository – I will discuss its two main features: getting data from a Photoshop document
and saving it on disk.
You’re allowed to extract Bitmap data as a Pixmap, a particular object that has several properties
such as width, height, channelCount, bitsPerChannel among the others, and of course the actual
Adobe Generator 323

raw data stored as a buffer in the pixels property. Generator provides you with dedicated API to
get pixels, either getPixmap() and getDocumentPixmap() functions, the difference between which
will be clear to you in a moment. An example implementation in a demo Generator plug-in could
be as follows:

1 (function () {
2 "use strict";
3
4 var PLUGIN_ID = require("./package.json").name,
5 MENU_ID = "generator-bitmap",
6 MENU_LABEL = "Generator Bitmap";
7
8 var _generator = null,
9 _currentDocumentId = null,
10 _config = null;
11
12 function init(generator, config) {
13 _generator = generator;
14 _config = config;
15
16 // ...
17
18 function initLater() {
19 _generator.getDocumentInfo(undefined, {
20 compInfo: false,
21 imageInfo: true,
22 layerInfo: true,
23 expandSmartObjects: false,
24 getTextStyles: false,
25 selectedLayers: false,
26 getCompSettings: false
27 }).then(function(document) {
28 // console.log(document)
29 getImageData(document);
30 })
31 }
32
33 process.nextTick(initLater);
34 }
35
36 function getImageData(document){
37 var _document = document;
38 console.log("Document ID: " + _document.id} +
Adobe Generator 324

39 "Layer ID:" + _document.layers[0].id);


40 _generator.getPixmap(_document.id,_document.layers[0].id,{})
41 .then(
42 function(pixmap){
43 console.log(pixmap)
44 // ...
45 },
46 function(err){
47 console.error("err pixmap:",err);
48 }).done();
49 }
50
51 exports.init = init;
52
53 }());

The structure is similar to what we’ve used so far. The core function is getImageData() on line
36, which requires as the parameter the Photoshop document we want to use as the source. For
this reason, I’ve not called it directly; instead, I’ve chained it after getDocumentInfo() (line 20),
within initLater(). Please note that getDocumentInfo() accepts two optional parameters: the first
one is the Document ID (here undefined, i.e. it will use the currently active document), the second
one is an object that will override the default flags for the Document Info request. I have set to
false the ones that are not useful here, and would only slow down the process. I’ve then passed
the returned document as a parameter to the function inside the .then() call (line 28), that finally
reaches getImageData() (line 30).
The getImageData() body (lines 37-50) contains the call to Generator’s getPixmap(), which is the
main function of interest here. It is passed three parameters: the Document ID, the Layer ID (here,
the topmost layer), and an object (here empty⁸) with params to request the pixmap. Alternatively,
you may want to use getDocumentPixmap(), which, according to the source code, gets “a pixmap
representing the pixels of a document in the same layer visibility state that is currently presented
in Photoshop”. In this case, you can skip the Layer ID and pass only the Document ID plus the
parameters object. In the code above I’ve just logged the pixmap, which produces the following
result:

⁸If you don’t need to specify any setting, you must supply an empty object {}.
Adobe Generator 325

Pixmap {
format: 2,
width: 2880,
height: 1754,
rowBytes: 11520,
colorMode: 1,
channelCount: 4,
bitsPerChannel: 8,
pixels: <Buffer ff 00 00 00 ff 00 00 00 ff 00 00 00 ff 00 00 00 ... >,
bytesPerPixel: 4,
padding: 0,
readChannel: [Function],
getPixel: [Function],
bounds: { top: 0, left: 0, bottom: 1754, right: 2880 }
}

I’ve run this on an image 2880 by 1754 pixels; the channelCount is 4 because it counts RGB plus Alpha
(the opacity); rowBytes turns out to be the width times channelCount; colorMode appears to be always
1, no matter whether you run it against RGB, Grayscale, Lab or CMYK images; bitsPerChannel is
apparently always 8, and bytesPerPixel always 4, while bounds take into account layers that may
extend farther than the document itself.
But the real deal here is the pixels property, that comes as a buffer of hexadecimal values. You must
group them by four: but as opposed to the usual RGBA, each pixel is encoded as ARGB (i.e., the
opacity comes first); hence, ff 00 00 00 means a fully opaque (ff is equal to 255) black pixel, i.e.
RGB of 0,0,0. If, for whatever reason, you need to deal with RGBA, you must swap values, as in the
following snippet that uses the ES6 destructuring assignment syntax⁹.

40 // ...
41 .then(
42 function(pixmap){
43 console.log(pixmap);
44 console.log("Swapping pixels...")
45 // cloning the pixels into a new Buffer
46 var rgba = Buffer.from(pixmap.pixels);
47 for (let i = 0; i < rgba.length; i+= pixmap.channelCount) {
48 [ rgba[i], rgba[i+1], rgba[i+2], rgba[i+3] ] =
49 [ rgba[i+1], rgba[i+2], rgba[i+3], rgba[i] ]
50 }
51 },
52 function(err){
⁹Because Generator relies on a modern Node.js instance we can use ECMAScript 6 features (fat arrow, destructuring assignment, etc.). In
contrast, ExtendScript supports only the ES3 implementation and its older syntax.
Adobe Generator 326

53 console.error("err pixmap:",err);
54 }).done();

If you feel inclined, you can also build a Pixmap from scratch. The specs for the right kind
of Buffer to provide are found in this file of the generator-core repository, where the Pixmap
class is defined as follows:

27 function Pixmap(buffer) {
28 if (!(this instanceof Pixmap)) {
29 return new Pixmap(buffer);
30 }
31 this.format = buffer.readUInt8(0);
32 this.width = buffer.readUInt32BE(1);
33 this.height = buffer.readUInt32BE(5);
34 this.rowBytes = buffer.readUInt32BE(9);
35 this.colorMode = buffer.readUInt8(13);
36 this.channelCount = buffer.readUInt8(14);
37 this.bitsPerChannel = buffer.readUInt8(15);
38 this.pixels = buffer.slice(16,
39 16 + this.width * this.height * this.channelCount);
40 this.bytesPerPixel = this.bitsPerChannel / 8 * this.channelCount;
41 this.padding = this.rowBytes - this.width * this.channelCount;
42 this.readChannel = this.getReadChannel(this.bitsPerChannel);
43
44 this._initGetPixelMethod(this.channelCount);
45 }

I’ve spent some extra time showing you how to reverse ARGB values because most of the Node.js
libraries used to write an image file on disk (e.g. pngjs) require such RGBA arrangement. There’s
no need to bother with them except if you need some specific file format because you can directly
write a Pixmap to disk via Generator’s savePixmap().
This function accepts as parameters the Pixmap source, a path to the destination file, and a setting
object for the image file (e.g. format, quality, ppi, etc.)
Adobe Generator 327

35 //...
36 function getImageData(document){
37 // used to find the cross-platform User's Home Folder
38 var path = require("path");
39 var homeFolder = process.env.HOME ||
40 process.env.HOMEPATH ||
41 process.env.USERPROFILE;
42 var _document = document;
43 _generator.getPixmap(_document.id,_document.layers[0].id,{})
44 .then(
45 function(pixmap){
46 // ...
47 _generator.savePixmap(pixmap,
48 path.join(homeFolder, 'generated.jpg'),
49 { format:"jpg", quality:100, ppi:72 });
50 console.log("Saved a JPG");
51 },
52 function(err){
53 console.error("err pixmap:",err);
54 }).done();
55 }

The formats you can use are "jpg", "png", "gif", "svg", "webp". Quality is in the range [1, 100]
for "jpg" and "webp", while accepted values for "png" are either 8, 24 or 32.
Alas, as I am writing this Chapter, there’s no setPixmap() function that can fill a Photoshop
Document’s layer with programmatically created Pixmap data, only a feature request of yours truly.
Please go vote it¹⁰: being able to write on a Layer would be amazing.
Please note that, even if so far I’ve used the initLater() function as the trigger for the features I’ve
demonstrated, nothing prevents you from using the menu click event handler.

Pixmap Options

When the Adobe Generator Wiki pages on GitHub fail to cover some features, the only way to
know what to do is to look at the source code. Here are the available options for getting and saving
Pixmaps.

generator.getPixmap(documentId, layerSpec, settings)

Get a pixmap representing the pixels of a layer, or just the bounds of that pixmap. The pixmap can
be scaled either by providing a horizontal and vertical scaling factor scaleX/scaleY) or by providing
¹⁰You can add your vote clicking on the thumb up emoji beneath my entry.
Adobe Generator 328

a mapping between an input rectangle and an output rectangle. The input rectangle is specified in
document coordinates and should encompass the whole layer. The output rectangle should be of the
target size.
Returns: Promise that resolves with a pixmap of a layer.

Param [Type] Description


documentId [number] Document ID
layerSpec [number/Object] Either the layer ID of the desired layer as a number,
or an object of the form {firstLayerIndex:
number, lastLayerIndex: number, hidden:
Array.<number>} specifying the desired index range,
inclusive, and (optionally) an array of indices to
hide. Note that the number form takes a layer ID,
not a layer index.
settings [Object] An object with params to request the pixmap.
settings.boundsOnly [boolean] Whether to return an object with bounds rather
than the pixmap. The returned object will have the
format (but with different numbers): { bounds:
{top: 0, left: 0, bottom: 100, right: 100 } }.
settings.inputRect [Object] Rectangular part of the document to use (usually the
layer’s bounds).
settings.outputRect [Object] Rectangle into which the layer should fit.
settings.scaleX [float] The factor by which to scale the image horizontally
(1.0 for 100%).
settings.scaleY [float] The factor by which to scale the image vertically
(1.0 for 100%).
settings.inputRect.left [float] Pixel distance of the rect’s left side from the doc’s
left side.
settings.inputRect.top [float] Pixel distance of the rect’s top from the doc’s top.
settings.inputRect.right [float] Pixel distance of the rect’s right side from the doc’s
left side.
settings.inputRect.bottom [float] Pixel distance of the rect’s bottom from the doc’s
top.
settings.outputRect.left [float] Pixel distance of the rect’s left side from the doc’s
left side.
settings.outputRect.top [float] Pixel distance of the rect’s top from the doc’s top.
settings.outputRect.right [float] Pixel distance of the rect’s right side from the doc’s
left side.
settings.outputRect.bottom [float] Pixel distance of the rect’s bottom from the doc’s
top.
settings.ClipBounds.left [float] Pixel distance of the rect’s left side from the layers’s
left side.
settings.ClipBounds.top [float] Pixel distance of the rect’s top from the layers’s top.
settings.ClipBounds.right [float] Pixel distance of the rect’s right side from the
layers’s left side.
settings.ClipBounds.bottom [float] Pixel distance of the rect’s bottom from the layers’s
top.
settings.useJPGEncoding [string] Use alternate Huffman encoding, either optimal or
precomputed.
Adobe Generator 329

Param [Type] Description


settings.useSmartScaling [boolean] Use Photoshop’s “smart” scaling to scale layer,
which (confusingly) means that stroke effects (e.g.,
rounded rect corners) are not scaled. (Default: false).
settings.includeAncestorMasks [boolean] Cause exported layer to be clipped by any ancestor
masks that are visible (Default: false).
settings.convertToWorkingRGBProfile: [boolean] If true, performs a color conversion on the pixels
before they are sent to generator. The color is
converted to the working RGB profile (specified for
the document in PS). By default (when this setting is
false), the “raw” RGB data is sent, which is what is
usually desired. (Default: false).
settings.useICCProfile [string] String with the ICC color profile to use. If set this
overrides the convertToWorkingRGBProfile flag. A
common value is "sRGB IEC61966-2.1". (Default:
"").
settings.getICCProfileData [boolean] If true then the final ICC profile for the image is
included along with the returned pixamp (added
after PS 16.1).
settings.allowDither [boolean] controls whether any dithering could possibly
happen in the color conversion to 8-bit RGB. If false,
then dithering will definitely not occur, regardless
of either the value of useColorSettingsDither and
the color settings in Photoshop. (Default: false).
settings.useColorSettingsDither [boolean] If settings.allowDither is true, then this controls
whether to (if true) defer to the user’s color settings
in PS, or (if false) to force dither in any case where a
conversion to 8-bit RGB would otherwise be lossy. If
allowDither is false, then the value of this parameter
is ignored. (Default: false).
settings.interpolationType [string] Force pixmap scaling to use the given interpolation
method. If defined, the value should be one of the
Generator.prototype.INTERPOLATION constants.
Otherwise, Photoshop’s default interpolation type
(as specified in Preferences > Image Interpolation) is
used. (Default: undefined).
settings.forceSmartPSDPixelScaling [boolean] If true, forces PSD Smart objects to be scaled
completely in pixel space (as opposed to scaling
vectors, text, etc. in a smoother fashion.) In PS 15.0
and earlier pixel space scaling was the only option.
So, setting this to “true” will replicate old behavior
(Default: false)).
Adobe Generator 330

Param [Type] Description


settings.clipToDocumentBounds [boolean] If true, crops returned pixels to the document
bounds. By default, all pixels for the specified layers
are returned, even if they lie outside the document
bounds (e.g., if the document was cropped without
“Delete Cropped Pixels” checked). Note that this
option cannot be used with an inputRect/outputRect
scaling. If inputRect/outputRect is set, this setting
will be ignored, and the pixels will not be cropped to
document bounds. (Default: false).
settings.maxDimension [number] This is the maximal dimension of pixmap that can
be returned by Photoshop (same for both axis).
Raise this value if you need to work with bigger
images. (Default: 10000).
settings.compId [number] Layer comp ID (exclusive of settings.compIndex).
settings.compIndex [number] Layer comp index (exclusive of settings.compId).

generator.getDocumentPixmap(documentId, [settings])

Get a pixmap representing the pixels of a document in the same layer visibility state that is currently
presented in Photoshop. Optionally pass settings with the same available params as getPixmap
method.
Returns: Promise that resolves with a pixmap representing the complete document.

Param [Type] Description


documentId [number] Document ID.
settings [Object] getPixmap settings.

generator.savePixmap(pixmap, path, settings)

Returns: Promise that resolves to the path of the file after the write is complete and the file stream
is closed.
Param [Type] Description
pixmap [Pixmap] An object representing the layer’s image.
pixmap.width [integer] The width of the image.
pixmap.height [integer] The height of the image.
pixmap.pixels [Buffer] A buffer containing the actual pixel data.
pixmap.bitsPerChannel [integer] Bits per channel.
path [String] The path to write to.
settings [Object] An object with settings for converting the image.
settings.format [String] ImageMagick output format.
settings.quality [integer] A number indicating the quality - the meaning depends on the
format.
settings.lossless [boolean] Lossless compression for webp format.
settings.ppi [number] The image’s pixel density.
Adobe Generator 331

Param [Type] Description


settings.padding [Object] Padding, in pixels, to add around the saved image. Should have the
format { top: 0, left: 0, bottom: 0, right: 0 }. Padding will
be transparent (for formats that support transparency) or white.
settings.extract [Object] Extract, coorindates and size to extract from the pixmap. Should
have ths format { x: number, y: number, height: number, width
number }. All numbers should be positive. X and Y can be 0, width
and height cannot.
settings.background] [Array] Background color as RGBA array (default [0,0,0,0.0]) RGB values
are in [0,255] and the A value is in [0,1].
settings._scale [number] A scale factor that causes the image to be resized using convert
(This API should be considered private and may be removed at any
time with only a bump to the “patch” version number of
generator-core. Use at your own risk).
settings.usePngquant [boolean] If true, quantize 8-bit pngs using pngquant instead of convert.
settings.useFlite [boolean] If true, use flite to for image encoding instead of convert.
settings.useJPGEncoding [string] Specify which type of huffman encoding for jpegs.

10.6 Metadata
The Generator API also includes a particular way to store metadata both at the Document and Layer
level. As opposed to XMP, it doesn’t focus on NameSpaces, but it is bound to the Generator Plug-in
ID and allows you to store JSON objects¹¹.

In fact, the API itself doesn’t refer to metadata at all, but, perhaps more appropriately, to
Plug-in Settings: you are supposed to store one key/value pair, where the key is the plug-in
ID, and the value is the JSON Object. It turns out though that the plug-in ID is nothing but
a regular string: theoretically, nothing prevents you from using multiple IDs to store more
than one object, provided that the IDs are reasonably unique not to collide with ones used
by other developers.

Find below a table showing the four setters and getters (two on the Document level, two on the
Layer level), and their respective parameters:

¹¹Nothing prevents you from storing JSON stringified objects in custom XMP NameSpaces too, but XMP was initially meant for a different
purpose.
Adobe Generator 332

Function Parameters
setDocumentSettingsForPlugin() JSON Object
Plug-in ID
getDocumentSettingsForPlugin() Document ID
Plug-in ID
setLayerSettingsForPlugin() JSON Object
Layer ID
Plug-in ID
getLayerSettingsForPlugin() Document ID
Layer ID
Plug-in ID

Please note that the setters only work upon the currently active Document/Layer, whereas the getters
can extract data also from other Documents/Layers.
I’ve built a very simple plug-in to show how this works: it creates a dummy payload with a nested
object and a timestamp, which can be attached to the current Document or current Layer, and then
read back.
Unlike previous examples, this plugin creates multiple menu entries using different IDs – which is
entirely possible.

Let’s look at the implementation:


Adobe Generator 333

1 (function () {
2 "use strict";
3
4 // Plugin metadata
5 const pluginMetadata = require("./package.json");
6 const PLUGIN_ID = pluginMetadata.name;
7
8 // Dummy payload
9 var payload = {
10 "plugin-id": PLUGIN_ID,
11 "timestamp": undefined,
12 "payload": {
13 "username": "unDavide",
14 "password": "ocaMorta"
15 }
16 }
17
18 var _generator = null;
19
20 function init(generator, config) {
21 _generator = generator;
22 _generator.addMenuItem("set-document-metadata",
23 "SET Document Metadata", true, false);
24 _generator.addMenuItem("get-document-metadata",
25 "GET Document Metadata", true, false);
26 _generator.addMenuItem("set-layer-metadata",
27 "SET Layer Metadata", true, false);
28 _generator.addMenuItem("get-layer-metadata",
29 "GET Layer Metadata", true, false);
30 _generator.onPhotoshopEvent("generatorMenuChanged",
31 handleGeneratorMenuClicked);
32 }

I’ve created the four menu instances, which trigger the same click handler.
Adobe Generator 334

78 function handleGeneratorMenuClicked(event) {
79 var menuName = event.generatorMenuChanged.name;
80 switch (menuName) {
81 case "set-document-metadata":
82 setDocumentMetadata();
83 break;
84 case "get-document-metadata":
85 getDocumentMetadata();
86 break;
87 case "set-layer-metadata":
88 setLayerMetadata();
89 break;
90 case "get-layer-metadata":
91 getLayerMetadata();
92 break;
93 default:
94 break;
95 }
96 }

For your information, the event that is passed to the callback has this simple structure¹²:

{
generatorMenuChanged: { name: 'set-document-metadata' },
timeStamp: 1527618661.351,
count: 7
}

Setting and Getting the Document metadata is performed with these two functions:

34 // Setting a dummy object as the Document Metadata


35 function setDocumentMetadata() {
36 payload.timestamp = new Date().toISOString();
37 console.log("Setting Document Metadata...", payload);
38 _generator.setDocumentSettingsForPlugin(payload, PLUGIN_ID);
39 }
40 // Getting the Document Metadata
41 function getDocumentMetadata() {
42 console.log("Getting Document Metadata...");
43 _generator.evaluateJSXString("app.activeDocument.id")
44 .then(function(documentID) {
¹²I have no idea what count refers to in this context.
Adobe Generator 335

45 return _generator.getDocumentSettingsForPlugin(documentID, PLUGIN_ID);


46 })
47 .then(function(response) {
48 _generator.alert("Document Metadata\n" + JSON.stringify(response));
49 })
50 }

I’ve injected the payload I did build at lines 9-16 with a Date (to differentiate each call).
setDocumentMetadata() is quite straightforward, calling directly the setDocumentSettingsForPlugin()
function (line 38) and passing the two required params. The getter is slightly more verbose, as it
requires an intermediate step to get the needed app.activeDocument.id (line 43): for the simplicity’s
sake, I’m working on the current Document.
Please note that is important to return the getDocumentSettingsForPlugin() call (line 45), in order
to send the response to the following then() – again, to keep it simple I’m just alerting the object
in Photoshop via Generator’s alert() utility.
The Layer’s version of the same code is similar:

51 // Setting a dummy object as the Layer Metadata


52 function setLayerMetadata() {
53 _generator.evaluateJSXString("app.activeDocument.activeLayer.id")
54 .then(function(layerID){
55 payload.timestamp = new Date().toISOString();
56 console.log("Setting Layer Metadata...", payload);
57 _generator.setLayerSettingsForPlugin(payload, layerID, PLUGIN_ID);
58 })
59 }
60 // Getting the Layer Metadata
61 function getLayerMetadata() {
62 console.log("Getting Layer Metadata...");
63 _generator.evaluateJSXString(
64 "app.activeDocument.id + ',' + app.activeDocument.activeLayer.id")
65 .then(function(response){
66 var ids = response.split(","),
67 documentID = ids[0],
68 layerID = ids[1];
69 return _generator.getLayerSettingsForPlugin(documentID,
70 layerID,
71 PLUGIN_ID);
72 })
73 .then(function(response) {
74 _generator.alert("Layer metadata\n" + JSON.stringify(response));
Adobe Generator 336

75 })
76 }

I’ve set the payload with setLayerSettingsForPlugin() on line 57 (make sure you input the
parameters in the correct order). In the getter, I need both the Document and the Layer IDs –
unstylishly returned at lines 63-64 as a string, and then split into an Array as you’ve seen before.
They are used at lines 69-71 in the getLayerSettingsForPlugin() call, which response is then
alerted.

10.7 Connecting to external services


The possibility to get a Pixmap in the background, combined with the Adobe Generator built-in
Node.js capabilities, can extend Photoshop to a great deal. For instance, you can quickly inject
Computer Vision / Artificial Intelligence algorithms in your plug-ins for all sort of purposes.
As an example, I’ve built a demonstrative “Privacy enforcing” Generator plug-in that finds and blurs
faces in a picture.

Artificial Intelligence: Face Detection

The core relies upon an external service, as the AI provider. I’ve decided to use Clarifai, a Computer
Vision company that can perform a variety of tasks on images: from face detection, to object
classification, sensitive content filtering, etc.
They have trained Neural Network to very specific tasks, so you can extract demographic data from
portraits (such as age and gender), recognize food items down to the ingredients level, filter NSFW¹³
¹³Not Safe For Work: nudity, profanity, violence, and everything else you wouldn’t like to be caught looking at while at work.
Adobe Generator 337

content, tag celebrities, deal with wedding related items, etc. From this perspective, I’ve used perhaps
the simplest of the available features: face detection.
To access Clarifai services (and run my plugin), you need to obtain an API key that allows you to
perform the remarkable number of 5000 API requests per month for free. It took me 47 requests in
total to build my demo from scratch in one night, so five thousand will surely accommodate even
the buggiest of the plugins.
Follow the instructions below to set up your account¹⁴.

Clarifai Setup

Browse to this page and create a Free Developer Account. When you’ve entered your data and
confirmed your email as usual, on your Account page you’ll find a pre-built Application; I’ve
renamed mine photoshop-face-detection. Expand the API Keys section and copy the key shown
there (it’s the so-called All Purposes key, a 32 chars string): you’ll need it in the main.js file.

Clarifai uses “Workflows” to specify what your application (using their service) should do: click
“Create Workflow” and set it up according to the following screenshot:

¹⁴Please note that details on setting up a Clarifai account and the face detection application/workflow may vary in the future for reasons I
cannot control. The instruction I’ve provided works at the time of this writing, in mid-2018: their website documentation is excellent though,
and you won’t have problems in finding your way if/when they update the API.
Adobe Generator 338

We’re telling them that we’re interested in the Face Detection feature only (you can disregard the
Version, it’s their internal identification string for the feature). Instead, you’re going to need the ID
(photoshop-face-detection), so write it down. Click the Save Workflow button, and you should be
ready to go.

Plug-in Setup

Since we’ll make use of Clarifai API, you need to cd into the plug-in folder and install their Node.js
module:

1 npm install clarifai --save-dev

If, besides what I’ll cover here, you need further information on the API, you can follow their
excellent documentation.

Plug-in Architecture

Decomposing the feature into smaller tasks, our plug-in will:

1. Create a new item in the Generator submenu.


2. Setup the Clarifai application (keep handy your API Key).
3. Setup the menu click handler so that it starts the entire Face Detection and Blurring routine.

When the user clicks the menu, the main function getAndBlurFaces() will:

1. Retrieve the Document information, needed for the next step.


2. Get the Document Pixmap, scaled to fit 1000px for the wider side.
3. Save a temporary .png file.
4. Read the temporary .png from disk, converting to a Base64 string, and send it to Clarifai.
5. Parse the JSON response from Clarifai: if faces are detected, pass the Array of coordinates to
the ExtendScript engine.
6. In the JSX, create a rectangular selection using the top, bottom, left, right coordinates, and blur
it – repeat for as many face items that have been found.

That’s it. Let’s inspect the entire Generator plug-in code, using the following photo by Emma
Goldsmith as our sample image.
Adobe Generator 339

Generator code

The main.js code starts with the usual IIFE – I’ve used the package.json as the source for the ID
and Label strings:

1 (function () {
2 "use strict";
3
4 // Plugin metadata
5 const pluginMetadata = require("./package.json");
6 const PLUGIN_ID = pluginMetadata.name,
7 MENU_ID = pluginMetadata.name,
8 MENU_LABEL = pluginMetadata.description;
9
10 // Node modules
11 const path = require("path"),
12 fs = require("fs"),
13 os = require("os");
14
15 // Paths for JSX files
16 const jsonFile = path.join( dirname, "jsx", "json.jsx");
17 const blurFile = path.join( dirname, "jsx", "blur.jsx");
18
19 // CLARIFAI https://clarifai.com/
Adobe Generator 340

20 // IMPORTANT! Create a free account and use YOUR API KEY


21 const Clarifai = require('clarifai');
22 const app = new Clarifai.App({
23 apiKey: 'YOURAPIKEYHERE!'
24 });
25
26 // Generator global
27 var _generator = null;

Nothing too fancy so far: I require "path", "fs", and "os" because I will need to read files from
disk. I’ve also defined the paths for .jsx files evaluation (lines 16-17) – I’ll make use of JSON, while
blur.jsx contains the actual Photoshop function that creates and blurs rectangular selections.

31 function init(generator, config) {


32
33 _generator = generator;
34
35 // Evaluate a JSX file
36 _generator.evaluateJSXFileSharedSafe(jsonFile);
37
38 _generator.addMenuItem(MENU_ID, MENU_LABEL, true, false)
39 .then(
40 function () {
41 console.log("Menu created", MENU_ID);
42 }, function () {
43 console.error("Menu creation failed", MENU_ID);
44 }
45 );
46
47 _generator.onPhotoshopEvent("generatorMenuChanged",
48 handleGeneratorMenuClicked);
49
50 }

The init() function is as simple as it gets – I’m only evaluating json.jsx, creating the menu item
as usual, and setting its click handler. No initLater() here, for it’s not needed.
Adobe Generator 341

54 function handleGeneratorMenuClicked(event) {
55 // Ignore changes to other menus
56 var menu = event.generatorMenuChanged;
57 if (!menu || menu.name !== MENU_ID) { return }
58 console.log("\nAbout to detect Faces...")
59 // Run the main Face Detection/Blurring routine
60 getAndBlurFaces();
61 }

The handler logs a message, and then runs the main getAndBlurFaces() routine, which is where
things really happen.

63 // Main function to detect and blur faces


64 function getAndBlurFaces() {
65 // Get the document information first
66 _generator.getDocumentInfo(undefined, {
67 compInfo: false,
68 imageInfo: true,
69 layerInfo: false,
70 expandSmartObjects: false,
71 getTextStyles: false,
72 selectedLayers: false,
73 getCompSettings: false
74 })

getAndBlurFaces() contains a long chain of promises, for we’re mostly dealing with asynchronous
code. At first (line 66) the Document Info must be retrieved, for the document.id is needed as a
parameter to get the Pixmap. Like you’ve seen earlier, I’m setting the input parameters to skip what’s
unnecessary in this context and speed up the process.
Now the chain of .next() call starts.

75 // Get the Document Pixmap


76 .then(function(document) {
77 // console.log(document);
78 var pixmapSettings = {},
79 w = document.bounds.right - document.bounds.left,
80 h = document.bounds.bottom - document.bounds.top,
81 maxDimension = Math.max(w, h);
82 if ( maxDimension > 1000 ) {
83 pixmapSettings.scaleX = pixmapSettings.scaleY = 1000 / maxDimension;
84 }
85 return _generator.getDocumentPixmap(document.id, pixmapSettings);
86 })
Adobe Generator 342

The document (returned from the previous call), is passed as a parameter to the anonymous function
in .next(). Here, I’m using the document.bounds properties to build a pixmapSettings object. To
make things quicker for this demo, I’m not retrieving nor sending to Clarifai the full resolution
image, but a version reduced to 1000px along its longest dimension. For such purpose, I’m using the
scaleX, scaleY properties – find them among the available ones in this table.

Using a reduced version is not going to pose any problem, because Clarifai will respond with
percentage coordinates that apply to the full resolution image without further processing.
I’m used to explicitly return (in this case the getDocumentPixmap() call) at the end of each
anonymous function within .then(), to make sure that the resolved value of the promise gets passed
down the line to the next anonymous function.
Please note that I use getDocumentPixmap() and not getPixmap(); it’s up to you whether to make
the plugin layer-based, for simplicity’s sake I assume a flattened image as the source.

87 // Save a temp PNG of the document


88 .then(function(pixmap) {
89 return _generator.savePixmap(pixmap,
90 path.join(os.tmpdir(), 'generated.png'),
91 { format:"png", quality:8, ppi:72 });
92 })

What is returned to the next function is a Pixmap. Why do I save it to disk, as an intermediate (and
extra) step, instead of sending the pixmap.pixels straight to Clarifai? Of course I tried, to no avail.
The fact is that, even if you process the pixels (swap ARGB to RGBA, and convert the stream to
Base64 as required), an image is something more than a Buffer of values, as you can check yourself
from the Bitmap file format specs. I should have manually created at least the header: in my case –
and, as far as I’ve heard from other people in the automation business, in almost anyone’s case too –
it’s easier and not exceedingly slower to save a temporary file on disk. Here, I’m using os.tmpdir(),
which is the Node’s way to point to the temporary folder.

87 // Read the PNG from disk...


88 .then(function(imagePath) {
89 var binaryImage = fs.readFileSync(imagePath);
90 var base64Image = binaryImage.toString('base64');
91 return app.workflow.predict("photoshop-face-detection",
92 { base64: base64Image })
93 })

Of course, I also need to read back the image. I’m using readFileSync(), the synchronous function,
for I must wait for the image anyway. The binaryImage is then turned to Base64 via toString()
– Node doesn’t have an atob() nor btoa() functions – and the string is passed to the Clarifai’s
app.workflow.predict().
Adobe Generator 343

You’ve already set your login credential (the API key), so here you’re sending the Workflow ID
(in my case "photoshop-face-detection"), and an object with a base64 property. See the “Images”
section “via Bytes” in this page for further details on the API.
Finally, we’re ready to parse the Clarifai response:

103 .then(function(response) {
104 console.log(response);
105
106 // 10000 is the code for OK
107 if (response.status.code !== 10000) {
108 console.log("There's been an error...")
109 return;
110 }
111 console.log("Clarifai response: ", response);
112 // If faces have been detected...
113 if (response.results[0].outputs[0].data.regions != undefined) {
114 // Send the coordinates to Photoshop for further processing (blur)
115 _generator.evaluateJSXFileSharedSafe(blurFile,
116 { clarifai_response:
117 JSON.stringify(response.results[0].outputs[0].data.regions) })
118 } else {
119 _generator.alert("No faces found!")
120 }
121 })

The response that you get back from Clarifai is a quite complex JSON (it needs to accommodate more
than our simple face detection request). This is what it looks like in the Chrome Dev Tool Console:
Adobe Generator 344

As far as I’ve been able to tell, the resulting object contains results, status, and workflow children.
results is an array (in my workflow, with one item only); in turn, its element has a data prop, that
holds a regions array. This is what we’re interested in: it contains as many items as the number of
faces found. In the code, I make sure that the array is not undefined (no faces at all), and I hand it to
the blur.jsx file (for which the path has been composed into the blurFile variable) as a parameter.
As you remember from the generator-jsx example found earlier in this Chapter, to pass parameters
to a .jsx file you need to build a params object. I made it (line 116) with one property only,
clarifai_response, which contains the Array of faces stored in the form of a String I’ve generated
using JSON.stringify(), that I will parse back in the ExtendScript side.

ExtendScript code

Time to look at the blur.jsx file.

1 // parse back into an array the clarifai_response from the params object
2 var faces = JSON.parse(params.clarifai_response)
3
4 // loop through the found faces
5 for (var i = 0; i < faces.length; i++) {
6 // select and blur them
7 selectAndBlur(faces[i].region_info.bounding_box);
8 }
9
10 // Alert a message when the script is done
11 alert("Done!\nFound and blurred " + faces.length + " face" +
12 ((faces.length > 1) ? "s." : "."));
Adobe Generator 345

On line 2 the clarifai_response property from the params object is parsed. The loop (lines 5-8)
through the faces array passes the each region_info.bounding_box object to the selectAndBlur()
function, which is in charge of the actual Photoshop operations.

17 // Select and blur one face. A box is an object with the following props
18 // {
19 // "top_row" :0.3090028,
20 // "left_col" :0.2866688,
21 // "bottom_row":0.61201197,
22 // "right_col" :0.69069064
23 // }
24 function selectAndBlur(box) {
25 app.activeDocument.selection.deselect();
26 selectBox(box);
27 app.activeDocument.activeLayer.applyGaussianBlur(50);
28 app.activeDocument.selection.deselect();
29 }

As you see, the bounding_box has "top_row", "left_col", "bottom_row", and "right_col" prop-
erties, that define as a percentage the distance from the top, left, bottom and right image edges.
selectAndBlur() first deselects everything, then calls selectBox() (an ActionManager-based cus-
tom function that you’ll see in a moment); when the selection is made, a Gaussian Blur filter is
applied. Finally, everything’s deselected.

31 // Make a rectangular selection according to Clarifai response


32 function selectBox(box) {
33 var d1 = new ActionDescriptor();
34 var d2 = new ActionDescriptor();
35 var r = new ActionReference();
36 r.putProperty( s2t("channel"), s2t("selection") );
37 d1.putReference( s2t("target"), r );
38 // Remember to multiply by 100!
39 d2.putUnitDouble( s2t("top"), s2t("percentUnit"), box.top_row * 100 );
40 d2.putUnitDouble( s2t("left"), s2t("percentUnit"), box.left_col * 100 );
41 d2.putUnitDouble( s2t("bottom"), s2t("percentUnit"), box.bottom_row * 100 );
42 d2.putUnitDouble( s2t("right"), s2t("percentUnit"), box.right_col * 100 );
43 d1.putObject( s2t("to"), s2t("rectangle"), d2 );
44 executeAction( s2t("set"), d1, DialogModes.NO );
45 }

selectBox() is nothing but the ScriptListener output wrapped with a function. Please don’t forget to
multiply by 100 the box values, because the percentages from Clarifai are in the range {0,1}, while
Photoshop uses {0,100}.
Adobe Generator 346

This demo plugin just scratches the surface of what you can build by injecting external services such
as Computer Vision and Artificial Intelligence into Photoshop via Generator – the possibilities are
truly endless.

10.8 Socket.io Server/Client communication


Easily building an HTTP Server is one of the things that Node.js was famous for since its early days –
it is indeed simple. With Photoshop Scripting, I can think of different scenarios where Generator can
act either as a Server or as a Client too: for such purposes in the past I’ve successfully used Socket.io,
a JavaScript library that “enables real-time bidirectional event-based communication”. The enjoyable
feature of Socket.io is that it provides the same syntax on both the Server and the Client side, so the
code is easier to write.

Sockets 101

The general idea – bear with me if I’m over-simplifying here – is that you set up one instance
of Socket.io on the Node.js Server and one on the Client. The Server has two purposes: first and
unsurprisingly, it serves HTML pages upon request; second, it listens for Socket.io connections. The
Client, in turn, sends a Socket.io connection request to the Server. When the communication channel
is established, the two¹⁵ can emit messages, and respond accordingly.
I will give you instruction to build local (i.e., on your machine, and not remotely hosted) Node.js
servers, but nothing prevents you from uploading the code on a remote machine and working from
there.
Create an empty folder (you can find the result of this in the provided source code, local-server-example
folder), open the Terminal, cd into it, initialize an empty project and install Socket.io

npm init -y && npm install socket.io --save

Now create an app.js file with this content (straight from the official documentation, very slightly
modified):

¹⁵Socket.io can deal with multiple Clients connected to the same Server of course.
Adobe Generator 347

1 var app = require('http').createServer(handler)


2 var io = require('socket.io')(app);
3 var fs = require('fs');
4
5 app.listen(8099);
6
7 function handler (req, res) {
8 fs.readFile( dirname + '/index.html',
9 function (err, data) {
10 if (err) {
11 res.writeHead(500);
12 return res.end('Error loading index.html');
13 }
14
15 res.writeHead(200);
16 res.end(data);
17 });
18 }
19
20 io.on('connection', function (socket) {
21 socket.emit('news', { hello: 'world' });
22 socket.on('my other event', function (data) {
23 console.log(data);
24 });
25 });

Line 1 creates a vanilla Node.js HTTP server, listening on port 8099 (line 5¹⁶), passing a handler
function to process requests and build responses. The function body is on lines 7-18: it is a very
bare example, serving just the index.html. On line 2 the server is passed to Socket.io, which uses
it on lines 20-25. There, when a client connects, it emits a 'news' message, with a payload object;
besides, it listens for incoming 'my other event', logging their payload on the Console.
Before running the Server, create alongside the app.js an index.html file with the following content:

¹⁶Don’t use the default port 80 or you’ll likely run into an EACCES 0.0.0.0:80 error – the port being already in use.
Adobe Generator 348

1 <!DOCTYPE html>
2 <html lang="en">
3 <head>
4 <meta charset="UTF-8">
5 <title>The Client</title>
6 </head>
7 <body>
8
9 <h2>The Node.js server seems to work!</h2>
10 <p>Look at the Console now.</p>
11
12 <script src="/socket.io/socket.io.js"></script>
13 <script>
14 var socket = io('http://localhost:8099');
15 socket.on('news', function (data) {
16 console.log(data);
17 socket.emit('my other event', {
18 my: 'data'
19 });
20 });
21 </script>
22
23 </body>
24 </html>

The script tags fetch the Client side socket.io.js file¹⁷, and connects to localhost:8099 (the Node.js
server, with its specific port, line 14). It then listens for the 'news' message (line 15), and log the
payload when it is received: only then, it emits a 'my other event' message (line 17), with an object
payload.
At this point, in the same Terminal window, type the following to boot the Server:

node app.js

Then point your browser to http://localhost:8099. You should get the following:
¹⁷You are not supposed to provide the socket.io.js yourself: according to this explanation, the Socket.io server will handle serving the
correct version of the client library in your place.
Adobe Generator 349

The Server has been created, and it’s actively listening for incoming connections. As soon as you
browse to localhost:8099, the connection Client/Server is made, hence the Server fires 'news'.
This 'news' message is received by the Client, which happened to be waiting exactly for that: as a
response, the Client emits 'my other event', that in turn the Server is listening for. In due course,
the payloads are logged in the respective Consoles. Hopefully, it all makes sense.
Given this basic information on Socket.io, let’s now look at two actual examples of use in the context
of Photoshop extensibility. In one case, Adobe Generator will act as a Server, in another as a Client.

Generator as a Server: CEP Panel interaction

Implemented as a solution by many developers in commercial products, Generator on the Server


side is usually combined with CEP Panels – to get the best of both worlds, so to speak. For instance,
you may want to make the most of Generator specific features, such as the completeness with which
it can extract Document Information, or exploit the possibility to background-process bitmap data.
I will demonstrate here a Panel that listens for Layers activation, and loads the current Layer’s
thumbnail as an <img>.
The plan involves Generator as a Socket.io Server, and the CEP Panel as a Client – their interaction
as follows. The Panel is instructed to listen for a Layer Changed Event: when that happens, it emits
a Socket Event of type 'layerChanged'. Generator, that in turn is listening for it, responds saving
the current layer as a Pixmap, and emitting a 'newImage' message at the end of the process. This
message reaches the Panel, that promptly loads the image. The idea is quite straightforward, but the
implementation requires some care.

Please bear with me if I ask you to take my word for some aspects related to CEP Panels,
particularly their Event System. A thorough discussion on them is out of the scope of this
book, and the reason why I’ve dedicated to Panels an entire course. Be pleased to know that
Photoshop HTML Panels Development contains a similar example, but what you’re going
to see in the next pages is content exclusive to this book.

To run the example Panel, as a reminder for CEP instruction given in Chapter 7, the copy the entire
com.example.generator folder either in:
Adobe Generator 350

• Mac: ∼/Library/Application Support/Adobe/CEP/extensions/


• Win: C:\Users\<yourUserName>\AppData\Roaming\Adobe\CEP\extensions\

Also set the Debug Flag on, and restart Photoshop – find the panel under Window > Extensions >
CEP and Generator Example. Start the generator-server plugin as usual.

Let’s look at the CEP Panel first. The HTML is bare to say the least:

1 <body>
2 <div id="content">
3 <div class="row" style="height: 300px;">
4 <h3 style="">Layer Thumb via Generator</h3>
5 <img id="layerThumb" src="img/placeholder.png"></img>
6 <p>Open an image and select different layers...</p>
7 </div>
8 </div>
9 <script src="js/CSInterface.js"></script>
10 <script src="js/themeManager.js"></script>
11 <script src="node_modules/socket.io-client/dist/socket.io.js"></script>
12 <script src="js/main.js"></script>
13 </body>

As you see, it is nothing but an <img> tag with a "layerThumb" ID, referring to a placeholder .png
file. Please note the socket.io.js client belonging to the node_modules folder: socket.io-client is
deployed automatically when you npm install the socket.io package.
The main.js starts with the Socket code outlined above:

5 // Connecting to the Generator Server


6 var generatorClient = io('http://localhost:8099');
7 // On connection
8 generatorClient.on('connect', function() {
9 console.log("Connected successfully to localhost:8099");
10 });
11 // When a new temp PNG has been created
12 generatorClient.on('newImage', function(imagePath) {
13 console.log("Got a new Image Path!", imagePath);
14 document.getElementById("layerThumb").src=imagePath;
15 });
16
17 var csInterface = new CSInterface(),
18 extensionID = csInterface.getExtensionID(),
19 applicationID = csInterface.getApplicationID();
Adobe Generator 351

On line 6 there’s a shortcut to connect to the (Generator) server, which is local and operates on
port 8099: on connection, a message is logged. The only other message that the Panel listens for is
'newImage' (line 12), which carries as a payload the String pointing to the new image to load –
a task accomplished by the one JavaScript line 14. This is definitely the easiest part; lines 17-19
instantiate CSInterface (the central CEP API Class) and storing a couple of useful constants.
The apparent complexity in the Panel’s code that follows is mostly because Generator comes with
no built-in way to listen for a Layer Changed event (review this table to double-check). So, we need
to subcontract the Event Listening to the Panel side; among the plethora of different Event types
that CEP can handle, this particular one goes under the category of the “ExtendScript Events”. Very
(very!) briefly, the main points are outlined below.
JSX Events are the ones that leave a trace in the ScriptListener.log file: you identify them by the
TypeID of the executed Event. In our case, if you select (i.e., make active) one layer, you get more or
less this blob of ActionManager code – which by now you should be familiar with:

var idslct = charIDToTypeID( "slct" );


var desc126 = new ActionDescriptor();
var idnull = charIDToTypeID( "null" );
var ref90 = new ActionReference();
var idLyr = charIDToTypeID( "Lyr " );
ref90.putName( idLyr, "Layer 1 copy" );
desc126.putReference( idnull, ref90 );
var idMkVs = charIDToTypeID( "MkVs" );
desc126.putBoolean( idMkVs, false );
var idLyrI = charIDToTypeID( "LyrI" );
var list68 = new ActionList();
list68.putInteger( 6 );
desc126.putList( idLyrI, list68 );
executeAction( idslct, desc126, DialogModes.NO );

You are interested in the "slct" charID, that is to say "select" in the human-friendlier stringID
syntax. Or better, in its correspondent typeID. Mind you, the typeID can change at runtime, so you
do not want to hardwire 1936483188 because it may point to something different (this has already
been discussed in the ActionManager Chapter).
You manifest interest in a JSX Event by instantiating the CSEvent class, and filling the newly created
instance with meaningful data:
Adobe Generator 352

21 // Define the TypeID for the 'select' event and the 'layer' class
22 csInterface.evalScript("stringIDToTypeID('select')",
23 function(selectID) {
24 // Create the CSEvent for 'select'
25 var event = new CSEvent();
26 event.type = "com.adobe.PhotoshopRegisterEvent";
27 event.scope = "APPLICATION";
28 event.appId = applicationID;
29 event.extensionId = extensionID;
30 // The 'select' Event (its TypeID)
31 event.data = selectID;
32 // Dispatch the Event
33 csInterface.dispatchEvent(event);
34 // Listen and attach a callback
35 csInterface.addEventListener("com.adobe.PhotoshopJSONCallback" + extensionID,
36 PhotoshopCallbackUnique);
37 });

Note that all the code is in the callback function of evalScript() (lines 23-36), that is given the
typeID correspondent to 'select'. Having that crucial bit of information, the event properties can
be filled (take my word for it: the type is "com.adobe.PhotoshopRegisterEvent", and everything
else is required). The data is assigned the typeID, and finally, the CSEvent instance is dispatched
(line 33). Strange as it may sound, the Event Listening architecture is based on the dispatching
of an Event, and the attachment of a callback (here PhotoshopCallbackUnique) to an Event of type
"com.adobe.PhotoshopJSONCallback", combined with the Extension’s ID. That’s the way they made
it.
We’re not done yet, let’s look at the PhotoshopCallbackUnique() body.

39 function PhotoshopCallbackUnique(evt) {
40 var payload = JSON.parse(evt.data.replace(/ver1,/,''));
41 console.log("Entire payload", payload);
42 // If the 'select' event has a layerID property, it means that it is
43 // a 'selection' of a layer, and not, say, of a tool
44 if (payload.eventData.layerID != undefined) {
45 generatorClient.emit('layerChanged', payload.eventData.layerID[0]);
46 }
47 }

The Event that is passed to the callback has a data property that is not an actual JavaScript object (as
one may expect) but a stringified JSON object with the ver1, string prepended: in order to inspect it
(line 40) you need to remove that string and JSON.parse() the result.
Adobe Generator 353

It turns out that the payload Object carries the eventID (here 1936483188, the 'select' typeID), and
an eventData object with some useful information, such as a layerID array: the ids of the selected
Layers¹⁸.
Selecting a Tool (the Hand, the Brush, etc.) also fires a 'select' Event, so I look for the layerID array,
as a way to discriminate the Layer events and respond to them only (line 44). At this point (line
45), we can confidently emit a 'layerChanged' Socket Event in Generator direction; I’m passing the
layerID as a payload, but this is not strictly required, since I can have the same information on the
Generator side as a byproduct of a getDocumentInfo() call.
To sum up the Panel side: it connects to the Socket.io server (on Generator), and listens for the
'select' Event. When one such Events is caught, it filters out unwanted selections such as Tools’,
and emits a 'layerChanged' Socket message. Time to build the Server as a Generator Plugin.
The code below is in the generator-server plugin, that is found in the source code .zip. The first
lines are nothing special, and you’ve seen them over and over again.

¹⁸It’s an Array because nothing prevents you from activating (I’m using selecting quite loosely here as a synonym) multiple Layers.
Adobe Generator 354

1 (function () {
2 "use strict";
3
4 // Plugin metadata
5 const pluginMetadata = require("./package.json");
6 const PLUGIN_ID = pluginMetadata.name;
7 const MAX_THUMB_SIZE = 400;
8
9 const path = require("path"),
10 os = require("os"),
11 fs = require("fs");
12
13 var _generator = null,
14 _documentID = null,
15 _layerID = null;
16
17 function init(generator, config) {
18
19 _generator = generator;
20 _generator.addMenuItem(PLUGIN_ID, "Socket.io Server", true, false);
21 _generator.onPhotoshopEvent("generatorMenuChanged",
22 handleGeneratorMenuClicked);

The MAX_THUMB_SIZE constant is used as a threshold for the maximum dimension in pixels for the
extracted Pixmap. Still in the init() function, we can create the Socket.io server (line 24 in the
following snippet), listening for incoming connections on port 8099.

24 // Shortcut to create a server


25 var io = require('socket.io')(8099);
26 // Respond to connection
27 io.on('connection', function (socket) {
28
29 socket.on('layerChanged', function (layerID) {
30 console.log('received Layer ID:', layerID);
31 _layerID = layerID;
32 // Get the current DocumentID
33 _generator.getDocumentInfo(undefined, {
34 compInfo: false,
35 imageInfo: false,
36 layerInfo: false,
37 expandSmartObjects: false,
38 getTextStyles: false,
39 selectedLayers: true, // selected layer only!
Adobe Generator 355

40 getCompSettings: false
41 })

When the connection with the Client – the CEP Panel – is made (line 27), the callback function
is passed a socket object, which can listen (via the .on() method, line 29) for messages emitted
by the Client. When 'layerChanged' is received, a long chain of events is executed. First, we
getDocumentInfo(), with layer data only for the selected one (line 39). Then…

42 // Get the Document Pixmap


43 .then(function(document) {
44 //console.log(document);
45 // Reducing the dimension to MAX_THUMB_SIZE
46 var pixmapSettings = {},
47 lay = document.layers[0],
48 w = lay.bounds.right - lay.bounds.left,
49 h = lay.bounds.bottom - lay.bounds.top,
50 maxDimension = Math.max(w, h);
51 if ( maxDimension > MAX_THUMB_SIZE ) {
52 pixmapSettings.scaleX =
53 pixmapSettings.scaleY = MAX_THUMB_SIZE / maxDimension;
54 }
55 return _generator.getPixmap(document.id, lay.id, pixmapSettings);
56 })

Having the document passed, we can do the same trick you’ve seen in the generator-bitmaps plugin,
to limit the size of the extracted Pixmap via pixmapSettings (lines 46-54). Then, we can return
the extracted Pixmap obtained via getPixmap(), passing the Document ID, the Layer ID, and the
extraction parameters. Then…

57 // Save a temp PNG on disk


58 .then(function(pixmap) {
59 var savePath = path.join(os.tmpdir(),
60 'generated_' +
61 new Date().getTime() +
62 '.png'),
63 saveOptions = { format:"png", quality:8, ppi:72 };
64 return _generator.savePixmap(pixmap, savePath, saveOptions);
65 })

We can save a temporary .png on disk, appending a new date in the filename to ensure that the file
is reloaded correctly by the panel. Then…
Adobe Generator 356

66 .then(function(imagePath) {
67 console.log("Yuppidoo, it works", imagePath);
68 socket.emit('newImage', imagePath);
69 })

As a result of the Pixmap saving process, we’re returned the image path: which in turn we pass on
to the final then() callback, so that we can emit the 'newImage' message to the CEP Client handing
it the path as a payload.
To test it, make sure the Generator Plug-in is running. Start Photoshop, start the Panel, and open a
multi-layered picture: switching from Layer to Layer, the Panel is going to display a 200px thumbnail
of the active Layer – if you allow me, it’s pretty neat!

Generator as a Client: Photoshop remote control

In this second example, I’d like to build a Server able to remote control Photoshop. Sort of a CEP
Panel if you will, but instead to have it within Photoshop, it’s going to be hosted on a remote machine
and accessed through a web browser. It’ll be able to drive a Photoshop installation no matter whether
on your machine, or somebody else’s.
Adobe Generator 357

There are few variations on this theme that we may build. The complete picture involves three
players: one Server, and two Clients. Among the Clients, one machine runs Photoshop and an Adobe
Generator plugin, and one separate machine runs a web browser pointing to an HTML page on the
Server and driving the other’s Photoshop. The Server accepts Socket.io connections from the two
Clients – when the connection is made, they will be able to emit and listen for messages and respond
accordingly.
In fact, I will demonstrate a more straightforward (but technically equivalent) setup, where the three
players still exist as separate entities but sit on the same machine: one Node.js/Socket.io Server,
one Photoshop/Generator Client, one web browser. This is going to be more practical to build and
test, and still represents valid code that would make possible for a remote Client to drive another’s
Photoshop – provided that the Server code is uploaded to a remote machine.
If you’re willing to lose the possibility to grant access to your Photoshop to a remote Client, but
keep the browser controlling the program from within the same machine, matters can be simplified
further: Generator itself can act as a Server, and hence dialog directly with the browser via Socket.io
messages. I won’t build this latest variant, but as soon as you get to the end of this section, you won’t
have any trouble doing it yourself.
Adobe Generator 358

The result of our work will be this:

Let’s start with the Browser Client first, which represents the View that the user will deal with. It is
a pretty standard HTML page:
Client side code in index.html

1 <!DOCTYPE html>
2 <html>
3
4 <head>
5 <meta charset="utf-8" />
6 <meta http-equiv="X-UA-Compatible" content="IE=edge">
7 <title>Photoshop HTML Panel</title>
8 <meta name="viewport" content="width=device-width, initial-scale=1">
Adobe Generator 359

9 <link rel="stylesheet" type="text/css" media="screen"


10 href="../css/topcoat-desktop-dark.css" />
11 <link rel="stylesheet" type="text/css" media="screen"
12 href="../css/style.css" />
13 </head>
14
15 <body>
16 <div class="container">
17 <h1>Photoshop HTML Panel</h1>
18 <div class="row">
19 <h3 class="step">Run commands</h3>
20 <p>Send ExtendScript code hosted on the server</p>
21 <button id="newDocument" class="topcoat-button--large--cta" >
22 Create new Document
23 </button>
24 </div>
25 <div class="row">
26 <h3 class="step">Run arbitrary code</h3>
27 <p>Type some code in the text area below...</p>
28 <textarea id="clientCode"class="topcoat-textarea"
29 rows="6" cols="36" placeholder="alert('Woohoo!');">
30 </textarea>
31 <div>
32 <button id="runCode" class="topcoat-button--large--cta run">
33 ... and Run it!
34 </button>
35 </div>
36 </div>
37 </div>
38
39 <!-- Client side version of Socket.io -->
40 <script src="../node_modules/socket.io-client/dist/socket.io.js"></script>
41 <!-- JavaScript logic for the Panel -->
42 <script src="../js/main.js"></script>
43
44 </body>
45 </html>

I’ve even used Topcoat for the .css (the same stylesheets I use for CEP Panels). As you see, it contains
one “Create new Document” button, and a large <textarea>; the script tags link the socket.io.js
Client library and a main.js that we’re about to inspect.
Adobe Generator 360

Client Side code in main.js

1 // Connect to the Socket server


2 var socket = io('http://127.0.0.1:8099/');
3
4 // Triggers an evaluation of JSX code that belongs to the Server
5 document.getElementById("newDocument").onclick = function() {
6 console.log("New Document Button pressed...")
7 // no payload is necessary here
8 socket.emit('newDocumentFromBrowser');
9 };
10
11 document.getElementById("runCode").onclick = function() {
12 console.log("Run Button pressed...")
13 var jsxCode = document.getElementById("clientCode").value;
14 socket.emit('clientEvalJSX', jsxCode);
15 };

At first, it connects to the Socket Server on port 8099: the Server side is local, for convenience reasons
discussed earlier, hence the address. There are two onclick functions, bound to the Button’s ids: the
'newDocument' emits a 'newDocumentFromBrowser' message (line 8), with no payload: it would be
of no use since the code for creating a new document is defined on the Server side.
Conversely, the 'runCode' Button emits an 'clientEvalJSX' (line 14) message with, as a payload,
the text content of the <textarea>.
As you suspect, these two messages are listened for in the Server side, so let’s now inspect it: you can
find the code in the local-server-socketio folder, on which I’ve installed via npm the socket.io
package.
To start the Server, cd into its directory and type:

node app.js

The app.js file is the main Node application, that works both as an HTTP Server (serving pages on
request), and Socket Server.
Adobe Generator 361

Server Side code in app.js


1 // Server Code mostly borrowed from:
2 // https://developer.mozilla.org/en-US/docs/Learn/Server-side/Node_server_without_fr\
3 amework
4
5 var fs = require('fs');
6 var path = require('path');
7 var app = require('http').createServer(handler);
8 var io = require('socket.io')(app);
9
10 app.listen(8099);
11 console.log('Server running at http://127.0.0.1:8099/');
12
13 // HTTP Server code
14 function handler(request, response) {
15 console.log('request ', request.url);
16
17 var filePath = '.' + request.url;
18 if (filePath == './') {
19 filePath = './html/index.html';
20 }
21
22 var extname = String(path.extname(filePath)).toLowerCase();
23 var mimeTypes = {
24 '.html': 'text/html',
25 '.js': 'text/javascript',
26 // Adding .jsx too!
27 '.jsx': 'text/javascript',
28 '.css': 'text/css',
29 '.json': 'application/json',
30 '.png': 'image/png',
31 '.jpg': 'image/jpg',
32 '.gif': 'image/gif',
33 '.wav': 'audio/wav',
34 '.mp4': 'video/mp4',
35 '.woff': 'application/font-woff',
36 '.ttf': 'application/font-ttf',
37 '.eot': 'application/vnd.ms-fontobject',
38 '.otf': 'application/font-otf',
39 '.svg': 'application/image/svg+xml'
40 };
41
42 var contentType = mimeTypes[extname] || 'application/octet-stream';
Adobe Generator 362

43
44 fs.readFile(filePath, function(error, content) {
45 if (error) {
46 if(error.code == 'ENOENT'){
47 fs.readFile('./html/404.html', function(error, content) {
48 response.writeHead(200, { 'Content-Type': contentType });
49 response.end(content, 'utf-8');
50 });
51 }
52 else {
53 response.writeHead(500);
54 response.end('Sorry, check with the site admin for error: ' +
55 error.code + ' ..\n');
56 response.end();
57 }
58 }
59 else {
60 response.writeHead(200, { 'Content-Type': contentType });
61 response.end(content, 'utf-8');
62 }
63 });
64 }
65
66 // Socket.io code
67 io.on('connection', function (socket) {
68
69 // Message received from the Browser
70 socket.on('newDocumentFromBrowser', function (data) {
71 // data is undefined here
72 var filepath = path.join( dirname, 'jsx', 'newDocument.jsx')
73 console.log(filepath);
74 // Read the JSX on disk
75 fs.readFile(filepath, function(error, filedata){
76 if(error) { throw error }
77 else {
78 // use broadcast.emit to reach the Generator Client
79 // emit would respond to the Browser Client only
80 socket.broadcast.emit("clientEvalJSX", filedata.toString());
81 }
82 });
83 });
84 // Message received from the Browser, and bounced back to Generator
85 socket.on('clientEvalJSX', function (data) {
Adobe Generator 363

86 socket.broadcast.emit("clientEvalJSX", data);
87 });
88
89 });

The HTTP Server code (with no use of frameworks such as Express to keep it simple) is mostly
borrowed from the Mozilla Developer Network. The Server is created on line 6, and passed a
handler() function (which body is on lines 13-63), where the HTTP requests are routed; it is a
very minimal code, we’re interested in serving one index.html page only.
The Socket Server is on lines 66-86. On 'connection', it listens for the two aforementioned
messages. When it receives 'newDocumentFromBrowser' (the message linked to the first button click
on the Browser), the Server fetches the content of the newDocument.jsx file (line 74) that resides
on the Server’s disk, and then emits a 'clientEvalJSX' message in the direction of the Generator
client. It does so via socket.broadcast.emit: if you use emit only, it would be targeting the Browser
(the Socket it has received the message from in the first place). Prepending broadcast, as the
documentation points out, ensures that the message is broadcasted to all Clients except the one
it came from. The payload of this message is the content of the .jsx file that was read from disk.
When the Server receives 'clientEvalJSX' (lines 84-86, the message linked to the textarea) instead,
it bounces it via broadcast directly to Generator, emitting the same clientEvalJSX event with the
very same payload.
Finally, it’s time to look at the Generator plug-in. Please note that, even if it’s running on Node.js, it
doesn’t require the Server side Socket.io, hence you need to install the Client side socket.io-client:

npm install socket.io-client --save

Here’s the Generator plug-in code:


Generator Client code in main.js

1 (function () {
2 "use strict";
3
4 // Plugin metadata
5 const pluginMetadata = require("./package.json");
6 const PLUGIN_ID = pluginMetadata.name;
7 var _generator = null;
8
9 function init(generator, config) {
10
11 _generator = generator;
12 _generator.addMenuItem(PLUGIN_ID, "Socket.io Client", true, false);
13 _generator.onPhotoshopEvent("generatorMenuChanged",
Adobe Generator 364

14 handleGeneratorMenuClicked);
15
16 var io = require('socket.io-client');
17 var socket = io.connect('http://localhost:8099', {reconnect: true});
18 // var socket = require('socket.io-client')('http://localhost:8099');
19 socket.on('connect', function(){ console.log("Connected")});
20
21 socket.on('clientEvalJSX', function (data) {
22 console.log("Being requested to eval JSX data...\n", data)
23 _generator.evaluateJSXString(data);
24 });
25
26 function initLater() {
27 }
28
29 process.nextTick(initLater);
30 }
31
32 function handleGeneratorMenuClicked(event) {
33 }
34
35 exports.init = init;
36
37 }());

Besides the usual Generator code, the interesting part coms at line 16-24. When the connection is
made, Generator listens for the 'clientEvalJSX' as we would expect. Its only task is to run the
ExtendScript string it receives, hence the handler contains the evaluateJSXString() function we’re
Adobe Generator 365

already familiar with.


At this point, you can probably make total sense of the picture (please note that I’ve removed the
part that would involve Generator emitting messages, which was not pertinent in this case).
To sum up: the Browser Client sends two messages, which are caught by the Server. In one
case, the content of a .jsx file is read from the Server’s disk and emitted as a payload to
Generator; in the other case, the payload received is emitted, as is, to Generator. In both cases,
using socket.broadcast.emit() and the 'clientEvalJSX' message. Generator has to connect to the
Socket server, listen to that message and obligingly run the code it receives. Voilà, you’re driving a
remote¹⁹ Photoshop installation via a Web interface and Socket.io connection!

10.9 The Kevlar API for Generator


You may find that I’ve left out of the discussion one crucial piece, namely how to communicate
(if possible) from ExtendScript to Adobe Generator. It turns out that it is indeed feasible; the
documentation suggests that the simplest way to trigger a Generator action is faking a menu click,
and it’s even possible to pass a parameter.

1 function s2t(s) { return app.stringIDToTypeID(s) }


2 var desc = new ActionDescriptor();
3 desc.putString (s2t("name"), "my-menu-name");
4 // Example of additional parameter passed to the node.js code:
5 desc.putString (s2t("sampleAttribute"), "moreInfo" );
6 var returnDesc = executeAction (s2t("generateAssets"), desc, DialogModes.NO);

As an example, if you want to programmatically select the “Generate > Image Assets” menu, you
have to use:

1 var desc = new ActionDescriptor();


2 desc.putString (s2t("name"), "generator-assets");
3 executeAction (s2t("generateAssets"), desc, DialogModes.NO);

In this case, if you look at the lib/statemanager.js file of the Generator Assets repository, the
MENU_LABER is a localized string, hence I’ve used the MENU_ID, that in turn points to the "name"
property in the package.json file.
On the Generator side, you can deal with this call and the extra parameter of the previous example
this way:

¹⁹As I’ve pointed out earlier, the example uses a local Server, so everything happens on the same computer: as soon as you upload the
Server code to a remote machine, the original statement is 100% correct.
Adobe Generator 366

1 function handleGeneratorMenuClicked(event) {
2 // Ignore changes to other menus
3 var menu = event.generatorMenuChanged;
4 if (!menu || menu.name !== "my-menu-name") { return }
5 var startingMenuState = _generator.getMenuState(menu.name);
6 console.log("Menu event %s, starting state %s", event, startingMenuState);
7
8 // Additional parameter passed in from the ExtendScript side:
9 var sampleAttr = event.generatorMenuChanged.sampleAttribute;
10 console.log("Got a menu event with sample attribute: " + sampleAttr);
11 }

The only caveat being that ” These events will only be sent to the built-in (launched by Photoshop)
Generator process that communicates with Photoshop over pipes. These events will not be sent to
Generator processes connected via sockets.”
I take the chance here to unveil part of the Generator mystery. If you look at the generator-core
source code, following the internal calls that the API makes, you’ll find out that most if not all of
the Photoshop-related command are JavaScript wrappers on ExtendScript code. As an example, the
ubiquitous getDocumentInfo() that I’ve used in almost all the plug-ins I’ve shown in the previous
pages, under the hood calls "sendDocumentInfoToNetworkClient", which is part of the Photoshop
Kevlar API Additions for Generator:

1 var desc = new ActionDescriptor();


2 // ...
3 executeAction( s2t('sendDocumentInfoToNetworkClient'), desc, DialogModes.NO );

These functions are thoroughly documented on this page, and I strongly suggest you have a look at
them, as well as the generator.js source, and the quite interesting jsx folder within that project,
which is mostly what I’ve used to document myself in writing this long Chapter.
Generator is a true and for the most part unknown gem which you should study, for it’ll amplify to
a great deal your Photoshop Scripts potential.
11. Cross-Application Communication
Scripting provides you with means to let Photoshop communicate with a selection of other (so-
called) message enabled Adobe applications. There are two different ways to accomplish this goal:
the Cross-DOM API and BridgeTalk. I’ll briefly mention the first one, and spend most of this Chapter
dealing with the latter for reasons that will be clear to you shortly.

11.1 Cross-DOM API


As simple as it gets, you can directly call a minimal subset of Cross-DOM methods (i.e., available in
all the message enabled apps) via the dot operator. The latest available list of app identifiers is quite
old¹, but here it is as a reference.
acrobat, aftereffects, soundbooth, bridge, contribute, devicecentral, dreamweaver, encore,
estoolkit, fireworks, flash, illustrator, indesign, indesignserver, incopy, photoshop, premierepro,
audition, ame, exman

The application ID can be postfixed with an optional version ID: which is not the actual app.version
(e.g., for Photoshop CC 2019 the version is 20) but the number that ESTK uses to identify it, in our
case 130. I won’t dig deeper because it seems that this internal reference, at least for Photoshop, has
not been updated since CC 2017 (that is, 110), so be aware that photoshop130 will fail. As a rule, the
application identifier alone (e.g. photoshop) always refers to the latest version.
The list of methods is rather short too:

• executeScript(): requires a String parameter, and performs a JavaScript eval on the specified
script.
• open(): requires as a parameter a File object or an Array of objects, and opens it/them.
• openAsNew(): same as “File > New”, but works only for Illustrator and InDesign (see JS Tools
Guide, p.169).
• print(): requires as a parameter a File object or an Array of objects, equivalent of “File >
Print”.
• reveal(): brings the application to the foreground. Accepts an optional File object or an Array
of objects, and opens it/them
• quit(): closes the application.

So you are allowed to:

¹We’re talking about CS4


Cross-Application Communication 368

indesign.quit();
photoshop110.reveal();

It is not particularly exciting, but worth mentioning. This list of Cross-DOM methods is then
extended on applications basis: e.g., InDesign exposes its unique functions, Photoshop its own, etc.
How do you know them? You have to look at a .jsx file in either:

• /Library/Application Support/Adobe/Startup Scripts CC/Adobe Photoshop


• /Library/Application Support/Adobe/Startup Scripts CC/Adobe Bridge CC 2019

or

• C:\Program Files (x86)\Common Files\Adobe\Startup Scripts CC\Adobe Photoshop


• C:\Program Files\Common Files\Adobe\Startup Scripts CC\Adobe Bridge CC 2019

Please note that InDesign follows the same Photoshop folder names convention, while the Bridge
folder in Windows is different.
As an example, in the indesign-v13.0.jsx file I can look for methods of the indesign13 object.
There I find, for instance, an extra place() method. Similarly, photoshop_v2019.jsx exposes the
photoshop object² which has additional methods such as photomerge(), loadFilesIntoStack(),
imageprocessor() etc.

Startup Scripts folders

Since I’ve just mentioned some of them, let me fully address this topic before going any further.
Adobe provides us with places to store .jsx files that are going to be automatically executed when
an application (PS, ID, BR, etc.) is launched.
As third-party developers, we are advised not to use the system folders and put our startup scripts
elsewhere. Restricting the list to just Photoshop and Bridge, we can identify some alternatives (first
Mac, then Windows):

• /Users/<yourUser>/Library/Application Support/Adobe/Startup Scripts CC/Adobe Photoshop


• /Users/<yourUser>/Library/Application Support/Adobe/Bridge CC 2019/Startup Scripts

and

• C:\Program Files (x86)\Common Files\Adobe\Startup Scripts CC\Adobe Photoshop³


²As opposed to InDesign, which defines the indesign13 object first and then assigns it to the indesign variable for convenience, Photoshop
does the reverse: it starts with photoshop and assigns it to photoshop110 – which, as previously stated, should be 130 instead.
³On Mac, there is a difference between User’s and System’s Library (so there are two paths); I haven’t been able to find the Windows
correspondent path, so I’ve replicated the one I have already mentioned.
Cross-Application Communication 369

• C:\Users\<yourUser>\AppData\Roaming\Adobe\Bridge CC 2019\Startup Scripts

Please note that while Bridge contains the Startup Scripts, it’s Startup Scripts that contains
Photoshop on Mac, and the Windows path is completely different. Also, there is the simpler (and
multi-platform):

• <AppFolder>/Scripts/Startup Scripts
• <AppFolder>/Startup Scripts

As a bonus, Bridge automatically creates /Startup Scripts as soon as you put a .jsx file in the
application folder, showing a popup that says: “The Bridge extension ‘whatever’ has been added to
Bridge. Do you want to enable it now?”. If you confirm, the file is actually moved there. Please note
that Bridge also reacts to scripts that belong to Photoshop’s folders.

11.2 BridgeTalk concepts


If you look at the Cross-DOM functions body (e.g. in photoshop_v2019.jsx) you’ll find that they
are just utility methods, internally implemented with BridgeTalk: the API that defines the entire
inter-application communication protocol.
At its core, BridgeTalk is a synchronous or asynchronous system that allows applications to send and
receive messages (which payload is usually a script that manipulates the target-application DOM)
and deal with responses.

Curiously, an application is allowed to target itself : e.g., Photoshop can send messages to
Photoshop, which is a handy way to make asynchronous calls.
I’ve used this system in the past to build sticky Palette Windows (that, as you remember
from Chapter 7, are officially unsupported). I’ve written an article on my blog called
ScriptUI: BridgeTalk persistent Window examples that you may want to check out – with
caveats when using jsxbin and BridgeTalk together.

Static Methods and Properties

The BridgeTalk Class is globally available: to build a message you need to instantiate it, but static
methods and properties are useful to perform a variety of tasks. They are quite self-explanatory, so
let’s check some of them out.
Cross-Application Communication 370

// Some Static Properties

BridgeTalk.appName // photoshop
BridgeTalk.appVersion // 130.64
BridgeTalk.appSpecifier // bridge-9.064

// Some Static Methods

BridgeTalk.getDisplayName("bridge") // Bridge CC 2019


BridgeTalk.getAppPath("bridge")
// /Applications/Adobe Bridge CC 2019/Adobe Bridge CC 2019.app
BridgeTalk.getAppPath("bridge-8") // using a version specifier
// /Applications/Adobe Bridge CC 2018/Adobe Bridge CC 2018.app
BridgeTalk.getTargets()
// ame, audition, bridge, estoolkit, exman, flash, flashbuilder,
// indesign, photoshop, premierepro, switchboard
BridgeTalk.isRunning("bridge-9") // true
BridgeTalk.launch("photoshop") // returns true afterwards

A comprehensive list is found, as usual, in the JS Tools Guide p.180-185.

The BridgeTalk Instance

It’s time to send our first message – to keep it super-simple, to Photoshop. At the very minimum,
you must:

• instantiate the Class


• set the target: the application the message is going to be sent to
• set the message body: the scripting code that you want the target application to execute
• send it.

The code that does this is as follows - make sure Photoshop is open, type and run this in ESTK and
an alert will pop up.

1 var bt = new BridgeTalk(); // Instantiating the Class


2 bt.target = "photoshop"; // Setting the message target
3 bt.body = "alert('Hello BridgeTalk!')" // Setting the message body
4 bt.send(); // Sending the message

Let’s build from that, gradually exploring deeper levels of complexity. First, it is good practice to
check if the target application is currently running:
Cross-Application Communication 371

if (BridgeTalk.isRunning("photoshop")) {
// Build the BridgeTalk instance
}

In case you want to open the target application if it’s not running, you can use BridgeTalk.launch():
although, it is a bit tricky to properly do it in such a way that subsequent BridgeTalk messages are
timely and working. After a series of unsuccessful tries, I’ve stumbled upon code written around the
year 2004 by Bob Stucky⁴, in a file called AdobeLibrary1.jsx. I quote below the comments that you
can read there.

// There is a bug in BridgeTalk that causes messages sent to an


// application that is starting to become disconnected from their
// onResult handlers. The solution is to start the application
// with a BT message, and wait until an onResult handler is fired.
// At that point, the target application is fully started and
// ready to work with BridgeTalk.
// [...]
// If you are relying on the onResult handler to fire in any
// script, ALWAYS execute this function prior to sending.

I have simplified the function that Bob uses, and reworked the previous example as follows:

1 function startApplication(target) {
2 if (!BridgeTalk.isRunning(target)) {
3 BridgeTalk.launch(target);
4 var counter = 0;
5 while (!BridgeTalk.isRunning(target)) {
6 $.sleep(3000);
7 // allow 60 seconds for the task
8 if (counter++ == 20) {
9 alert("Can't launch " + target);
10 return false;
11 }
12 }
13 var counter = 0,
14 isOK = false;
15 while (!isOK) {
16 var bt = new BridgeTalk();
17 bt.target = target;
18 bt.body = "var t = " + target;
⁴Bob is a Computer Scientist at Adobe, who in the past has worked on cross-application communication; he has shared an awful lot of
high-quality code in the forums over the years.
Cross-Application Communication 372

19 bt.onResult = function (obj) { eval("isOK = true") }


20 bt.send();
21 $.sleep(3000);
22 BridgeTalk.pump(); // process queued messages (if any)
23 // allow 60 more seconds
24 if (counter++ == 15) {
25 alert("Can't launch " + target);
26 return false;
27 }
28 }
29 }
30 return true;
31 }
32
33 if(startApplication("photoshop")) {
34 // Build the BridgeTalk instance
35 };

Don’t worry if you can’t get its meaning entirely now, by the end of the Chapter you will. Going
forward, the message body is usually far more complex. One of the common strategies is to
encapsulate it in a function, stringify it and call it immediately after that, as in the following example.

1 function main() {
2 var docNum = app.documents.length, popup;
3 // Fancy way to alert grammatically correct messages about open documents
4 var message = "There " +
5 (docNum == 1 ? "is" : "are") +
6 (docNum == 0 ? "n't" : " " + docNum) +
7 " open document" + (docNum != 1 ? "s" : "");
8 alert(message);
9 }
10
11 var bt = new BridgeTalk();
12 bt.target = "photoshop"
13 // The `body` property is the function definition plus the function call
14 bt.body = "" + main.toString() + "; main();"
15 bt.send()

In case you happen to use toSource() instead of toString() to stringify the function, be
aware that using the double forward slash // for comments within the body will likely cause
errors; always use the /* */ syntax.
Cross-Application Communication 373

Alternatively, you can avoid wrapping the payload within a function and store it in a sidecar .jsx
file instead; you can then read it from disk and put the content directly in the BridgeTalk body (this
method works fine even when reading .jsxbin files from disk).

1 /* alertDocument.jsx */
2 var docNum = app.documents.length, popup;
3 var message = "There " + (docNum == 1 ? "is" : "are") +
4 (docNum == 0 ? "n't" : " " + docNum) +
5 " open document" + (docNum != 1 ? "s" : "");
6 alert(message);
7
8 /* ReadExternalFile.jsx */
9 var includeFile = new File(File($.fileName).path + "/alertDocuments.jsx");
10 if (includeFile.exists) {
11 includeFile.open("r");
12 var fileContent = includeFile.read();
13 includeFile.close();
14 var bt = new BridgeTalk();
15 bt.target = "photoshop";
16 bt.body = fileContent;
17 bt.send();
18 }

The BridgeTalk message has two additional properties that you should know about. First, there’s the
headers: it is an object that most of the times you’re not very much interested into, except when
either it contains a ["Error-Code"] prop (see the Event Handling section), or if you want to add
custom, extra information:

bt.headers.operator = "John Doe";


bt.headers["ticket-id"] = "BAR-2018-223901"

Lastly, there’s the type property, which by default is "ExtendScript" (i.e., scripting code, to be
executed in the target application). If you set a different one, make sure to implement a onReceive()
callback, as explained further in this Chapter.

Synch and Asynch behavior

BridgeTalk can work either synchronously or asynchronously, depending on the optional param-
eter passed to the send() method: the timeout in seconds. If you don’t specify the timeout or set it to
zero, the call is async; if the timeout is greater than zero, it is sync – i.e., the function won’t return
until either the target has processed the message or the timeout seconds have passed.
Cross-Application Communication 374

I’ve found particularly unreliable to work in ESTK when BridgeTalk is concerned. For
instance, callbacks (which will be covered in the next session) may be bypassed, BridgeTalk
always behaves asynchronously, etc. I strongly suggest you write a file on disk and run it in
Photoshop.

11.3 Event handling


In asynchronous code, callbacks are the way to go to control and guide the Events flow. Let’s set up
a simple scenario involving Photoshop and Bridge: PS wants to know how many files are currently
selected in BR and open them.
A selected document is a Thumbnail object in Bridge lingo, stored in the app.document.selections
collection. Conveniently, a Thumbnail has a path property. To test this, select a couple of files in
Bridge and run the following lines in the Console:

app.document.selections
// [object Thumbnail],[object Thumbnail]
app.document.selections[0].path
// /Users/davidebarranca/Desktop/test.jpg

So, Photoshop (the sender) is going to dispatch a BridgeTalk message to Bridge (the target), with a
Payload (the body) that consists of some Bridge Scripting code (involving the selections collection).
In Bridge, the result of the evaluation of the script is stringified and packed as the body of a response
object, that in turn is passed as the parameter of the onResult() callback function, that (if present)
is invoked. onResult() is took in charge by the sender (Photoshop), and it serves the purpose of
elaborating the response from the target (Bridge): in our case, PS will open the files BR has handed
it. Let’s implement this one.

1 if (BridgeTalk.isRunning('bridge')) {
2 var bt = new BridgeTalk();
3 bt.target = "bridge";
4 bt.body = "" + getSelectedFilesPath.toString() + "; getSelectedFilesPath();";
5 bt.onResult = function(response) {
6 var filesArray = eval(response.body);
7 for (var i = 0; i < filesArray.length; i++) {
8 app.open(new File(filesArray[i]))
9 }
10 }
11 bt.onError = function(err) {
12 alert("Error!\n" + err.body)
13 }
Cross-Application Communication 375

14 bt.send(20);
15 alert("Done")
16 }
17
18 // Bridge function
19 function getSelectedFilesPath() {
20 var filesArray = [];
21 for (var i = 0; i <= app.document.selections.length - 1; i++) {
22 if (app.document.selections[i].type == "file") {
23 filesArray.push(app.document.selections[i].path);
24 }
25 }
26 return filesArray.toSource();
27 }

The BridgeTalk instance is built as usual; the body is the stringified function getSelectedFilesPath()
(line 19) that will be executed in the Bridge environment. There, I’m looping through the selections
array, filtering off folders, and pushing the path strings into filesArray. Please note that such array
is not directly returned: all returned values are casted to Strings, and to be able to rebuild the Array
down the line, I need to stringify it with toSource() in advance (line 26).
The onResult() function (line 5) is passed one parameter, the response object, which body contains
the stringified array of paths that Bridge has found and returned. To use it, I need to parse it into an
actual Array with eval(). As the last step, I’m looping and opening all the Files in Photoshop.
Note also the onError() callback, (line 11) that I strongly suggest you to always use, for BridgeTalk
debugging can be quite difficult. When something goes wrong, this callback is passed an object,
which headers prop, in turn, contains a ["Error-Code"] prop⁵: a number linked to the kind of issue
you’ve run into (see JS Tools Guide, p.190 for a detailed list of error numbers). Lastly, I’ve set a 20
seconds timeout with send(20) (line 14) to demonstrate the synchronous behavior: the alert (line
15) is called only when all the files have been opened. If you send() the BridgeTalk message, the
callback will be fired immediately, in a perfectly asynchronous fashion.

DOM Objects returns

According to the documentation, it should be possible to return DOM objects too, provided the use
of toSource() and eval() and only when the properties of interest have been accessed once in the
target application. As an example, take this Bridge function, modeled on the previous example:

⁵You can use either the dot or the square brackets notation to access headers props, e.g. headers["Error-Code"] or headers.stuff.
Cross-Application Communication 376

1 // to be stringified, injected and called in the BridgeTalk body


2 function returnDOMObj() {
3 var thumbArray = [];
4 for (var i = 0; i <= app.document.selections.length - 1; i++) {
5 var thumb = app.document.selections[i]
6 if (thumb.type == "file") {
7 // according to the documentation the following lines somehow
8 // "enable" the props, and allow them to be sent down the line
9 var p = thumb.path; // <- dummy access
10 var r = thumb.rating // <- dummy access
11 thumbArray.push(thumb.toSource());
12 }
13 }
14 return thumbArray.toSource();
15 }

No way to make this work, alas! I’ve tried with a less ambitious version too (one Thumbnail only),
no luck. I even tested CE6, the older versions that I have installed.
Mind you, if you try to pass stuff like Folders and Files it works, e.g.:

bt.body = "var f = new Folder('" + File($.fileName).path + "/img'); f.toSource()";

The fact is that, to the best of my knowledge, no actual DOM object (at least in Photoshop, and
apparently in Bridge too) can be toSource()-ified properly, hence it cannot be eval()-ued later on.
If you want to check what can pass through the BridgeTalk net and what can’t, test if toSource()
produces something different than an empty object: if that’s the case, it’s going to do the trick.

// Photoshop DOM, feel free to test other DOMs


app.activeDocument.activeLayer.toSource(); // KO -> ({})
app.preferences.toSource(); // KO -> ({})
app.preferences.imageCacheLevels.toSource(); // OK -> (new Number(4))
(new Folder(Folder.desktop)).toSource(); // OK -> (new Folder ("~/Desktop"))

You can find a couple of examples in the code .zip file. That aside, remember that all returns are
casted to String (even primitive values, such as numbers, booleans, etc.): hence, no matter whether
you want to return Objects, Arrays or even Primitives, always use toSource() and eval().

Receiving messages
To implement some slightly more sophisticated scripts, let me introduce two easily confused
BridgeTalk features which apparently differ only by one letter.
The onReceived() callback is a dynamic method of the BridgeTalk instance.
Cross-Application Communication 377

bt.onReceived = function(){/* ... */}

It is fired when the message gets to the target, and acts as a dispatching receipt. An entire copy of
the original message is passed as a parameter, but stripped of the body, which is empty. It is rarely
used, but as an example:

1 if (BridgeTalk.isRunning('bridge')) {
2
3 // Keep ESTK open to read the log messages
4 var bt = new BridgeTalk();
5 bt.target = "bridge";
6 bt.body = "$.writeln('Bridge alerting...')";
7
8 bt.onResult = function(response) {
9 $.writeln('onResult')
10 parseResponse(response);
11 }
12
13 bt.onReceived = function(response) {
14 $.writeln('onReceived')
15 parseResponse(response);
16 }
17
18 bt.onTimeout = function(response) {
19 $.writeln('onTimeout')
20 parseResponse(response);
21 }
22
23 bt.onError = function(err) {
24 $.writeln("onError\n" + err.body);
25 }
26 bt.send(20);
27 $.writeln("Process completed")
28 }
29
30 function parseResponse(res) {
31
32 var headers = "";
33 for (var prop in res.headers) {
34 headers += "\t" + prop + ": " + res.headers[prop] + "\n";
35 }
36 var retVal = "==============\n" +
37 "sender: " + res.sender.toString() +
Cross-Application Communication 378

38 "\ntarget: " + res.target.toString() +


39 "\nheaders:\n" + headers +
40 "body: " + res.body.toString() +
41 "\n ---------------- \n";
42
43 $.writeln(retVal);
44 }
45
46 // Bridge alerting...
47 // onReceived
48 // ==============
49 // sender: bridge-9.064
50 // target: photoshop-130.064
51 // headers:
52 // Sender-ID: localhost:mac82734
53 // body:
54 //
55 //
56 // onResult
57 // ==============
58 // sender: bridge-9.064
59 // target: photoshop-130.064
60 // headers:
61 // Sender-ID: localhost:mac79666
62 // body: undefined
63 //
64 //
65 // Process completed

Photoshop sends a BridgeTalk message, which instructs Bridge to write a 'Bridge alerting...'
string (line 6), the first thing in the log. Immediately after that, the onReceived() callback is triggered
(line 13), then, as expected, onResult() (line 8), and eventually the "Process completed" string (line
27) is logged.
I took the chance to explicitly write all the possible callbacks, including onTimeout(), and I wrote
a little utility function to log the received payloads. Please note the stripped body in onReceived(),
and the fact that in both cases bridge is the sender while photoshop is the target: i.e. both callbacks
are triggered in consequence of the Bridge reaction, which is responding.
In contrast with the dynamic onReceived() callback that is set while building the BridgeTalk object
by the sender as a dispatch confirmation, onReceive() is a static property of the BridgeTalk class,
and it is used to override the target application’s default behavior, when it gets the message.
Cross-Application Communication 379

BridgeTalk.onReceive = function(){/* ... */}`

By default, onReceive() only evaluates the ExtendScript code that is sent as a payload in the body
prop. In other words, if you don’t explicitly write the callback yourself, this is what it would look
like under the hood:

// To be set in the target app!


BridgeTalk.onReceive = function(obj) { return eval( obj.body, true ); }

In the following snippet, I’ve injected an additional alert() to onReceive() so that Bridge will pop
up 'Got a message' every time he gets a BridgeTalk message.

1 if (BridgeTalk.isRunning('bridge')) {
2 var bt = new BridgeTalk();
3 bt.target = "bridge";
4
5 var onReceiveStr = "BridgeTalk.onReceive = function(obj) { " +
6 "alert('Got a message'); return eval( obj.body, true ) }"
7 bt.body = onReceiveStr + "var t = 'Dummy message'; t;"
8
9 bt.onResult = function(response) {
10 $.writeln("onResult")
11 }
12 bt.onReceived = function(payload) {
13 $.writeln("onReceived")
14 }
15 bt.onError = function(err) {
16 alert("Error!\n" + err.body)
17 }
18 bt.send(20);
19 alert("Done")
20 }

In the example above, Photoshop sends a dummy message to Bridge (the target). Let me
stress the fact that the BridgeTalk.onReceive() definition must belong to the target code,
not the sender’s. onReceive() is, in fact, in the message body (lines 6-8). Had I put it, say,
after onError(), it would have affected Photoshop instead – which is not what I wanted.

In the onReceive() you may also deal with unsolicited messages, or filter them by their type to
process them accordingly. For instance:
Cross-Application Communication 380

1 // to be put in the bt.body


2 BridgeTalk.onReceive = function (msg) {
3 switch (msg) {
4 case "Data":
5 return doSomethingWithIt(msg);
6 break;
7 default: // It is "ExtendScript"
8 return eval(msg.body);
9 }
10 }

Let’s jump to the next section for an example that combines message’s custom type and the
onReceive() callback.

Sending intermediate responses

To wrap this Chapter up, I wanted to show you one tricky way to deal with intermediate responses:
how to dispatch back to the sender partial results while keeping the communication channel alive.
This is described in the JS Tools Guide, but no complete .jsx is supplied for testing: and even if
they provided it, given the success rate of the BridgeTalk section a Hollywood ending wouldn’t be
assured anyway.
I’ve spent a couple of days trying to cook a Photoshop & Bridge demo from the available code, but
it doesn’t entirely work – I think due to an onResult() bug, mixed with some caching problems. I
haven’t found any help online either. I will share my state-of-the-art code here, so that you can pick
up from there and try to progress on your own.
Intermediate (or multiple) responses in this context are, if you will, the BridgeTalk equivalent
of Node.js HTTP responses: if you’re not familiar with them, when making an http request in
Node, the server replies sending back chunks, which are passed to the on('data') callback that
processes, or just collects, them. When the server is done, the on('end') callback is eventually able
to process/return the joined response.
The concept here is similar: we have a Photoshop script that sends a BridgeTalk message to Bridge,
asking it to return Thumbnails objects from a folder. Bridge won’t respond in one shot with an Array
of Thumbs, but will chunk the response into single Thumbnails, one by one. Given this plan, let’s
check out my implementation, according to the Documentation principles.
The idea is to exploit a custom message type (which I’ll call "iterator"), and modify the
onReceive() callback so that when a message of type equal to "iterator" gets to the target, a loop
is created and intermediate responses are sent back to Photoshop via the message’s sendResult()
method. Let’s check the onReceive() code first.
Cross-Application Communication 381

1 BridgeTalk.onReceive = function(message) {
2 switch (message.type) {
3 case "iterator":
4 var done = false;
5 var i = 0;
6 while (!done) {
7 message.sendResult(eval(message.body));
8 BridgeTalk.pump();
9 i++;
10 }
11 break;
12 default:
13 return eval(message.body);
14 }
15 }

The key is in the "iterator" case: a boolean is set to false and a counter is initialized to zero:
both are used in the while loop. The message.sendResult() sends back the intermediate result,
automatically packing the returned value of the Photoshop message’s body property into the body of
a newly created message that should trigger the onResult() callback. You need to see the original
message now, to make sense of this.

1 // The Payload
2 var payload = """imgFolder.refresh();
3 var c = imgFolder.children;
4 alert('Children: ' + c)
5 if (i == (c.length - 1)) {
6 alert('We are done processing...')
7 done = true;
8 }
9 alert('Iterator: ' + i);
10 f = c[i];
11 if (f.spec.constructor.name == 'File') {
12 alert(f.path);
13 p = f.path
14 } else {
15 p = -1;
16 }""";
17
18 var idx = 0;
19 bt = new BridgeTalk();
20 bt.target = "bridge";
21 bt.type = "iterator";
Cross-Application Communication 382

22 bt.body = "BridgeTalk.onReceive = " +


23 BridgeTalk.onReceive.toString() +
24 "var imgFolder = new Thumbnail(Folder('" +
25 new File($.fileName).path +
26 "/img'));" +
27 payload;
28
29 // process intermediate results
30 bt.onResult = function(rObj) {
31 alert("Received #" + idx + ": " + rObj.body);
32 app.open(new File(rObj.body));
33 idx++;
34 };
35
36 bt.onError = function(eObj) {
37 alert("ERROR!" + eObj.body)
38 };
39 bt.send();

Let’s start with line 18 (we’ll get to the payload later): I’ve initialized another index to be used in
Photoshop for logging purposes. The body (line 22-27) is composed joining the onReceive() callback,
an imgFolder variable containing the path to the /img folder that sits alongside with this script itself,
and eventually the payload.
The onResult() callback (line 30) alerts the data received via sendResult() (from the onReceive()
function), and opens the one File he’s been passed the reference to.
Let’s check the payload string now: imgFolder, as we’ve seen, is a Thumbnail pointing to a Folder. I
refresh() it first (line 2), to get rid of the otherwise empty cached content (no files); then I use the c
variable to store the children (the files contained therein), checking the i counter. There’s no variable
declaration for i here, since it is the one that belongs to the while loop we’ve seen in onReceive();
if we’re done with the files, the done boolean (from onReceive() too) is set to true and the while
loop there will exit. Otherwise, a check for the type of the child is performed, and if it is a File, the
path is returned – and it’ll be packed in the rObj.body of line 31.

Issues

If you run the 11_06_MultipleResponses.jsx script, you’ll see that it will suddenly stop at iteration
#0, alerted in Bridge. The first problem is that, for reasons I couldn’t be able to understand, even if
the folder is refresh()-ed, the content is not cached properly the first time you launch the script.
Try rerunning it, and you’ll see that it will work (at least on Mac).
Cross-Application Communication 383

Let me stress again the fact that, when BridgeTalk is involved, you should never use ESTK to
test your scripts. Open them in the application or the onResult() callback will have issues.
Also, closing and reopening Bridge may help too, so if you’re stuck with unwanted outputs,
try restarting the application.

Bridge, at the second run, correctly loops and alerts all the files in the /img folder, but they can’t get
to the onResult() callback. If you check the returned value of the sendResult() call in onReceive()
you’ll see that it is false. Besides the caching issues, the main problem here is that onResult() is
never triggered. In the scarce information I’ve been able to find online, nobody’s been able to make
it work, so I guess it is a bug⁶.

11.4 Wrap-up
BridgeTalk resources, besides the JS Tools Guide p.166-193, can be found in the ESTK samples and
some of the internal ESTK resources: if you’re on Mac, right-click the ExtendScript Toolkit.app
and “Show Package Contents”, you’ll find several interesting .jsx files in:

• /ExtendScript Toolkit.app/Contents/SharedSupport/Required

On Windows you should be able to find them directly within the “Adobe ExtendScript Toolkit CC”
folder. If you want to dig deeper, I suggest you check out the Forums: don’t limit yourself to the
Photoshop Scripting one, but also try in the InDesign’s, for this is a cross-application technology.
I’m afraid I may have sound pessimistic here and there in this Chapter, but BridgeTalk seems to be
one of those subjects that are either neglected by Adobe, or plain unlucky. Combined with Bridge,
which is in the record for having lots of scripting bugs, they don’t make a glorious experience, so to
speak.
⁶At least on Mac / CC 2019.
12. Appendix
12.1 Deploying Scripts
At this point, the last piece of the puzzle is knowing how to distribute your work: you’ve developed
neat scripts, they run fine on your computer, it’s time to let others have and use them. Let me give
you a short checklist of recommended practices.

Testing
The very first thing to do before releasing scripts into the wild either for free or as paid products, as
obvious as it sounds, is to test them thoroughly.
Decide what the supported backward compatibility range is (say, from CC 2015 to CC-latest), then
install all the versions of Photoshop that you need. The Creative Cloud application allows you to
download software as old as CS6: follow the instruction on this page to see how.
If you don’t own both Mac and Windows-based computers, use a Virtual Machine software such
as Parallels or VMWare, and make sure that your script is truly multi-platform – especially, but
not exclusively, if it makes use of ScriptUI dialogs. Do not give anything for granted! In Photoshop,
old bugs are fixed while new ones are born: QA (Quality Assurance, i.e., testing) is tedious and
time-consuming, but in the long run it pays off always.

A considerable part of my scripts’ code is dedicated to edge-cases: decide in advance which ones you
want to deal with, and document all the rest so that you can point your users to proper instruction.
Don’t over-use try/catch blocks, but make sure that, if errors are thrown, they’re less scary as
possible to the user.
Appendix 385

Obfuscation

In case you’re concerned about your code’s privacy, that is to say, you want to protect it from users
and developers prying eyes, some kind of obfuscation is required. Before going any further, let’s split
hair in two for a moment.

Minifiy, Optimize, Obfuscate?


Minification is the process of compressing your code, usually aiming to a less bandwidth-
consuming delivery over the internet – not a big concern of ours in this context. Usually
minified code is stripped of comments and white spaces, variable and function names are
replaced with shorter strings, etc.
Optimization targets faster execution, e.g., via: “Dictionary compression, a lossless data
compression algorithm that uses as dictionary your own JavaScript source code to replace
duplicates by a reference to the existing match thus reducing even more its raw byte size.”
(quoted from a paid service).
Obfuscation is a process which ultimate goal is to make code harder to understand. As a
funny but working example, try this to see what a “Hello World” alert looks like in Japanese
textual emoticons.

As I’ve mentioned earlier in this Course, ExtendScript doesn’t particularly like to be massaged by
modern tools. Its extended features throw errors when parsed by JavaScript engines; even if you
don’t make use of, say, XML literals, old bugs in the language implementation (e.g., RegExp, ternary
operator) and ES5 syntactic sugar are likely to break the code anyway.
Bitter surprises are behind the corner: many years ago I did test libraries such as ES5 Shim and
CryptoJS against a few minification algorithms: I’ve found that even if the compressed code gets
parsed without errors, you shouldn’t assume that it actually runs as expected. In fact, some functions
didn’t behave properly at all. So be careful and test, test, test.
Besides JavaScript obfuscation services (the one I’ve been using for HTML Panels is the free and
excellent JavaScript Obfuscator Tool), you can export ExtendScript in its unique so-called binary
form. Open a .jsx file in ESTK and choose “File > Export as Binary…” The result is going to be a file
with .jsxbin extension, that you are required to rename to .jsx before distribution.
The jsxbin is not a real binary format, though, but a highly sophisticated textual obfuscation:

1 @JSXBIN@ES@2.0@MyBbyBn0ACJAnASzCjNjFByBWzGiPjCjKjFjDjUCEzEjOjBjNjFDFePiEjBjWjJjE
2 jFhAiCjBjSjSjBjOjDjBzDjBjHjFEFdhIzHjDjPjVjOjUjSjZFFeFiJjUjBjMjZzFjHjSjFjFjUGNyBn
3 AMEbyBn0ABJEnAEXzHjXjSjJjUjFjMjOHfjzBhEIfRBFeFiDjJjBjPhBff0DzAJCEnftJGnAEXGfVBfy
4 BnfABB40BiAABAJByB

Cryptography theory says that, like with any other ciphertext, given an infinite number of
plaintext/cipher pairs (jsx and corresponding jsxbin), you can find the key to decode it. Which
Appendix 386

is what ESTK provides you: start with var a; and inspect the resulting jsxbin code, then keep
adding complexity and write down your findings. On and on.
You will understand me if I’m vague here, as we’re at the dangerous intersection of legal and privacy
issues: lately, jsxbin has proved to be not as sealed as we liked to think. I’m not saying that everybody
can flip it and turn it back into readable code in a snap (can you? can somebody you know?), but
the probability that somebody can decipher your jsxbin isn’t zero anymore. I’d say quite small, yet
not zero.
Made by the very talented InDesign developer Marc Autret, a free utility called JsxBlind tackles
the issue of code protection from a different perspective. It presupposes jsxbin as the source and
outputs a further processed jsxbin. According to the author, if somebody decoded the JsxBlind-ed
version, he would get scrambled variables and function names.

Image courtesy of Marc Autret

JsxBlind is provided for free on Marc’s website: the latest versions work as a library too, so both
the jsxbin and JsxBlind conversions can be automated. As follows, a demo compile.jsx script that
reads the content of a main.jsx sitting in the same folder, and turns it into a JsxBlind-ed version
named main.blind.jsx.
Appendix 387

compile.jsx demo
1 #target estoolkit#dbg
2 #include 'JsxBlindLib.jsxinc'
3 // This code refers to JsxBlind V1.x,
4 // it implies that JsxBlindLib.jsxinc is available
5
6 var currentFolder = File($.fileName).path;
7
8 /* Read the current, plain JSX */
9 var originalFile = new File(currentFolder + "/main.jsx");
10 originalFile.open("r");
11 var originalString = originalFile.read();
12 originalFile.close();
13
14 /* Compile JSXBIN */
15 var jsxBinString = app.compile(originalString);
16
17 /* Run JSXBLIND */
18 JsxBlind.keepFunctionNames = 1;
19 JsxBlind.muteAlerts = 1;
20 var jsxBlindString = JsxBlind(originalString);
21
22 var blindedFile = File(currentFolder + "/main.blind.jsx")
23 blindedFile.open("w");
24 blindedFile.write(jsxBlindString);
25 blindedFile.close();

To run the script, make sure ESTK is closed, in the Terminal (Mac) or Command Prompt (Windows)
make sure you’re in the same folder and:

1 "/Applications/Adobe ExtendScript Toolkit CC/ExtendScript Toolkit.app/Contents/MacOS\


2 /ExtendScript Toolkit" -cmd " ./compile.jsx"
3
4 "C:\Program Files (x86)\Adobe\Adobe ExtendScript Toolkit CC\ExtendScript Toolkit.exe\
5 " -cmd "compile.jsx"

The command should launch and target ESTK, run the compile.jsx script, output the main.blind.jsx
file and quit ESTK.

Installation
There are several ways to let people install your scripts. I’ve thoroughly discussed them in a dedicated
book of mine, titled The Ultimate Guide to Native Installers and Automated Build Systems.
Appendix 388

Your options range from:

• manual installation: copy and paste files in the appropriate folder, usually Photoshop’s
Presets/Scripts.
• ZXP installers: suitable to be deployed via Adobe Exchange/Add-ons and/or installed with
third-party tools.
• Scripted Installers: such as my open source project PS-Installer.
• Native Installers: .pkg and .dmg for Mac, .exe for Windows, with an optional digital signature.

It is hard to generalize which is the best option, it really depends on you. In any case, I strongly
suggest to spend some extra time documenting the installation process, for it has proved to be the
most frequent source of support tickets: it wins hands down, and by a large margin.
Appendix 389

12.2 Resources
I’ve collected below a variety of links to freely available resources about, and around, Photoshop
development. If you think I’ve missed something important, please let me know!

Adobe’s Official
• Photoshop Scripting, with links to the official documentation and Scripting Listener plug-in.
• Photoshop Scripting Forum
• Bridge Scripting Forum
• InDesign Scripting Forum, helpful for topics on language, patterns, etc
• Adobe Tech Blog
• Adobe Photoshop on GitHub
• Adobe Photoshop Generator

Third-party Documentation
• Photoshop Object Library by Gregor Fellenz
• JS Tools Guide web version by AEnhahncers
• ExtendScript API Documentation

Third-party Forums and Groups


• PS-Scripts is the most important independent PS Scripting forum
• AEnhancers, dedicated to AfterEffects
• Extendscript Slack Group

Software Developers’ websites


In which they usually share interesting thoughts and resources too.

• Trevor Morris
• Michel Mariani aka Mikaeru
• Marc Autret
• Jeffrey Tranberry
• Jaroslav Bereza
• Cristian Buliarca
• Kris Coppieters
• Gabe Harbs
• Dirk Becker
• Trevor
• Zetta aka Sandra Voelker
Appendix 390

ScriptUI
• ScriptUI for Dummies by Peter Kahrel

Text Editors Plug-ins


• Visual Studio Code: Adobe Script Runner, ExtendScript, VSCode to Photoshop, Adobe Type-
Script Sandbox
• Atom: Atom to Photoshop, Adobe Script Runner and Build ExtendScript
• Sublime Text
• Adobe Brackets

Libraries, Frameworks, and Repositories


• The one and only xtools
• JSON Action Manager
• ExtendScript ES5 Shims
• ExtendScript ES6 Shims
• TypeScript Adobe Scripting
• Photoshop TypeScript Declaration
• Eslint ExtendScript Plugin
• ExtendScript Babel Plugin
• ExtendScriptr
• Get Adobe Generator
• Node jsxbin
• Generator Core wrapper
• ExtendScript and Photoshop Scripting on GitHub
• Tom Ruark on GitHub
• Descriptor Info and Parse Action Descriptor Code by Javeir Aroche
• Clean SL by Tomas Šinkūnas
• Action Manager Humanizer by Jaroslav Bereza

Books
• Power, Speed & Automation with Adobe Photoshop (2012), by Geoff Scott and Jeffrey
Tranberry
• InDesign automatisieren: Keine Angst vor Skripting, GREP & Co (2011, German edition) by
Gregor Fellenz
• InDesign CS5 Scripting (2011), by Grant Gamble
• Scripting InDesign CS3/4 with JavaScript (2009) by Peter Kahrel
• Adobe Scripting: Your visual blueprint for scripting in Photoshop and Illustrator (2003) by
Chandler McWilliams
Acknowledgments
Writing four hundred pages on Photoshop Scripting proved to be crazy, very much like I suspected
when I started too many months ago. I have a wide array of people to thank for the support they
have provided in assorted ways.
My dear friend and experienced Adobe Scripter Sandra Voelker has been the book’s technical editor:
her suggestions and bird-eye view have greatly shaped both the content and structure. Her annotated
.pdfs made me rewrite entire chapters – I tried to resist, but her commentary was ruthlessly spot-on!
Sid Palas has volunteered a full round of proofreading, and Ilaria Meliconi has contributed with her
remarkable publishing experience.
I want to thank Jeffrey Tranberry for the Foreword, and Thomas Ruark for his help: I’m really
honored! Their work has meant a lot to me over the years, as it should to anyone who’s ever been
in this business. Also from Adobe, I’d like to thank Eric Ching, Jesper Storm Bache, Erin Finnegan,
Ash Ryan Arnwine, Kerri Shotts, Alexandru Costin, and Ari Fuchs.
Cameron McEfee shared unfiltered thoughts and his points of view as a fellow developer each time
I needed them, for which I’m deeply grateful. My friends and business partners Roberto Bigano and
Fixel Algorithms helped along the way countless times – thank you, guys!
When I made my early steps in Photoshop Scripting, xBytor was the #1 reference. To date, I still
can’t figure out how one person can contribute so much to a community; each time I’ve needed
help, he’s kindly shared snippets and opinions. Among the elite of the past era, I must mention Paul
Riggott and Michael Hale, who have played a substantial role in the Forums.
I owe a lot to Derrick Barth, Jaroslav Bereza and other people in Adobe’s third-party developers
community: among them in random order, Michel Mariani, Sergey Kritskiy, Vasily Hall, Joel
Galleran, David Hartman, Zack Lovatt, Trevor Morris, Christoph Pfaffenbichler, Justin Taylor,
Tomas Sinkunas, Pedro Marquez, Kris Coppetiers, Marc Autret, Martinho da Gloria, Peter Karhel,
Gregor Fellenz, Dirk Becker, Matias Kiviniemi, Olav Kvern, Anastasiy Safari, Fabian Zirfas, Gabe
Harbs, Karol Rzadczyk, Pravdomil, Rico Holmes, Thomas Zagler, Vlad Vladila, Stephane Baril, Jake
Brown, Javier Aroche, Loic Aigon, Jacob Rus.
My dear friend Daniele Di Stanio has never failed once to support the erratic mood of yours truly
over the years, as well as my partner in crime Giuliana Cromaline Abbiati.
A special thank (the kind of which is mixed with dark, north-Italian curses) to the large team of
craftsmen who, in between 2014 and 2019, has been nominally working in my house’s restoration:
without the dark-hole money-sucking bottomless pit that your nearly-zero-satisfaction-providing
work has created in my otherwise averagely quiet family’s life, I wouldn’t have had such an urgent
drive to write this book.
Lastly, but not leastly, Elena and Anita: who always gift me with purpose and magic.
Bio
Davide Barranca – Author
Davide has studied Applied Color Theory with Dan Margulis, and he’s a Photoshop retoucher by
trade since the early 2000s. Teaching himself scripting to speed up and customize image processing
routines for his job, he quickly got into the extensibility rabbit hole. Over the last 10 years, he has
regularly blogged about it, and published two books: Adobe Photoshop HTML Panels Development,
and The Ultimate Guide to Native Installers and Automated Build Systems, which have been very
well received by the developers’ community and Adobe itself. Davide is also collaborating with
a small team of smart people worldwide on several Photoshop extensions, that retouchers and
photographers alike seem to enjoy very much.

Sandra Voelker – Technical Editor


Sandra started her Photoshop scripting journey in 2002 while a digital artist working at a large game
studio. Realizing the power of automation, she quickly became an evangelist for Photoshop Scripting,
defining a new role at the studio called ‘Technical Artist’. While her scripted tools were hard at work
in studios around the world, she led training seminars exposing other creative professionals to the
power of automation.
In 2012 she decided to branch out on her own, forming Zetta Graphics LLC, a small company that
creates automated workflow solutions for a variety of customers. Zetta’s clients started mostly with
production studios, but quickly branched out to advertising agencies, photography studios, even such
unexpected places such as the US Department of Transportation, and human prosthetic technology!
In 2016 she answered the call of the mothership and spent 18 months contracting with the Adobe
Photoshop team, helping them update their own automated QA tools, and adding some new scripted
features to Photoshop CC2015.5 and CC2017. Now you can find her writing more creative workflow
solutions at Zetta, and partnering with professionals around the world.

You might also like