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 Photoshop CS5 Scripting Guide and Photoshop CS5 VBScript Reference found in the Adobe Photoshop Developer Center. While the above wasn't VBScript, the COM interface we used is nearly identical.

59 comments:

Anonymous 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!

Anonymous 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.

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.

Anonymous said...

nice post

Anonymous said...
This comment 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?

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.

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

呱呱 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.

Anonymous said...

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

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" )

mistermatti 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.

mistermatti 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

Anonymous 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.

Marco Bauriedel said...

This is a great base to get into controlling Photoshop through Python.
I`ve only one problem coming up, which is the question how to move a Layer down in a PSD File.

I`ve found the commands that should do it:

Move(ApplicationObject,InsertionLocation) or duplicate(ApplicationObject,InsertionLocation)

in combination with

PsElementPlacement

0 (psPlaceInside)
1 (psPlaceAtBeginning)
2 (psPlaceAtEnd)
3 (psPlaceBefore)
4 (psPlaceAfter)

The object’s position in the Layers
palette.

This example works as far that the active layer is duplicated, but not into another file or another layer position:

layer = doc.ArtLayers[0]
layer.duplicate(doc, ElementPlacement.PLACEATBEGINNING)


Python doesn`t recognize the
ElementPlacement flag/command.

I tried to "load" it with
psPlacement = win32com.client.Dispatch("Photoshop.psElementPlacement")
and
psPlacement = win32com.client.Dispatch("Photoshop.ElementPlacement")
which didn`t work.

Any hint on how to get the ElementPlacement working is highly
appreciated. Thanks again Adam for posting this great intro to Photoshop scripting with Python.

mistermatti said...

Marco, have you had a look at this?

http://tech-artists.org/forum/showthread.php?t=634

Just had a quick look but it might help you?

Marco Bauriedel said...

Hi MattiG,

I haven`t looked in detail over there, but just found out that my mistake was a simple matter of upper case/lower case-typing .

instead of

layerNew = doc.activeLayer
layerNew.Move (doc, 2)

I tried layerNew.move (doc, 2).

(2 stands for moving the layer below the bottom layer)


Everything works like a charm now.
Thanks alot for the link and your fast reply.

automotive hand tools said...

nice description... A co- worker said me that Photoshop with Python is a good combination... he recommended me your blog...
Good job

Adam Pletcher said...

Apologies to "Send flowers to Poland", it seems I accidentally deleted your comment. :/

I'm glad you enjoy the blog, thanks for the note!

etyhhdfgh 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!

Unknown said...

ah,, nice,

could this code be modified to work with illustrator(*.ai) files?

Adam Pletcher said...

@eugene: After Photoshop loads an AI file you can work with it just like any other image.

If you mean scripting inside Illustrator instead of Photoshop, then yes you can do that as well. Illustrator also has a scripting API, here's the Adobe documentation:
Illustrator Scripting

Similar to Photoshop, the VBScript documents are probably the closest match to the COM APIs you'd use from Python.

Unknown said...

Thanks for the reply,

I need something similar to below for illustrator and wonder how you knew that Photoshop.ExportOptionsSaveForWeb exists.


options = win32com.client.Dispatch('Photoshop.ExportOptionsSaveForWeb')

Adam Pletcher said...

@eugene:
The ExportOptionsSaveForWeb method is listed in the Photoshop CS5 VBScript Reference document. For Illustrator CS5, the document you want is Illustrator CS5 VBScript Reference. If you're using a release earlier than CS5, grab the right one from the Illustrator Scripting page I listed above.

While those docs are for VBScript, VB methods are essentially COM methods, and the Python techniques I showed in this post should work. You just need to use the Illustrator-specific methods listed in its scripting API doc.

Unknown said...

ah thanks.

I've looked at their api and
options = win32com.client.Dispatch('Illustrator.ExportOptionsPNG24')

is what I need and I need to set
artBoardClipping to true so that image size doesn't get cropped.

but it seems win32com doesn't seem to support this option changed.
I'd like to look at the python source code so that maybe I can add the option. any rough guess where I should look?

options2 = win32com.client.gencache.EnsureDispatch('Illustrator.ExportOptionsPNG24')
>>> dir(options2)
['CLSID', 'SetMatteColor', 'SetObjectValue', '_ApplyTypes_', '__call__', '__doc__', '__eq__', '__getattr__', '__init__', '__int__', '__module__', '__ne__', '__repr__', '__setattr__', '__str__', '__unicode__', '_get_good_object_', '_get_good_single_object_', '_oleobj_', '_prop_map_get_', '_prop_map_put_', 'coclass_clsid']
>>>

Unknown said...

http://cssdk.host.adobe.com/sdk/1.0/docs/WebHelp/references/csawlib/com/adobe/illustrator/ExportOptionsPNG24.html

There artBoardClipping is listed but not listed when dir()

Unknown said...

almost found..
under $PythonHome/Lib/site-packes/win32com/gen_py/A3A65...../ExportOptionsPNG24.py

just need to modify the source code and find out how to apply (maybe need convert it to *.pyc file?) so that I can use the modified version

Adam Pletcher said...

I doubt that approach will work. COM methods are defined by the application itself (Illustrator), not by a Python file sitting somewhere. The Win32 Python extensions just offer a way to "late-bind" to those methods from Python. Which is also why dir() does not show them.

I would recommend asking how to use specific methods on Adobe's Illustrator Scripting Forum.

Unknown said...

@adam

Actually, it did work.

def SetArtBoardClipping(self, arg0=defaultUnnamedArg):
return self._oleobj_.InvokeTypes(1883324995, LCID, 8, (24, 0), ((11, 0),),arg0

is the code that I inserted into the file.

Exactly, COM APIs are defined by illustrator, and python didn't have function to call it, and I just inserted a function.


I'm stuck with another problem though, illustrator has something called objects(opposed to layers, and I didn't know about it until today). I need to convert them to layers programmatically.

Looks like pywin32 lacks many apis that are available to officially supported photoshop/illustrator script language.

Although, python can do most of what I need with illustrator, looks like i'll need some other script's help to complete the task :(

Adam Pletcher said...

Updated a couple Adobe links that had gone dead.

xxvii-xxiv-xiv said...

Saved my day. Thank you :).

Chris V said...

Great info, thanks!...but I have a question: How do you access the python script from within PS? I read that one could create a simple javascript to call the python script, but I'm not sure how it's done, I know little about javascript. I've tried
System.Diagnostics.Process.Start("python.exe", "C:\\psTest.py");
but it doesn't seem to do anything.
Also, is there a way to create a custom menu in PS where I can place my scripts? I do this in Maya for our company tools, and ideally I'd like to maintain a similar setup in PS.

Chris V said...

Nico, thanks for the help, that was almost too easy. Knowledge is king! Thanks again.

Francis Vega said...

Thanks nico for the js tip :D
How I could pass params?, anything like:

var checkout = new File("/sandbox/photoshop/pyCheckOut.py 1024 768 AnyName");
checkout.execute();

Thanks!!

Daniel Serodio said...
This comment has been removed by the author.
Daniel Serodio said...

Great post, it really helped me getting started with PyCOM Photoshop scripting.

How can I access Photoshop constants like Units and TypeUnits? I tried win32com.client.constants.Units but I get an AttributeError...

Thanks.

Captain Denis Super Awesome said...

All this has worked amazingly for me.. except for some damn reason the application.documents object doesn't respond to any of it's documented methods or properties except for .parent (and although not documented the .count) Anyone else run into this?

Chris V said...

I'm having the same selection problem that Nico solved using comtypes, is there a fix for win32com yet? And if not, can someone explain the comtypes solution? Is it a replacement for win32com?

当当 said...

this is a life saver post, thank you guys. But I get a question, when I look into the documentation of photoshop_cs5_vbscript_ref.pdf I found there's only ExportDocument function there but no Export function.

How do you guys figured it out this ?

Adam Pletcher said...

@当当
That is either a typo in the VBScript documentation, or perhaps a difference between the VBScript and COM interfaces.

The "Export" function on document objects is still there in Photoshop CS5 and callable from COM/Python, and there is no function named "ExportDocument".

Jay Goodman said...

Wow, really useful. Thank you for the post!

Jo said...

Hi, I'm not sure if anyone reads this post anymore but I have a question regarding python interfaces in photoshop.

My problem is that it seems to slow down Photoshop a lot. I have a simple interface generated in Flash that call some jsx script themselves calling a PyQt script using com to interact with Photoshop. I'm not entirely sure it is the simplest way to go, but it does the trick. Only thing is, for some users, it slows down the Photoshop interface.
Any idea where it might come from ? Any suggestion would be appreciated ! Thanks

Adam Pletcher said...

I still read this! :)
I don't have experience with that exact kind of setup (JSX->Py->COM->PS), but...

If you have that kind of circular connection in the tools, I'd suspect it's waiting around for the last "message" to be processed, which is starving the UI and making it unresponsive. Hard to say without seeing the exact code, however.

I would suggest adding some timing code and profiling it, see where the time is going.

Jo said...

Thanks for the reply :)

Well the exact code is a lot to read.. I actually didn't write it, so I have to dig in it before I can get out some simplified code.

But I'll try the timer thing, see what it gives. Thanks for the tip !

Jo said...

I didn't see what Nico said.
Well actually, it's not really my code that is the problem. The real issue, is that it slows down the actions in Photoshop itself. For example, cancelling an action will not be immediate. Or switching from one layer to another will take 1 second instead of being instantaneous.

Evan said...

I had been wondering about this for a while and since this helped me so much I figured I would share the answer no matter how basic it may be.

When I tried to make all this work I had some troubles. Not only did I have multiple versions of photoshop but I had multiple bit versions (CS5 x86, CS6 x86/x64, CC 2014. I knew I had to use win32.client.Dispatch(Adobe.Application) to connect to the app and open a document but I could not connect to the Adobe.ActionReference or Adobe.ActionDescriptor

I tried to find out why my Application returned a timeout error and it seems it revolved around the Generic call to the Photoshop COM. I needed to specify the application version...but what was the number?

After much searching I found in the Windows Registry all Photoshop call actions and Versions to use.

If you want to make an Action Manager interaction without photoshop timeing out specify the version

Photoshop Application COMM Dispatch Commands Action Manager
(will use the recent version of photoshop installed)

win32.client.Dispatch(
Photoshop.Application.80
Photoshop.ActionDescriptor.80
Photoshop.ActionReference.80
)

For a full list of commands specific to you go to Start->(Search)regedit -> Locate Photoshop.X registry details

Rustomji said...

Evan, your post was enormously helpful, thank you!

Anonymous said...

amazing post, thank you a billion !!

The Smart Digital said...
This comment has been removed by a blog administrator.
Anonymous said...

It's 10 years later since the tutorial has been written and there's not much more sources to look for knowledge about this topic ;) Thanks a lot, finally managed to understand the SaveAs options objects.

Anonymous said...

how to run .ATN file using phthon?

suresh said...
This comment has been removed by the author.
Best Python Tutorials said...

excellent tutorial I am searching for long days python Tutorials

BRISKLOGIC IT SOFTWARE COMPANY said...

Hi,
Greetings !

We are brisklogic (www.brisklogic.co).
Dynamo's of digitization” with a remarkable heritage.
Build digital products faster, smarter, and scalable.
Brisklogic is Solutions for a small digital planet.
Develop your business faster with less efforts with Us.
Let’s Make Things Better.
HELLO@BRISKLOGIC.CO


Sr. Business Manager
Thanks & Regards,