Tuesday, August 5, 2008

Photoshop scripting with Python

Photoshop natively supports scripting with AppleScript, JavaScript and VBScript. While Python is notably absent from that list, it can still be used to automate nearly anything in Photoshop. This is thanks to the extensive COM interface Photoshop provides.

The methods here are similar to those used in my GDC 2008 Python lecture, about driving 3ds Max via Python. You start by dispatching the Photoshop COM server, using Python as the client:

import win32com.client
psApp = win32com.client.Dispatch("Photoshop.Application")
This connects to your already-opened Photoshop session, or opens one if none are running. The root COM object is then assigned to psApp, and you're ready to do some cool stuff. Here's a quick example:
psApp.Open(r"D:\temp\blah.psd")         # Opens a PSD file
doc = psApp.Application.ActiveDocument # Get active document object
layer = doc.ArtLayers[0] # Get the bottom-most layer
layer.AdjustBrightnessContrast(20,-15) # Bright +20, Contrast -15
doc.Save() # Save the modified PSD
Here's a more complex example. This script recursively scans a folder for PSD files, exporting various textures contained inside. One PSD can have specifically-named Layer Groups, each of which is written to a separate PNG file with a specific suffix. If a Group contains several layers, they're flattened when exported, allowing you to keep all your layered effects intact in the PSD.

In the example below, a group named "diffuse" is exported as "psdname_D.png", the "normal" group as "psdname_N.png", and so on. The exportType dictionary determines the name/suffix pairs.
# Recursively scans a folder (psdRoot) for Photoshop PSD files.
# For each, exports various 24-bit PNG textures based on layer
# groups found in the PSD.
# Requires the Win32 Extensions:
# http://python.net/crew/mhammond/win32/

import win32com.client
import os

# Change to match your root folder
psdRoot = r'C:\ArtFiles\PSD'

# Map of layer group names and the suffixes to use when exporting
exportTypes = {'diffuse':'_D', 'normal':'_N', 'specular':'_S'}

if (__name__ == '__main__'):
# COM dispatch for Photoshop
psApp = win32com.client.Dispatch('Photoshop.Application')

# Photoshop actually exposes several different COM interfaces,
# including one specifically for classes defining export options.
options = win32com.client.Dispatch('Photoshop.ExportOptionsSaveForWeb')
options.Format = 13 # PNG
options.PNG8 = False # Sets it to PNG-24 bit

# Get all PSDs under root dir
psdFiles = []

for root, dir, files in os.walk(psdRoot):
for thisFile in files:
if (thisFile.lower().endswith('.psd')):
fullFilename = os.path.join(root, thisFile)
psdFiles.append(fullFilename)

# Loop through PSDs we found
for psdFile in psdFiles:
doc = psApp.Open(psdFile)
layerSets = doc.LayerSets

if (len(layerSets) > 0):
# First hide all root-level layers
for layer in doc.Layers:
layer.Visible = False
# ... and layerSets
for layerSet in layerSets:
layerSet.Visible = False

# Loop through each LayerSet (aka Group)
for layerSet in layerSets:
lsName = layerSet.Name.lower()

if (lsName in exportTypes):
layerSet.Visible = True # make visible again

# Make our export filename
pngFile = os.path.splitext(psdFile)[0] + exportTypes[lsName] + '.png'

# If PNG exists but older than PSD, delete it.
if (os.path.exists(pngFile)):
psdTime = os.stat(psdFile)[8]
pngTime = os.stat(pngFile)[8]

if (psdTime > pngTime):
os.remove(pngFile)

# Export PNG for this layer Group
if (not os.path.exists(pngFile)):
doc = psApp.Open(psdFile)
doc.Export(ExportIn=pngFile, ExportAs=2, Options=options)
print 'exporting:', pngFile
else:
print 'skipping newer file:', psdFile

# Make LayerSet invisible again
layerSet.Visible = False

# Close PSD without saving
doc.Close(2)
It only exports when the PNG is missing or older than the PSD. This makes it good for running a batch texture export on your project's entire texture tree.

Here is a ZIP containing the above script and a sample PSD file to try it on: exportTextureLayers.zip (143 KB)

I imagine you can do all of the above with the native Photoshop scripting. I just think it's cool being able to use Python instead of rooting through a language I'm less familiar with. Dinosaurs were roaming the earth the last time I tried anything in VB.

If you dig this, I'd recommend reading the Scripting Guide and CS3 VBScript Reference found in the Adobe Photoshop Developer Center. While the above wasn't VBScript, the COM interface we used is nearly identical.

28 comments:

Tom said...

Killer!
Just discovering Python and COM interfaces. wrapping my head around these things. Your blog has been a great insight into creating great studio tools!

Martin Michel said...

In Mac OS X you can also script Photoshop with Python by using appscript:

http://appscript.sourceforge.net/

It's great for people who don't like the AppleScript syntax, but who do like the power of automation.

nico said...

Not sure if anyone is still reading this, but while I get the basic stuff to work (create new doc, add layers, change names, types, etc.), I can't get a selection to work. I get an error telling me that either I need a one dimensional array, or that my argument is illegal.
This won't work: myDoc.Selection.Select([...8-number array to define selection area...])

Currently at work, so I will also try at home in case something is wrong here, but has anyone gotten this to work?

Thanks.

nico said...

After a day of failing to get it to work, I found comtypes (http://sourceforge.net/projects/comtypes/) which solved the problem of passing a multi dimensional array for selection. I still have to test the rest, but that's a start....

Just in case it can help others.

Adam Pletcher said...

Great, thanks for that update, nico!

We found that same issue with the Photoshop Select method, and also worked around it using comtypes. Glad to hear we weren't alone.

nico said...

Me again. I managed to get a lot done with some basic Python calls to automate document creation in Photoshop.

My current problem: when I run the script from IDLE, everything works like a charm, and everything I tell Photoshop to do is happening.

When I call my script from inside Photoshop, it just stops for no appearant reason. The Python code is called through an Action that calls a one-liner Javascript. I use this method for my other scripts, and I never got a problem.

My question: What would be a good way to see what errors are happening when running my script inside Photoshop.

I tried a few write statements that are saved to a text file, but I cannot find a smart way to check for things (since I don't know what to check for). I'm not a programmer, so while the whole try/except statement business makes some sense, I just have no idea how to approach this. If it were throwing errors in IDLE, that would be great, but the fact that it works in one environment and not another leaves me clueless.

(I'm working with comtypes, not win32.com).

Cheers. nico.

nico said...

I found the problem:

My python code is saved with the extension .pyw to prevent the console from popping up every time an artist would use the tool.

I was suggested to change the extension back to .py just to see if the console would show anything interesting. After doing so and calling the script from Photoshop, it worked fine.

Seeing all the normal output in the console made me think that maybe the verbose feedback through my print statements was too much. I commented them out, saved the file back to .pyw, and the script worked fine.

I think this comes maybe from the limitation of how Javascript is dealing with output to a console. I will try to uncomment the print statements one at a time to see how much I can print before it crashes again (my other scripts also had print statements to the console, and they work fine).

Lesson learned: when trying out the tools inside Photoshop, log to a file instead of the console.

website design nyc said...

nice post

seo expert said...
This post has been removed by a blog administrator.
matt said...

Very nice. However it doesn't seem as though I can use dir() or help() on objects or methods?

nico said...

Matt, are you getting any errors in your shell? While some of the info is not as verbose as your typical Python help, I can do a dir() help() on my objects.

Could you perhaps tell us what you get, and what setup you have?

Cheers.

matt said...

sure. I'm on winXP using the win32com.client module, performing the following:

import win32com.client
psApp = win32com.client.Dispatch("Photoshop.Application")
print help(psApp)

I would expect to get the methods of the Photoshop.Application class but I'm getting the class instance of the com module which is pretty much:

__abs__
__add__
__and__
etc ...

what am I missing here. I'd like to find a way to get the members of a particular class such as:

dir(layer)
def AdjustBrightnessContrast(...)

etc ...

Thanks for the reply,

Adam Pletcher said...

COM objects don't generally offer that kind of reflection. I've never dug too deep into the particulars, but you can find more on that if you search for "early- and late-inding" for COM.

Also have a look at the "makepy" tool in pywin32, that sometimes provides a bit more info with help/dir.

Before you bother with that, have a look at the Adobe Photoshop VBScript Reference doc I linked to in the blog. It has a full list of commands for the COM interface.

nico said...

Matt, as Adam suggested, if you lookup the PS docs and dig out the VBScript reference, you will have all the info you need.

I am not on a machine with Photoshop at the moment, but in terms of interactive feedback in your shell, I think if you have an active document, and a layer, you should be able to get a little help on those during your session.
So, if you have: doc = psApp.Application.ActiveDocument or layer = doc.ArtLayers[0], you should be able to get some help on these from your shell (dir/help). Again, I don't have this handy and it's been a while since I last did something, but definitively keep the VBScript Ref open when you work.

Adam Pletcher said...

Make that "early- and late-binding" in my comment above. I dropped the B somehow...

matt said...

Cool. thanks for keeping the thread alive. I'll look at the pdf reference.

Thanks again

nessus said...

How do I load different versions of Photoshop installed on my machine?
win32com.client.Dispatch("Photoshop.Application")
seems only loads the latest one.

BTW, how do I know the name of a COM program to pass it to Dispatch.

nico said...

While it is passed 3AM here and I am on linux at the moment, I would say that it depends on what/how Photoshop is registered.
I am not sure why you would want to call different versions of PS through your scripts, but the registry entry for Photoshop.Application should only point to one version, if I am correct, and Photoshop.Application.# should call the specified version (# being 1,2,3,etc.).

I'm pretty sure there is a smart way to have COM tell you the names of the app you want to call, but you can lookup the registry and it will show you all software and how to call them (sorry, not a big Windows user here).

For more serious answers on COM and Python, try to check out the O'Reilly Python for Windows book.

Anonymous said...

is there a way to get the python COM script, to tell Photoshop to run a .jsx script ?

nico said...

I remember seeing something about ActionLists/Descriptors and such in the VBScript Reference for Photoshop. You might want to check it out to see if you can associate your jvscript code to an action in PS, and then call that action from Python. I have never tried it, and I'm on linux at the moment, but I'd be curious to see if this works.

Erik said...

found this great article:

http://www.kirupa.com/motiongraphics/scripting5.html

python + comtypes + scriplistener
= long route to automate photoshop, so it can call a jsx from python:

def runJSX ( aFilePath ):
id60 = psApp.stringIDToTypeID ( "AdobeScriptAutomation Scripts" )
desc12 = comtypes.client.CreateObject('Photoshop.ActionDescriptor')
id61 = psApp.charIDToTypeID( "jsCt" )
desc12.putPath( id61, aFilePath )
id62 = psApp.charIDToTypeID( "jsMs" )
desc12.putString( id62, "null" )
psApp.executeAction( id60, desc12, 2 )

psApp = comtypes.client.CreateObject('Photoshop.Application')
runJSX ( "C:\\Program Files\\Adobe\\Adobe Photoshop CS3\\Scripting Guide\\Sample Scripts\\JavaScript\\OpenDocument.jsx" )

nico said...

Neat... Actually, to the previous poster: when you want to achieve something that is not directly doable (i.e. no code equivalency), using the script listener is definitively a valuable tool; I used this a few times to create methods that were not found in VBScript. Thanks for the reminder Erik.

MattiG said...

is there a way to call win32 from Maya? Can you point me to some documentation? :) Very appreciated!

P.S.: Great article, Adam!

Adam Pletcher said...

@MattiG:
I'm not a big Maya user, but there's this thread over at TAO that seems to have some info on that. Specifically the post half-way down the first page from WHW that starts "It would appear...".

There are now x64 builds of pywin32 available now, so it may even work with Maya 64-bit.

MattiG said...

@Adam thx a lot! Gonna have a shot at that :) Tech-Artists is a geat ressource!

Anonymous said...

hi..i want to open feather dialog box in photoshop but with the use of script not directly.ok.plz

Eric said...

Nice posting! psd development

Sunny Carter said...

Hi there,

Thanks for this post - I've used it as the base for a script to recolor PSD files.

It works fine using Python 2.6 but fails when I've upgraded to Python 3.0. I have installed pywin32-214.win32-py2.6.exe for Python 2.6 and pywin32-214.win32-py3.0.exe for use with Python 3.0

Subset of the script:
sorry if indentation wrong
import win32com.client
psApp = win32com.client.Dispatch('Photoshop.Application')
doc = psApp.Open(os.path.abspath(psd_image.psd_filename))
# Here I do the image manipulation which I haven't #included here as it is irrelevant (and working fine -
# calling Photoshop and making it do stuff)
psd_save_options = win32com.client.Dispatch("Photoshop.PNGSaveOptions")
psd_save_options.Interlaced = False
print("Save as %s" % (os.path.join(output_directory, psd_image.final_filename)))
doc.SaveAs(os.path.join(output_directory, psd_image.final_filename),
psd_save_options,
True,
2) # lower case extension



Exception hit:

Unexpected error detail:
Traceback (most recent call last):
File "C:\brander\brander.py", line 2519, in reconstructPSDImageFile psd_save_options.Interlaced = False
File "C:\Program Files\Python31\lib\site-packages\win32com\client\dynamic.py", line 550, in __setattr__ self._oleobj_.Invoke(entry.dispid, 0, invoke_type, 0, value)
pywintypes.com_error: (-2147352567, 'Exception occurred.', (0, 'Adobe Photoshop' , 'Boolean value expected', None, 0, -2147220276), None)




Does anyone have any ideas how to solve? The problem must be with pywin32. I've tried using True, "False", 0 and 1 for the Boolean value and none work. I repeat that this worked fine on Python 2.6 but I need to upgrade to 3.0 for other reasons. I've also tried Python 3.1.1 and relevant pywin32 module with the same results. Any call using True or False across the API seems to fail.