Use Python to use JavaScript to get Photoshop to do stuff... and tell you about it!

Its been pretty well established that you can send a .jsx file to Photoshop using a subprocess call in Python. The tricky part is then getting Photoshop to send some information back.

This is my awesome hacky method of passing information from Photoshop to Python. It relies on being able to write to a temporary text file from Photoshop and then reading that information back into Python. This method relies on actually being able to write data to disk... if that's a problem I suspect you could do the same to the console standard output and get the same results... somehow? Maybe?

Because writing to the disk was not a problem in this situation, I went with a temp file solution. The only kinda tricky part was making my program wait for the return value to be passed. Subproces.call() returns a value of 1 or 0 from the shell, but this only indicates that the program successfully (or not) opened.

Its highly likely that whatever script you passed to Photoshop as part of the Subprocess call will still be executing by the time your Python comes to the section where you want to read the return data. In this case, your Python code will likely be reading old data from the temp file, or, no data at all.

In this case, I was fine with having my program wait until the data it needed was available. I did this by doing a check to see if the temporary output text file had been modified. Once this condition was met, the file was opened in Python and the contents were pulled back into the main program.

I've seen some people recommending using a JSON file to do this, which is something I might look into if I need more complex feedback than a single line.

Here is an example of a Python script which builds a .jsx file, sends it to Photoshop, waits for a return value and then prints the return value out to the console.

"""
Example which builds a .jsx file, sends it to photoshop and then waits for data to be returned. 
"""
import os
import subprocess
import time
import _winreg

# A Mini Python wrapper for the JS commands...
class PhotoshopJSWrapper(object):
    
    def __init__(self):
        # Get the Photoshop exe path from the registry. 
        self.PS_key = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, 
                                      "SOFTWARE\\Adobe\\Photoshop\\12.0")
        self.PS_APP = _winreg.QueryValueEx(self.PS_key, 'ApplicationPath')[0] + 'Photoshop.exe'          

        # Get the path to the return file. Create it if it doesn't exist.
        self.return_file = 'c:\\temp\\ps_temp_ret.txt'
        if not os.path.exists('c:\\temp\\'):
            os.mkdir('c:\\temp\\')
        
        # Ensure the return file exists...
        with open(self.return_file, 'w') as f:
                f.close()  
            
        # Establish the last time the temp file was modified. We use this to listen for changes. 
        self._last_mod_time = os.path.getmtime(self.return_file)         
        
        # Temp file to store the .jsx commands. 
        self.temp_jsx_file = "c:\\temp\\ps_temp_com.jsx"
        
        # This list is used to hold all the strings which eventually become our .jsx file. 
        self._commands = []    
    
    # This group of helper functions are used to build and execute a jsx file.
    def js_new_command_group(self):
        """clean the _commands list. Called before making a new list of commands"""
        self._commands = []

    def js_execute_command(self):
        """Pass the commands to the subprocess module."""
        self._compile_commands()
        self.target = '"' + self.PS_APP +'"' + " " +  '"' + self.temp_jsx_file + '"'
        print self.target
        ret = subprocess.Popen(self.target) 
    
    def _add_command(self, command):
        """add a command to the commands list"""
        self._command_list.append(command)

    def _compile_commands(self):
        with open(self.temp_jsx_file, "wb") as f:
            for command in self._commands:
                f.write(command)
           
    # These are the strings used to build the .jsx file.  
    def js_create_document(self, varName, w, h, docName):
        """
        Javascript command to create a new document. Returns varname as 
        a reference to the jsx variable. 
        """
        self._mode = " NewDocumentMode.RGB" # Hard set, but easy to add as a python var
        self._init_fill = "DocumentFill.WHITE" # Hard set, but easy to add as a python var
        self._PaR = 1.0 # Hard set, but easy to add as a python var
        self._BpC = "BitsPerChannelType.EIGHT" # Hard set, but easy to add as a python var 
        
        self._com = (
            """
            %s = app.documents.add(%s, %s, 72, "%s", %s, %s, %s, %s);
            """ % (varName, w, h, docName, self._mode, self._init_fill, self._PaR, self._BpC)
            )
        self._commands.append(self._com)
        return varName # Return the name we used for the jsx var, we can use this later in the Python code

    
    def js_write_data_out(self, returnRequest):
        """ An example of getting a return value"""
        self._com = (
            """
            var retVal = %s; // Ask for some kind of info about something. 
            
            // Write to temp file. 
            var datFile = new File("/c/temp/ps_temp_ret.txt"); 
            datFile.open("w"); 
            datFile.writeln(String(retVal)); // return the data cast as a string.  
            datFile.close();
            """ % (returnRequest)
        )
        self._commands.append(self._com)
        
        
    def read_return(self):
        """Helper function to wait for PS to write some output for us."""
        # Give time for PS to close the file...
        time.sleep(0.1)        
        
        self._updated = False
        while not self._updated:
            self._this_mod_time = os.path.getmtime(self.return_file)
            if str(self._this_mod_time) != str(self._last_mod_time):
                self._last_mod_time = self._this_mod_time
                self._updated = True
        print "Return Detected"
        
        f = open(self.return_file, "r+")
        self._content = f.readlines()
        f.close()      
        self._ret = []
        for item in self._content:
            self._ret.append(str(item.rstrip()))
        return self._ret
    
    
# An interface to actually call those commands. 
class PhotoshopJSInterface(object):
    
    def __init__(self):
        
        self.psCom = PhotoshopJSWrapper()
    
    def create_new_document(self, x, y, docName):
        """Compile a command to create a new document"""
        self.psCom.js_new_command_group() # Clears the command list. 
        self.docRef = self.psCom.js_create_document('docRef', x, y, docName) # Adds the new document command to the list. 
        self.psCom.js_write_data_out(self.docRef + ".activeLayer.name") # Get the document's active layer name. 
        self.psCom.js_execute_command()
        
        # Now I find the return value. 
        self.layerName = self.psCom.read_return()[0]
        print "Current active layer:", self.layerName
        
        
PS = PhotoshopJSInterface()
PS.create_new_document(512, 512, 'My Amazing Document')

Now, in my mind this is pretty handy, and could be extended to a point where it could become a viable Python API for Photoshop. One thing I do want to look into is using Socket control to talk to the Photoshop application directly, replacing the use of the Subprocess module. Maybe it would be possible to then get information back without writing to a temp file. Has anyone tried this?

Texture Monkey: Starting to look like a real tool.

Stuff is happening...
After a little while in the field it came to be pretty apparent that the win32com module can't be relied upon to consistently work on everyone's machines. I spent a good amount of time trying to work out why, but came up with no solution that worked consistently on everyone's workstations. Errors like ('Member not found, None, None) would constantly pop up in their debug feedback, but not on mine. Talk about frustrating. So I got pretty annoyed and just ripped the win32com components out. 

This was a pretty big undertaking, because apart from the GUI, win32com stuff made up about 80% of the remaining code. The P4 functionality wasn't touched, because its operating through it's own native API. 

The solution I went with was to write a wrapper around the JavaScript equivalent of what I was using the win32com module for. Each of these commands would be put into a list, which is then compiled into a temporary .jsx file. Finally this file is sent to Photoshop using the subprocess module. 

To deal with getting information from Photoshop a similar process was used. A JavaScript command is called to write the requested data to a file on disk, while Python waits until it can see that the file has been modified. Once Python can see that Photoshop has completed writing to the file, the contents are read and fed back into the main program. 

Benefits/Cons
Benefits? Right off the bat, the JavaScript executes much more quickly than the win32com commands. This is pretty cool, especially when coupled with the fact that it now works on all the artists machines without mysterious bugs. Yet. Also, I still get to keep writing the tool in Python.

The biggest con is that I can't help but feel that its a mother of a hack. I mean, writing JavaScript on the fly from Python to send to a program which can only feed data back through a temporary text file? Man. Awful. But it works. But I feel dirty. But it works. But what about those ugly chunks of strings pretending to be JavaScript? But it works. Yes it works. The structure of the code behind the scenes feels like its taken a train to the chest, but it works. My next couple of days will be pulling it together and making the code a little easier on the eyes.

First Speedsculpt 2014- Bear

Zbrush, one hour. Felt like doing something a little more natural looking. Kinda feel I could have pushed the character a little further... maybe I will later.

Its a happy/bored bear...