Python and the Photoshop Script listener

This little guide is written for Photoshop Cs5. Other versions of Photoshop may have slightly different folder setups and listener output locations. 

As discussed in a few of my earlier posts, Python is a great choice for scripting in Photoshop when coupled with a good COM interface and a desire to make monkeywork just go away. But as you are busy putting those monkeys to work it is inevitable that you will come up against a command or two that's not made available in the regular Photoshop scripting documents.

CRISIS! The monkeys go on strike. But wait, there is hope!


Introducing awesomeness...
The scripting listener is a brilliant plugin for Photoshop. Using it gives you access to many commands that are not included in the Photoshop scripting reference.

While active (that is, present in the Automate folder) it spits out a JS and VB log of all all your actions in Photoshop. For the purposes of this guide we are only really interested  in the VB log. Getting useful Python code from it can take a little bit of tinkering with the output, but once you get the hang of it you can make those monkeys do anything.

Lets get on with it! To install the plugin is simple enough.
  • To find it go to your Photoshop installation directory and find <PS_directory>\Scripting\Utilities\    
  • In this you will find a file called ScriptListener.8li 
  • Make a copy of this file and put it in <PS_directory>\Plug-Ins\Automate\
  • Done! 
  • Restart Photoshop and do something!
Once you do something you will notice that a couple of files have appeared on your desktop. In earlier versions of Photoshop these files may appear in C:\. If you open the ScriptingListenerVB.log you will see a mess of DIM idNm idNm blah blah etc. Don't worry, it will (hopefully) all make sense. 

So now that its installed, here are a couple of hints at how to maximize the readability of the subsequent output. 
  •  Record a single, deliberate action at a time. This will make it really obvious as to what the output is trying to do. 
  • Put specific values into any requested options so that you can easily spot where they appear in the output. 
  • Nuke the output file before you start recording and directly after you have got what you need from it. This saves having to sift through miles of code to get to what you want.  
Now! For the purposes of this guide, I'm going to create a Python script that selects a specific color across an entire image. This is easy enough to do in Photoshop by hand, but can be a very time consuming task for a script depending on the approach taken, and is not exposed as a regular scripting function. Either way, its a good example.

Right on!
It may seem like cheating, but the way to get the code to do the action, is just to do the action! In the image I have chosen there is a very specific color I am aiming at, 255 green, so the output will be legible. So now I nuke the listener output, and do my action:


Looking at the ScriptingListenerVB.log file now, I can see the results:

REM =======================================================
DIM objApp
SET objApp = CreateObject("Photoshop.Application")
REM Use dialog mode 3 for show no dialogs
DIM dialogMode
dialogMode = 3
DIM idClrR
idClrR = objApp.CharIDToTypeID( "ClrR" )
    DIM desc71
    SET desc71 = CreateObject( "Photoshop.ActionDescriptor" )
    DIM idFzns
    idFzns = objApp.CharIDToTypeID( "Fzns" )
    Call desc71.PutInteger( idFzns, 0 )
    DIM idMnm
    idMnm = objApp.CharIDToTypeID( "Mnm " )
        DIM desc72
        SET desc72 = CreateObject( "Photoshop.ActionDescriptor" )
        DIM idLmnc
        idLmnc = objApp.CharIDToTypeID( "Lmnc" )
        Call desc72.PutDouble( idLmnc, 87.840000 )
        DIM idA
        idA = objApp.CharIDToTypeID( "A   " )
        Call desc72.PutDouble( idA, -79.040000 )
        DIM idB
        idB = objApp.CharIDToTypeID( "B   " )
        Call desc72.PutDouble( idB, 79.380000 )
    DIM idLbCl
    idLbCl = objApp.CharIDToTypeID( "LbCl" )
    Call desc71.PutObject( idMnm, idLbCl, desc72 )
    DIM idMxm
    idMxm = objApp.CharIDToTypeID( "Mxm " )
        DIM desc73
        SET desc73 = CreateObject( "Photoshop.ActionDescriptor" )
        DIM idLmnc
        idLmnc = objApp.CharIDToTypeID( "Lmnc" )
        Call desc73.PutDouble( idLmnc, 87.840000 )
        DIM idA
        idA = objApp.CharIDToTypeID( "A   " )
        Call desc73.PutDouble( idA, -79.040000 )
        DIM idB
        idB = objApp.CharIDToTypeID( "B   " )
        Call desc73.PutDouble( idB, 79.380000 )
    DIM idLbCl
    idLbCl = objApp.CharIDToTypeID( "LbCl" )
    Call desc71.PutObject( idMxm, idLbCl, desc73 )
    DIM idcolorModel
    idcolorModel = objApp.StringIDToTypeID( "colorModel" )
    Call desc71.PutInteger( idcolorModel, 0 )
Call objApp.ExecuteAction( idClrR, desc71, dialogMode )
There is a lot going on there, but I can see some sort of sense in it.  The first six lines are calling the application and setting the dialog mode to none. We don't have to worry too much about that at this point.

Lines 11-13 are where the 'fuzziness' value of the color range is defined. It starts to get interesting with line 15. If you look quickly at line 15 and line 31, you will notice that two colors are defined, first a minimum, or "Mnm  " on line 15, and then a maximum, or "Mxm  " on line 31.

From line 16 to line 26 the desc72 values are assigned. Although apparently bearing no relation to my expected single value, lines 20, 23 and 26 are defining 255 green in Lab color. (87.84, -79.04, 79.38). Worth knowing! As I only defined one color for my range, so the same values are expressed for the maximum in desc72 between lines 32 and 42.

The action itself is put together gradually using all these smaller packages. The entire minimum color is assembled on line 29:

Call desc71.PutObject( idMnm, idLbCl, desc72 )


Pretty much saying "Minimum range is a lab color of these values" and line 45:

Call desc71.PutObject( idMxm, idLbCl, desc73 )


Which says "Maximum range is a lab color of these values".

Finally, the actual action is performed on line 49.

objApp.ExecuteAction( idClrR, desc71, dialogMode )


What a heap of stuff going on! If you have made it this far, awesome. We will make those monkeys work yet. The beauty of Python is that right off the bat we can remove a whole heap of clutter, which will increase legibility from the word go. With the previous notes in mind, here is a very quick translation of the VB script outpt into Python! I've left the variable names the same so that a comparison can be made easily, although I will change them later for readability.

#Our access to the photoshop application
#Get this COM module from:
#http://sourceforge.net/projects/comtypes/
import comtypes.client

objApp = comtypes.client.CreateObject('Photoshop.Application')

#Set dialog mode to none
dialogMode = 3

idClrR = objApp.CharIDToTypeID( "ClrR" )
desc71 = comtypes.client.CreateObject( "Photoshop.ActionDescriptor" )

#Fuzziness value
idFzns = objApp.CharIDToTypeID( "Fzns" )
desc71.PutInteger( idFzns, 0 )

#Define the minimum colours
idMnm = objApp.CharIDToTypeID( "Mnm " )

#Create an action descriptor
desc72 = comtypes.client.CreateObject( "Photoshop.ActionDescriptor" )

#Luminance value
idLmnc = objApp.CharIDToTypeID( "Lmnc" )
desc72.PutDouble( idLmnc, 87.840000 )

#A and B colors
idA = objApp.CharIDToTypeID( "A   " )
desc72.PutDouble( idA, -79.040000 )
idB = objApp.CharIDToTypeID( "B   " )
desc72.PutDouble( idB, 79.380000 )

#Define the colour type. In this case 'Lab color'
idLbCl = objApp.CharIDToTypeID( "LbCl" )

#Assemble the instructions with the action descriptor.
desc71.PutObject( idMnm, idLbCl, desc72 )

#Define the maximum colors using the same steps.
idMxm = objApp.CharIDToTypeID( "Mxm " )
desc73 = comtypes.client.CreateObject( "Photoshop.ActionDescriptor" )

#L A B
idLmnc = objApp.CharIDToTypeID( "Lmnc" )
desc73.PutDouble( idLmnc, 87.840000 )
idA = objApp.CharIDToTypeID( "A   " )
desc73.PutDouble( idA, -79.040000 )
idB = objApp.CharIDToTypeID( "B   " )
desc73.PutDouble( idB, 79.380000 )

#Color type
idLbCl = objApp.CharIDToTypeID( "LbCl" )

#Assemble action
desc71.PutObject( idMxm, idLbCl, desc73 )

#Call the color model
idcolorModel = objApp.StringIDToTypeID( "colorModel" )
desc71.PutInteger( idcolorModel, 0 )

#Execute the action with 'Defined color, Fuzziness, no dialog
objApp.ExecuteAction( idClrR, desc71, dialogMode )


Executing this script from the console will select all of 255 green in the active document. Awesome! But... still, kinda useless without being able to define specific values. Unless, you know, you always want it to select 255 green. Then you are set! Go forth!

But I'm not satisfied yet. The script works at this point, but is both fairly unreadable and totally inflexible. I want to be able to define colors, and I want to be able to define them MY way. I don't use Lab color, and all my other scripts use RGBA already.

Meanwhile, back in Photoshop...

Quickly setting an RGB value in Photoshop gives me enough info in the script listener on how to define an RGB color rather than Lab. Looking at the results gives me a clear idea on how to replace the Lab color in my script with these values instead.

Because I know what I am looking for I don't bother too much about the rest of the junk. I only want the idRGBC code on line 15 and and all the individual channel value IDs on lines 6, 9 and 12.

DIM idT
    idT = objApp.CharIDToTypeID( "T   " )
        DIM desc83
        SET desc83 = CreateObject( "Photoshop.ActionDescriptor" )
        DIM idRd
        idRd = objApp.CharIDToTypeID( "Rd  " )
        Call desc83.PutDouble( idRd, 0.000000 )
        DIM idGrn
        idGrn = objApp.CharIDToTypeID( "Grn " )
        Call desc83.PutDouble( idGrn, 255.000000 )
        DIM idBl
        idBl = objApp.CharIDToTypeID( "Bl  " )
        Call desc83.PutDouble( idBl, 0.000000 )
    DIM idRGBC
    idRGBC = objApp.CharIDToTypeID( "RGBC" )
    Call desc82.PutObject( idT, idRGBC, desc83 )
Call objApp.ExecuteAction( idsetd, desc82, dialogMode )

Makin' it purdy... 

So lets clean it up and make it a little flexible. We know where the colors are defined and where they are used. I'm going to replace the Lab options with some RGB ones. After some playing around, here is the (somewhat) tidier Python code I came out with, along with RGB options hacked in:

#Our access to the photoshop application
#Get this COM module from:
#http://sourceforge.net/projects/comtypes/
import comtypes.client

#Set dialog mode to none
dialogMode = 3

#Set my maximum and minimum RGB values
minR = 0.0
minG = 255.0
minB = 0.0

maxR = 0.0
maxG = 255.0
maxB = 0.0

#Define the fuzziness
fuzz = 0

#Begin!
psApp = comtypes.client.CreateObject('Photoshop.Application')

selColRange = psApp.CharIDToTypeID( "ClrR" )
selColRangeOptions = comtypes.client.CreateObject( "Photoshop.ActionDescriptor" )
fuzziness = psApp.CharIDToTypeID( "Fzns" )
selColRangeOptions.PutInteger( fuzziness, fuzz )

defineMinCol = psApp.CharIDToTypeID( "Mnm " )

#Create an action descriptor for the minimum color values using RGB
min_color_values = comtypes.client.CreateObject( "Photoshop.ActionDescriptor" )

color_type = psApp.CharIDToTypeID( "RGBC" )

red = psApp.CharIDToTypeID( "Rd  " )
min_color_values.PutDouble( red, minR )

green = psApp.CharIDToTypeID( "Grn " )
min_color_values.PutDouble( green, minG )

blue = psApp.CharIDToTypeID( "Bl  " )
min_color_values.PutDouble( blue, minB )

#Assemble the instructions with the action descriptor.
selColRangeOptions.PutObject( defineMinCol, color_type, min_color_values )

#Define the maximum colors using the same steps.
defineMaxCol = psApp.CharIDToTypeID( "Mxm " )

#Create an action descriptor for the minimum color values using RGB
max_color_values = comtypes.client.CreateObject( "Photoshop.ActionDescriptor" )

red = psApp.CharIDToTypeID( "Rd  " )
max_color_values.PutDouble( red, maxR )

green = psApp.CharIDToTypeID( "Grn " )
max_color_values.PutDouble( green, maxG )

blue = psApp.CharIDToTypeID( "Bl  " )
max_color_values.PutDouble( blue, maxB )

#Assemble the instructions with the action descriptor.
selColRangeOptions.PutObject( defineMaxCol, color_type, max_color_values )

#Call the color model
idcolorModel = psApp.StringIDToTypeID( "colorModel" )
selColRangeOptions.PutInteger( idcolorModel, 0 )

#Execute selectColRange with our options and no dialog
psApp.ExecuteAction( selColRange, selColRangeOptions, dialogMode )

#Done!

Brain... cooling... crispy...

And there we have it. By adding variables up the top that are called throughout the script, it will be pretty straight forward from here to make this into a function and what have you. If you made it this far, kudos!

Just to reiterate, follow these simple guidelines:

  • Delete output file before recording a new action to start with a clean slate. 
  • Use the VB output log, as it most easily translates into Python.
  • Record single actions to increase readability of the output code.
  • Put easily identifiable values in on the Photoshop side and the output will be much more legible. 

Whoo! I selected a color! With CODE!
Although this example might not yield the most ground breaking results, that part is up to you. In a production environment Photoshop scripting can produce some spectacular time savings, as well as automating some mindbogglingly complex tasks demanded of current generation game projects. But apart from all that, its pretty fun watching Photoshop do your work for you. Right on.

-Pete

1 comment:

  1. This is awesome dude! The script listener plugin is super helpful. Thank you!

    ReplyDelete

Comments?