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?

4 comments:

  1. Hey Pete, Thanks for making this script! I'm really interested in trying it out, but for some reason Python's winreg dosnt seem to be able to see any of the applications that i can see in my Registry Editor. Any thoughts as to why it wouldn't be able to read the Registry fully? I posted a related pic in this thread where i though the script had originated earlier:

    https://community.adobe.com/t5/after-effects/python-and-after-effects-scripting-on-windows/m-p/11051476?page=1#M108998

    ReplyDelete
    Replies
    1. Got it working, needed to allow the winreg to see 64 bit programs

      https://stackoverflow.com/questions/61235839/how-do-i-make-pythons-winreg-see-registries-that-can-be-found-in-the-regestry-e/61238282#61238282

      Delete
    2. Glad you were able to work it out! Thanks for sharing the solution.

      Delete
  2. Hi Pete, Thank you so much for sharing this!

    I am trying to get it this to work on mac, but I can't seem to get it to open with subprocess.popen.

    At 'ret = subprocess.Popen(self.target)' I get:

    FileNotFoundError: [Errno 2] No such file or directory: '"/Applications/Adobe Photoshop 2022/Adobe Photoshop 2022.app" "/Users/gebruiker/temp.jsx"': '"/Applications/Adobe Photoshop 2022/Adobe Photoshop 2022.app" "/Users/gebruiker/temp.jsx"'

    Any ideas? Would be awesome

    ReplyDelete

Comments?