Python Photoshop Automation without win32com- The Example

So here is a working example of what I was talking about in my last blog post- making Photoshop automation possible without needing to use the win32com module.

This specific example is not cross-platform compatible. It relies on getting the Photoshop application through it's registry entry, but the subprocess module is cross platform so it shouldn't be too hard to hammer this into a shape that can be used on Mac and PC. Or just Mac. Or whatever.

Essentially there is no real magic going on here, just lots of string manipulation behind the scenes. Like I said in my last post, the biggest hole in this right now is the lack of feedback from Photoshop, but that's something I'm looking into when I get a few spare minutes here and there.

Here is the example. When you run it, you should come up with something that looks like this:

What an Amazing document. 

And the code! This is a particularly long snippet. Hang in there!

Essentially when the commands are called, like add_new_layerSet(), the arguments are substituted into a string that contains the Javascript equivalent. Things like Booleans have to be modified to be lowercase before they are substituted because of the differences between the languages.

Once the string has been assembled, it is added to the _commands_list object, along with any other commands that will eventually be called.

Finally, when everything is ready, in Python execute_commands() is called, compiling our list of Javascript command strings into a .jsx file, and using Subprocess to call the Photoshop application with the .jsx as an argument. This makes Photoshop execute the actions contained in the script.
  
"""
Photoshop Python->JavaScript interface example.

An example of a Python wrapper for Photoshop's native Javascript. 

-Compiles Javascript commands into a temporary file.
-Calls the Photoshop executable with the jsx as an argument. 

Author: Pete Hanshaw 2013
"""

import subprocess  
import _winreg
import os


class PhotoshopInterface(object):
    
    def __init__(self):
        """
        Set up the application path, temp file location and our commands list. 
        """
        self._psApp = self._find_ps_app()
        self._temp_file = "c:\\temp\\ps_temp_com.jsx"

        # This command list holds all our commands until we are ready to write them
        # to the temp jsx file. 
        self._command_list = []
        
    
    # BEGIN INTERFACE FUNCTIONS
    def _find_ps_app(self):
        """
        Find and return the location of the Photoshop exe from it's registry entry. 
        I do this because its more robust than an assumed absolute path. 
        (although it does assume version...)
        """
        
        # I use Photoshop V12.0 64x. Change this to whatever version you are using. 
        self._psApp_reg_key = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, "SOFTWARE\\Adobe\\Photoshop\\12.0")
        self._psApp_path = _winreg.QueryValueEx(self._psApp_reg_key, 'ApplicationPath')[0] + 'Photoshop.exe'
        return self._psApp_path
    
    
    def _add_command(self, command):
            """
            Generic method to add commands to the list. 
            """
            self._command_list.append(command) 
            
        
    def _compile_commands(self):
        """
        Write the commands to a temporary javascript file. 
        After this is done, empty the command list. 
        """
        if len(self._command_list) > 0:
            with open(self._temp_file, "wb") as f:
                for command in self._command_list:
                    f.write(command)
            f.close()
            self.clean_commands()  
            
        else:
            print "No commands to compile."
            
    
    def clean_commands(self):
        """
        Call this to make sure the command list is clean. 
        """
        self._command_list = []        
    
    
    def execute_commands(self):
        """
        - Call to Compile the commands in the command list. 
        - Call the target subprocess with the compiled javascript as
        an argument. 
        """
        self._compile_commands()
        self._target = self._psApp + " " +  self._temp_file
        
        # This is the magic part. Subprocess calls Photoshop with the jsx as an arg. 
        self._p = subprocess.Popen(self._target)  
        

    # BEGIN COMMAND FUNCTIONS
    
    # Because the wrapper is manipulating large chunks of text, the command
    # functions begin to take up a lot of lines very quickly. 
    def new_document(self, doc_x = None, doc_y = None, doc_name = None):        
            
        """
        Python wrapper for the new document command. Simplified to only accept
        name and resolution. 
        Supported Arguments are:
        doc_name : String
        doc_x : Int
        doc_y : Int
        """
        if (doc_x != None):
            self._doc_x = doc_x
        else:
            self._doc_x = 512
        
        if (doc_y != None):
            self._doc_y = doc_y
        else:
            self._doc_y = 512
        
        if (doc_name != None):
            self._doc_name = doc_name
        else:
            self._doc_name = "new_document"
        
        # Hard set values
        self._res = 72
        self._mode = " NewDocumentMode.RGB"
        self._init_fill = "DocumentFill.WHITE"
        self._PaR = 1.0
        self._BpC = "BitsPerChannelType.EIGHT"
        
        self.command = (
            """
            doc = app.documents.add(%s, %s, %s, '%s', %s, %s, %s, %s);
            """% (self._doc_x, self._doc_y,
                  self._res, self._doc_name,
                  self._mode, self._init_fill,
                  self._PaR, self._BpC
                  )
            )
        self._add_command(self.command)    
        
        
    def add_new_layerSet(self, Name = None):
        """
        Adds a new layer set to the currently active document. 
        Supported Arguments are:
        Name : String
        """
        
        if (Name != None):
            self._name = Name
        else:
            self._name = 'Group'
                
        self.command = (
            """
            var docRef = activeDocument;
            var newLayerSet = docRef.layerSets.add();
            docRef.activeLayer.name = '%s';
            """ % (
                    self._name
                    )
        )
                
        self._add_command(self.command)          
        
    
    # By default layer sets are transparent. This one looks a little more complicated
    # Because it has the option of adding a fill and a parent.
    def add_new_layer(self, Name = None, Parent=None, Fill = None):
        """
        Add a new layer. 
        Supported arguments are:
        -Name : String
        -Parent: String
        -Fill : Tuple
        """
            
        if (Name != None):
            self._name = Name
        else:
            self._name = 'New_Layer'       

        if Parent != None:
            self._parent = (
            """
            parentSet = docRef.layerSets['%s'];
            """ % (Parent)
            )
            
            self._move = (
            """
            thisLayer.move(parentSet, ElementPlacement.INSIDE);
            """)
            
        else:
            self._parent = ""
            self._move = ""

        if Fill != None:
            self._fill = (
                """
                var fillColor = new SolidColor();
                fillColor.rgb.red = %s;
                fillColor.rgb.green = %s;
                fillColor.rgb.blue = %s;
                app.activeDocument.selection.fill( fillColor, ColorBlendMode.NORMAL, 100, false );
                """ % (Fill[0], Fill[1], Fill[2]))
        else:
            self._fill = ""

        self.command = (
            """
            var parentSet;
            
            %s
            var docRef = activeDocument;
            var newLayer = docRef.artLayers.add();
            var thisLayer = newLayer;
            thisLayer.name = '%s';
            %s
            %s
            """ % (
                    self._parent,                    
                    self._name,  
                    self._move,
                    self._fill
                    )
        )
        
        self._add_command(self.command)    

# With this setup, I'm going to make a new document with a new layerSet. 

psCom = PhotoshopInterface()

# Clean the command list first. 
psCom.clean_commands()

# My commands. 
psCom.new_document(doc_x=512, doc_y=512, doc_name="Wrapped")
psCom.add_new_layerSet(Name="My Wonderful Layerset")

# Now add a layer set, making My Wonderful Layerset it's parent. 
psCom.add_new_layer(Name="My Fantastic Layer", Parent="My Wonderful Layerset", Fill=(128, 128, 255))

# Execute the lot of them by calling the Photoshop application.
psCom.execute_commands()

# Whoop whoop.