Showing posts with label Photoshop Scripting. Show all posts
Showing posts with label Photoshop Scripting. Show all posts

Photoshop Comtypes 2020 edition

Hello! I got asked a question about smart layer manipulation and I didn't know the answer off of the top of my head, so I dug out this ye ole snippet of mine from 2012 and updated it to 2020.

Some small differences are that I had to add a couple of flags to the comtypes object creation (which may have just been a quirk of my machine, see the stackoverflow link for details) and I've added some details about manipulating smart layers at the bottom of the snippet.

Happy coding!

##############################################################################
#
# Eight years later, here is my 2020 version of how to use Comtypes to drive photoshop
#
# Here is a quick code sample showing how to manage different layers and groups  
# in a photoshop document using Python. 
#
# Pete Hanshaw, 2020
# http://peterhanshawart.blogspot.com.au/
#
##############################################################################
#
# How to make a layerSet (aka, 'group') artLayer (aka 'layer'), how to make them
# active and how to move them. 
#
# These examples use the comtypes module. Grab it here:
# http://sourceforge.net/projects/comtypes/
#
##############################################################################


#Create the application reference
import comtypes.client as ct

# https://stackoverflow.com/questions/42794530/error-pointeriunknown-when-trying-to-access-com-object-properties/42823644
psApp = ct.CreateObject('Photoshop.Application', dynamic=True)
psApp.Visible = True

#Create a new document to play with
doc = psApp.Documents.Add(256, 256, 72, 'test_bed', 2, 1, 1)

#When scripting 'groups' are called 'layerSets'. 
new_layerSet = doc.LayerSets.Add()

#Once you create a layerSet object reference, you can access it's
#'name' attribute. The same goes for other objects you can normally
#name within Photoshop.
new_layerSet.name = "I'm a layerSet"

#regular, paintable layers are called 'ArtLayers'
new_art_layer = doc.ArtLayers.Add()
new_art_layer.name = "I'm an ArtLayer"

#To add a nested art layer into a LayerSet, use our layerSet object as a reference
nested_art_layer = new_layerSet.ArtLayers.Add()
nested_art_layer.name = "I'm a nested ArtLayer"

#The same goes for adding a nested LayerSet!
nested_layerSet = new_layerSet.LayerSets.Add()
nested_layerSet.name = "I'm a nested LayerSet"

#and so on!
deep_art = nested_layerSet.ArtLayers.Add()
deep_art.name = "Deep man, deep."

#Every time a new object is made, it will become the active layer. 
#To make other layers active, you can refer to them either by their name, or 
#their index location. 

#For example:

#Making an art layer active using the layer's name:
doc.activeLayer = (doc.artLayers["I'm an ArtLayer"])

#Making an art layer active using the layer's index location:
doc.activeLayer = (doc.artLayers[-1]) #This will select the background!

#Selecting a nested art layer is a little more difficult, as you have to
#'drill down' through the hierachy in order to select it. 
doc.activeLayer = (doc.layerSets["I'm a layerSet"].
    layerSets["I'm a nested LayerSet"].
    artLayers["Deep man, deep."])

#Moving a layer in the hierachy is done using the move command.
#The arguments specify which hierachy to move in, and where to put it. 

#For example, this will move the first layerSet we made just above the background
#layer.

#Make a new layer set
mobile_layerSet = doc.LayerSets.Add()
mobile_layerSet.name = "move me"

#Move the 'mobile' layerSet to just after the 'background' layer
mobile_layerSet.Move(doc, 2)

# Smart Layer Manipulation Examples adapted from:
# https://www.photopea.com/tuts/edit-smart-objects-with-a-script/
# Create a smart object layer.

# CREATING A SMART LAYER
# select a layer that you want to work with
smart_layer = doc.ArtLayers.Add()
smart_layer.name = "Smarty pants Layer."

doc.activeLayer = smart_layer

# Convert the active layer into a smart object
psApp.executeAction(psApp.stringIDToTypeID("newPlacedLayer"));

# EDITING A SMART LAYER
# Now we can edit the smart object
psApp.executeAction(psApp.stringIDToTypeID("placedLayerEditContents"))

# now, the Smart Object is an active document, we can work with it. Rename the layer...
smartDoc = psApp.activeDocument
super_smart_layer = smartDoc.ArtLayers[0]
super_smart_layer.name = "Amazingly smart layer."

# save the smart object and close it
psApp.activeDocument.save()
psApp.activeDocument.close()

# We are now back in the root object

# QUERYING A SMART LAYER
# If we want to check if a layer is smart, we can query it...
doc = psApp.activeDocument

for layer in doc.ArtLayers:
    # 17 is a psSmartObjectLayer - see the Adobe Scripting API 'PsLayerKind' to see what each value means.
    if layer.Kind == 17:
        print "We found a smart layer named {}".format(layer.name)

New Tool- Substance Painter File Manager



I've added a new tool to my Gumroad store- a Substance Painter file manager to help browse larger projects.




Writing the tool was interesting- it took a while to find my way around some of the more advanced ideas in QML but I was pretty happy with how well it ties into the default Substance Painter GUI, as well as the examples contained in the default Painter installation directory.

If you want to see some examples of Substance's use of QML take a browse through your C:/Program Files/Allegorithmic/Substance Painter/qml/ directory. These are a fantastic resource, and it's great to see them included and accessible.

The Painter Javascript API is still very limited. A friend suggested I write the tool in the new Python API but I just missed the boat on that, with the majority of work already done prior to the release (I only have so much midnight oil to burn). To access the operating system and file information, I wrote a simple file browser utility in C++ and used the JS API to bridge it with the QML user facing GUI.

The cool thing about this approach (as heavy handed as it is) is that it massively opens up the options available to you in terms of OS operations, while at the same time keeping the compiled program really tiny (53kb) and removing worries about correct Python configurations.

That being said, I totally want to see what's available in the new Python API to see what kind of time saving tools I can provide to the content creators.



New Photoshop Exporter- ExportThis!

After a long and (mostly) successful run of controlling Photoshop with Python, I decided to take a stab at writing a fully integrated Photoshop Extension, this time using the native Javascript based API.

The end result is a cleaner, smaller and all round better Photoshop tool that does a number of kinda cool things.

Export This!
Some of the User-facing features
Because its written to take advantage of the native API there is a significant difference in app size- Texture Monkey being almost 40mb when including all the additional GUI and Python library files it required, vs a 29kb .ZXP installer for ExportThis. The ZXP is easily distributed, and uses Adobe Extension Manager CC to install/update/remove itself. 

Within the tool, the document template system was added to allow artists to easily make documents that would be compatible with the exporter (as a lot of it is group name/hierachy based) but I expanded on it to allow artists to make and save their own particular document setups too. 

I chose JSON files for the templates, user configuration and project definitions due to it's ease of use- and the fact that it can easily be read by anybody. This turned out to be a little bit of a challenge, as Javascript is generally blocked from accessing any local files, as well as the Asynchronous nature of Javascript itself meaning that sometimes your functions end up being called before you have any data to read.

In addition to the trouble with reading files, another odd little challenge popped up due to Javascript supporting JSON parsers out of the box and Javascript Extended completely lacking any. I got around this using the eval feature in order to read the JSON strings back into useful objects.

The tool uses the Adobe extension framework, which is made up of several components, each of which operate on different levels and have their own little quirks. 

The GUI is created in a very similar way to a web page, using HTML and CSS to define it's layout, content and internal behaviors. I was able to embed things like interactive help drop-downs using JQuery, as well as easily modifiable selection lists and other standard web-fare. 

Behind the GUI is a Javascript file that bridges the GUI to the actual Photoshop JSX extension. This is where it starts to get a little clunky, for three particular reasons:
  • This part of the tool has the same security restrictions imposed on it as any other web related Javascript code, for example, restrictions to the local file system. 
  • All calls to the actual Photoshop extension are passed as strings to evaluate (including arguments) meaning passing data back and forth between the extension and the GUI can be pretty cumbersome.
  • Javascript operates in an async manner. Due to this, the Javascript layer was kept very lightweight to minimize introducing any lame async related bugs. Although initially this lead to a little bit of head-scratching on my part, it resulted in a much easier to maintain tool, with all the heavy lifting confined to the Photoshop JSX engine. 
As I decided pretty early on that I wanted to use JSON files to store both user settings (such as the last project used, and their local project root) I had to figure out a way to be able to both read and write data from the tool, which I ended up doing on the Photoshop JSX side, which is the third component!

The Photoshop JSX (JavascriptExtended) file is where all the work actually happens- This is the part of the tool that can access the local drive, as well as drive Photoshop. I wrote it to contain a mix of environment definition and actual exporting functions. Any data sourced from the local drive that might be required by the GUI is read by the JSX and then passed back as a string to be parsed into a JSON object in memory. In a similar fashion, any GUI modified data is passed back as a string to be written back to the file system by the JSX. Lots of juggling!

The JSX also contains a component that can interact with the local system through the command line in order to check out elements from Perforce, using .bat files. 

(the hidden) GUIRilla, or a sprite exporter. 

Although the main focus of the exporter is Texture exporting for 3d models, an additional feature is the ability to author and export multiple 2D sprites from a single document. The exporter allows a "UI_SPRITE" flag document name prefix to be defined in the Project Settings, which will change the exporter to treat the document as a sprite atlas and export each group as a individual trimmed element, with transparency. It's pretty cool, but I'll go into this in a little more depth in another post, as its a whole other workflow. 

Overall, it was a fun exercise and the Artists using it have responded very well to it as a replacement for TextureMonkey. 

If you made it this far, hooray! Drop me a line if you are working with the Adobe Extension framework at all. Next up will be some tips for rapid iteration while working on extensions, and some of the terrible mistakes I made along the way...

Adobe Photoshop CC- Not exactly an early adopter...

Its a words post! Where is the code? That comes later... for now, words. I'm now an official owner (or at least subscriber) of/to Adobe CC... yeah, I got through my whining stage and now I'm learning to love the cloud, or, at least learning how to accept the inevitable.
Long live the cloud! I guess.

But from the Photoshop tool development perspective I think I actually find it a little more exciting than I am letting on. You mean *everyone* will be on a standard version? No kidding! What an awesome development! No more hacking in (sometimes) seeming random version numbers! The ability to assume everything you want to support is supported. Great! Don't have the right version? Update your Photoshop buddy!

When taken from that perspective I really think that Adobe's decision (aside from the whole aspect of never actually 'owning' the software) is a pretty great one. Maintaining pipelines for multiple versions of Photoshop ceases to be a major problem*, and tool development and distribution becomes, if not simpler, at least a little more direct in execution. 

I'm also taking my first steps into the Photoshop SDK, which is an incredibly powerful and daunting piece of architecture. Not only does it require C++ for creating plug-ins, but it also seems to be half way between a development framework and a history lesson on ye early days of Photoshop. And the documentation? Reading through it, there seems to be a big Photoshop SDK tutorial shaped hole where the SDK tutorials ought to be. 

But, if it were easy, it wouldn't be as fun! Now to work through that hello world tutorial...


* Currently supporting four different versions at work and trying very hard not to.  

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.

PSD Metadata and Real time GUI updates.

It might not be the absolute best solution, but I have been able to put into practice my idea of storing the tool settings in the PSD metadata, and it actually works pretty well.

The current setup works like this:

  • On export, go through a dictionary of controls inside the GUI and check the settings. 
  • Store each object name and it's setting as a text block with easily splittable characters. For this I chose to use ~ and | as neither of these are used for windows file or directory names. 
  • Write this text block to the PSD metadata. 
  • Meanwhile, in the export, use the GUI's current settings to determine output locations, formats and files to include.  
To reload the settings:
  • A background thread is constantly listening for any changes in the current active Photoshop document. 
  • If the name of the document changes, it automatically kicks off a function that reads the metadata out of the active PSD and splits it into objectName, setting value pairs. 
  • If no data is found, it kicks off a process to apply default settings. 
  • This data is passed to another function that finds these QT objects, and applies the relevant setting recast as the applicable object type- eg bool, int, string.
  • These settings are only saved when the document is exported. I'm expecting a little grief from the artists about this if they switch documents before exporting them at least once, so I might try to fix it. 
The Stored Metadata, ready to be read back by the tool. 
With the listener process working on a pretty short timer and a very simple function, it's possible to get what feels like real time feedback in the tool without experiencing any lag or hangs. This is the first time I've made a tool where it's a two way street- usually it's just my tool telling Photoshop what to do. It's actually pretty satisfying to click between documents in Photoshop and watch all the checkboxes of Texture Monkey light up and change. 

Main settings to be saved are the different maps and resolutions associated with the various LODs, although settings for format and destination folders are saved as well. 




Texture Monkey... version"some big number"

We are now getting Perforce. Oh how I  missed thee Perforce! And what a fantastic opportunity to revisit my favorite pet project, Texture Monkey! Be be honest, I'm kinda sick of re-writing this tool, but I've got it to a point where it meets a couple of extra prerequisites that it was sucking at before, mainly:

  • It's written at a point where I actually feel I can competently write functional and readable code.  
  • It's UI uses the QT framework. Which it MUCH nicer to work with than the previous WX. 
  • It's been built around the premise that it  should easily be portable across projects without having to change any of the source code- provided afew assumptions about project structure are met. 
  • It does more than texture exporting, and now supports Perforce integration as well. 
  • Most importantly, it is written in a modular fashion, and can be expanded upon or cut back without too much trouble. 
Now with extra stuff!
Behind the scenes:
  • It stores and loads tool settings as metadata in each PSD file, so you don't have to select those settings again when opening the file later. In addition to that, if you change the settings, then export the file, these new settings are saved to the PSD for later use. 
  • It supports exporting multiple LOD textures for different asset bundles. This is an experimental feature, and may be cut. But its fun to play around with. The idea behind this is that Unity doesn't make a distinction between different IOS devices, and instead lumps them all into one category in it's asset settings, even though there is a significant difference in specs between devices. 
  • It brings Perforce into the tool chain. When it checks out whatever active document is in Photoshop, it also brings along with it all the associated exported texture maps, basing it's search on the export locations stored in the Metadata and the source file name. 
  • I'm still thinking about adding a check in function... I kind of want to get people to check in via the P4 client, just so they have an overview of exactly what they are checking in before they actually commit it (especially if things like models are dependent on the texture changes, but fall out of the purview of this tool) For now, I have just added a function that brings the P4 client to the foreground. 
  • I have tried to add a little flexibility for other artists to be able to use this tool. It's mainly aimed at people working on textures for models, but by adding custom destinations and formats hopefully it will be of some use to export to custom locations in different formats. 
  • The configuration is all stored as an .ini file, and contains all the file naming rules, default destination folders and stuff like that. It can be hand edited, but I don't think I'm going to bother with a custom UI at this point. 
  • The Style is one billion times better, mainly due to LoneWolf's dark orange stylesheet he has provided on tech-artists.org

Writing Metadata to a PSD file using Python

I want to save some tool specific information about a PSD file, but I don't want to have another pesky metadata file floating about to bloat my source texture folder.

Luckily, the PSD file format supports writing custom MetaData within it, which is perfect for what I want to do. In this particular example, I want my tool to be able to remember which folder the flattened image associated with this PSD will eventually be put into, the format the image will be in and the resolution.

There are many fields you can write to, but I have chosen to write to the Instructions information, because that just makes the most sense. Usually in Photoshop, you can see this fields available by going to the file info panel and going to the advanced tab:

In Python, we can access and write to the PSD's metadata very easily.

import win32com.client.dynamic as w32dynamic
w32 = w32dynamic.Dispatch

psApp = w32('Photoshop.Application')
doc = psApp.activeDocument

# When I pull this info out of the Metadata I split the | into
# a list of toolObjectName~setting pairs and then
# split these pairs into a tuple of strings (toolObjectName, setting)

settings = "chkBox_res_1024~True|export_dir~c:/test/my_doc.tga|rBtn_format_tga~True"

# Now to write this to the metadata
doc.Info.Instructions = settings

# If I want to get it back out...
settings = doc.Info.Instructions
print settings


I'm still experimenting with ways of storing the settings data in a prettier way, but so far this is working well for me, although the format and information I'm saving is *very* specific to the particular tool I am writing. Still early days... but hopefully the whole Metadata thing will be handy to other people out there.



Maya Exporter: Tabs and Tools!

I've been chipping away at my Maya Exporter and have a few Photoshop functions tied into it now, along side the original exporter functionality.

Behind the scenes the Photoshop commands are built on Standard modules, using Subprocess and some string manipulation in order to get JavaScript commands to the Photoshop application. As the tool has expanded beyond it's initial export-only functionality, there has been a decent amount of clean up and re-factoring behind the scenes to make sure that it doesn't just turn into a mega-script.

Maya-side functions and Photoshop functions have been split into separate classes and files from the GUI, which has made keeping track of the code a lot easier.

The GUI is built using the Pyside QT Libraries, which comes standard with Maya 2014.

Next on the list, adding the option to load different project environments.

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. 

Python Photoshop Automation without win32com.

Clunky but kinda fun, its possible to automate Photoshop using Javascript files passed as arguments to the subprocess. 

Although this essentially means you have to write your script in two languages (PS's native JS and Python for pipeline) the practical aspect of this is that it allows scripts to be written that do not require people to have the win32com/comtypes modules installed, and paves the way for cross platform scripts that aren't tied to windows only modules. 

I've had some success with creating a dynamic JavaScript builder that wraps Python functions around Photoshop's native Javascript commands. When the list of commands is executed it is compiled into a jsx file and then passed on to the Photoshop subprocess as an argument.

The only short coming right now is obtaining the output from Photoshop and making it into a two-way street. I have some ideas on how to do this, and will update with the solution I end up using. 


PyMel- Getting texture file from a material

So, my first inelegant-yet-functional PyMel script is done and working. The core of it is in this little snippet here, that digs through a mesh's inputs until it finds the texture node on the surface shader:

#Pass through the mesh's shape and return a texture name. 
import pymel as pm

def getTextureFile(geo):

    sg = []

    #Get the shading group from the selected mesh
    raw_sg = pm.listConnections(geo, type='shadingEngine')

    #Rapidly check for duplicates in the list- I do this by checking each item 
    #against an empty list. 
    #If the item is not in the empty list I append it. 
    
    for element in raw_sg:
        if element not in sg:
            sg.append(element)

    #Get some info
    sgInfo = pm.listConnections(sg[0], type='materialInfo')
    fileNode = pm.listConnections(sgInfo[0], type='file')
    textureFile = pm.getAttr(fileNode[0].fileTextureName)



    print 'This is the file', str(textureFile)
    return str(textureFile)
It still leaves a lot to be desired, for instance it doesn't know what to do if a mesh doesn't actually have a texture! But, provided it does, using the win32com module I can then send the texture file information onto Photoshop and Alienbrain and open files/check out texture chains etc etc.

EDIT:
------------------------------------------------------------------------------------------------------------
Thanks to Daydreamer who pointed out the missing pymel reference at the top of the snippet, as well as other issues with the script which I am working through...

After a happy weekend of actually reading part of the official tutorial, I have a slightly better understanding of how to access a mesh's attributes and nodes using PyMel. Here is a snippet of the above snippet that does pretty much the same thing, only using the pymel connections, input and output commands. The intention is only to test the functionality.
    
    import pymel as pm

    geo = pm.ls(sl=True)[0].getShape()

    #Get the shading group from the selected mesh
    sg = geo.outputs(type='shadingEngine')

    #Work through the node to get the file texture name
    sgInfo = sg[0].connections(type='materialInfo')
    
    #It falls apart here if you have no file node! Oops...
    fileNode = sgInfo[0].connections(type='file')

    #Get the file texture name attribute's value
    textureFile = pm.getAttr(fileNode[0].fileTextureName)

    print 'This is the file', str(textureFile)

Getting Maya to talk to Photoshop

Sadly, at work I am not blessed with the latest and greatest Maya versions, and I have to be content with Maya 8.5. That also means I have to be content with Python 2.4.2 in Maya... eh. Its enough to do some cool stuff.

It has been a little pet project of mine to get Photoshop and Maya to talk to each other and share texture information (like texture file locations, etc etc) and after a bit of fussing around, and a total failure to get comTypes to work (missing ctypes module in py 2.4) I have been able to get the win32com module to work within the Maya Python 2.4.2 scripting environment.

I don't think its going to be a two way street, but it does mean I can open a model's source PSD file(s) in Photoshop from within Maya. Kinda a neat little workflow thing.

If anyone wants to share a better method give me a shout!

-Pete

Photoshop Constants Roestta Stone

Hey peeps, if you have been using the Photoshop scripting listener to help automate your day to day tasks you would be familiar with those vague CharID's that Photoshop uses to describe each action it performs.

In an earlier post I suggested using the Photoshop SDK's terminology files, but here is something just as good and far more accessible.

John Deurbrouck put up a list of Photoshop constants back in 2005. Although he was using it for C#, this page is a fantastic resource to poke at in-case you come across any CharID types that are too ambiguous to work out. Bear in mind the list may be a little dated, but if you need a quick CharID look up its worth keeping in mind.

-Pete

Managing Photoshop Layers using Python

Just another quick example of how to make Photoshop do your bidding. This is bare bones basics so don't expect a massive revelation if you have been doing PS scripting for a while.

Running this script will give you a document with a bunch of layers and groups. Real useful, I know. But the goal is just to see how those layers and groups are made, named and accessed.

If anyone out there is using Python with Photoshop I'd be interested in hearing about how you have used it and what kind of tasks you have found it useful for!

Anyway, here we go:


##############################################################################
#
# Here is a quick code sample showing how to manage different layers and groups  
# in a photoshop document using Python. 
#
# Pete Hanshaw, 2012
# http://peterhanshawart.blogspot.com.au/
#
##############################################################################
#
# How to make a layerSet (aka, 'group') artLayer (aka 'layer'), how to make them
# active and how to move them. 
#
# These examples use the comtypes module. Grab it here:
# http://sourceforge.net/projects/comtypes/
#
##############################################################################


#Create the application reference
import comtypes.client
import pythoncom

psApp = comtypes.client.CreateObject('Photoshop.Application')

#Create a new document to play with
doc = psApp.Documents.Add(256, 256, 72, 'test_bed', 2, 1, 1)

#When scripting 'groups' are called 'layerSets'. 
new_layerSet = doc.LayerSets.Add()

#Once you create a layerSet object reference, you can access it's
#'name' attribute. The same goes for other objects you can normally
#name within Photoshop.
new_layerSet.name = "I'm a layerSet"

#regular, paintable layers are called 'ArtLayers'
new_art_layer = doc.ArtLayers.Add()

new_art_layer.name = "I'm an ArtLayer"

#To add a nested art layer into a LayerSet, use our layerSet object as a reference
nested_art_layer = new_layerSet.ArtLayers.Add()
nested_art_layer.name = "I'm a nested ArtLayer"

#The same goes for adding a nested LayerSet!
nested_layerSet = new_layerSet.LayerSets.Add()
nested_layerSet.name = "I'm a nested LayerSet"

#and so on!
deep_art = nested_layerSet.ArtLayers.Add()
deep_art.name = "Deep man, deep."

#Every time a new object is made, it will become the active layer. 
#To make other layers active, you can refer to them either by their name, or 
#their index location. 

#For example:

#Making an art layer active using the layer's name:
doc.activeLayer = (doc.artLayers["I'm an ArtLayer"])

#Making an art layer active using the layer's index location:
doc.activeLayer = (doc.artLayers[-1]) #This will select the background!

#Selecting a nested art layer is a little more difficult, as you have to
#'drill down' through the hierachy in order to select it. 
doc.activeLayer = (doc.layerSets["I'm a layerSet"].
                   layerSets["I'm a nested LayerSet"].
                   artLayers["Deep man, deep."])

#Moving a layer in the hierachy is done using the move command.
#The arguments specify which hierachy to move in, and where to put it. 

#For example, this will move the first layerSet we made just above the background
#layer.

#Make a new layer set
mobile_layerSet = doc.LayerSets.Add()
mobile_layerSet.name = "move me"

#Move the 'mobile' layerSet to just after the 'background' layer
mobile_layerSet.Move(doc, 2)


Python and the Photoshop Script listener, Part Two

If you read my earlier post about using the scripting listener to help write useful Python code, you probably noticed that there is alot of CharIDtoTypeID going on, followed by one of any number of ambiguous four letter codes.

You probably figured out that these are pretty much shorthand codes that call Photoshop functions. While that's useful to know, the raw script listener output does not make it too easy to work out exactly what they are calling. Most of these are listed in Appendix A of the VB, or Javascript, scripting guides.

Even better, if you are tech savvy enough and the appendix doesn't contain the information you need, you can download the relevant PhotoshopSDK for your version of Photoshop and poke around in the PITerminology.h file.

As an example, this VB scripting listener output:

idRGBC = objApp.CharIDToTypeID( "RGBC" )

Is calling upon this CharID, as seen in the PITerminology.h file:

#define classRGBColor  'RGBC' // keyRed, keyGreen, keyBlue.

As you can see, the great thing about this resource is that many of the available classes include comments that clarify other required components. Good resource for any scripter!

Python and the Photoshop Script listener

This little guide is written for Photoshop Cs5. Other versions of Photoshop may have slightly different folder setups and listener output locations. 

As discussed in a few of my earlier posts, Python is a great choice for scripting in Photoshop when coupled with a good COM interface and a desire to make monkeywork just go away. But as you are busy putting those monkeys to work it is inevitable that you will come up against a command or two that's not made available in the regular Photoshop scripting documents.

CRISIS! The monkeys go on strike. But wait, there is hope!


Introducing awesomeness...
The scripting listener is a brilliant plugin for Photoshop. Using it gives you access to many commands that are not included in the Photoshop scripting reference.

While active (that is, present in the Automate folder) it spits out a JS and VB log of all all your actions in Photoshop. For the purposes of this guide we are only really interested  in the VB log. Getting useful Python code from it can take a little bit of tinkering with the output, but once you get the hang of it you can make those monkeys do anything.

Lets get on with it! To install the plugin is simple enough.
  • To find it go to your Photoshop installation directory and find <PS_directory>\Scripting\Utilities\    
  • In this you will find a file called ScriptListener.8li 
  • Make a copy of this file and put it in <PS_directory>\Plug-Ins\Automate\
  • Done! 
  • Restart Photoshop and do something!
Once you do something you will notice that a couple of files have appeared on your desktop. In earlier versions of Photoshop these files may appear in C:\. If you open the ScriptingListenerVB.log you will see a mess of DIM idNm idNm blah blah etc. Don't worry, it will (hopefully) all make sense. 

So now that its installed, here are a couple of hints at how to maximize the readability of the subsequent output. 
  •  Record a single, deliberate action at a time. This will make it really obvious as to what the output is trying to do. 
  • Put specific values into any requested options so that you can easily spot where they appear in the output. 
  • Nuke the output file before you start recording and directly after you have got what you need from it. This saves having to sift through miles of code to get to what you want.  
Now! For the purposes of this guide, I'm going to create a Python script that selects a specific color across an entire image. This is easy enough to do in Photoshop by hand, but can be a very time consuming task for a script depending on the approach taken, and is not exposed as a regular scripting function. Either way, its a good example.

Right on!
It may seem like cheating, but the way to get the code to do the action, is just to do the action! In the image I have chosen there is a very specific color I am aiming at, 255 green, so the output will be legible. So now I nuke the listener output, and do my action:


Looking at the ScriptingListenerVB.log file now, I can see the results:

REM =======================================================
DIM objApp
SET objApp = CreateObject("Photoshop.Application")
REM Use dialog mode 3 for show no dialogs
DIM dialogMode
dialogMode = 3
DIM idClrR
idClrR = objApp.CharIDToTypeID( "ClrR" )
    DIM desc71
    SET desc71 = CreateObject( "Photoshop.ActionDescriptor" )
    DIM idFzns
    idFzns = objApp.CharIDToTypeID( "Fzns" )
    Call desc71.PutInteger( idFzns, 0 )
    DIM idMnm
    idMnm = objApp.CharIDToTypeID( "Mnm " )
        DIM desc72
        SET desc72 = CreateObject( "Photoshop.ActionDescriptor" )
        DIM idLmnc
        idLmnc = objApp.CharIDToTypeID( "Lmnc" )
        Call desc72.PutDouble( idLmnc, 87.840000 )
        DIM idA
        idA = objApp.CharIDToTypeID( "A   " )
        Call desc72.PutDouble( idA, -79.040000 )
        DIM idB
        idB = objApp.CharIDToTypeID( "B   " )
        Call desc72.PutDouble( idB, 79.380000 )
    DIM idLbCl
    idLbCl = objApp.CharIDToTypeID( "LbCl" )
    Call desc71.PutObject( idMnm, idLbCl, desc72 )
    DIM idMxm
    idMxm = objApp.CharIDToTypeID( "Mxm " )
        DIM desc73
        SET desc73 = CreateObject( "Photoshop.ActionDescriptor" )
        DIM idLmnc
        idLmnc = objApp.CharIDToTypeID( "Lmnc" )
        Call desc73.PutDouble( idLmnc, 87.840000 )
        DIM idA
        idA = objApp.CharIDToTypeID( "A   " )
        Call desc73.PutDouble( idA, -79.040000 )
        DIM idB
        idB = objApp.CharIDToTypeID( "B   " )
        Call desc73.PutDouble( idB, 79.380000 )
    DIM idLbCl
    idLbCl = objApp.CharIDToTypeID( "LbCl" )
    Call desc71.PutObject( idMxm, idLbCl, desc73 )
    DIM idcolorModel
    idcolorModel = objApp.StringIDToTypeID( "colorModel" )
    Call desc71.PutInteger( idcolorModel, 0 )
Call objApp.ExecuteAction( idClrR, desc71, dialogMode )
There is a lot going on there, but I can see some sort of sense in it.  The first six lines are calling the application and setting the dialog mode to none. We don't have to worry too much about that at this point.

Lines 11-13 are where the 'fuzziness' value of the color range is defined. It starts to get interesting with line 15. If you look quickly at line 15 and line 31, you will notice that two colors are defined, first a minimum, or "Mnm  " on line 15, and then a maximum, or "Mxm  " on line 31.

From line 16 to line 26 the desc72 values are assigned. Although apparently bearing no relation to my expected single value, lines 20, 23 and 26 are defining 255 green in Lab color. (87.84, -79.04, 79.38). Worth knowing! As I only defined one color for my range, so the same values are expressed for the maximum in desc72 between lines 32 and 42.

The action itself is put together gradually using all these smaller packages. The entire minimum color is assembled on line 29:

Call desc71.PutObject( idMnm, idLbCl, desc72 )


Pretty much saying "Minimum range is a lab color of these values" and line 45:

Call desc71.PutObject( idMxm, idLbCl, desc73 )


Which says "Maximum range is a lab color of these values".

Finally, the actual action is performed on line 49.

objApp.ExecuteAction( idClrR, desc71, dialogMode )


What a heap of stuff going on! If you have made it this far, awesome. We will make those monkeys work yet. The beauty of Python is that right off the bat we can remove a whole heap of clutter, which will increase legibility from the word go. With the previous notes in mind, here is a very quick translation of the VB script outpt into Python! I've left the variable names the same so that a comparison can be made easily, although I will change them later for readability.

#Our access to the photoshop application
#Get this COM module from:
#http://sourceforge.net/projects/comtypes/
import comtypes.client

objApp = comtypes.client.CreateObject('Photoshop.Application')

#Set dialog mode to none
dialogMode = 3

idClrR = objApp.CharIDToTypeID( "ClrR" )
desc71 = comtypes.client.CreateObject( "Photoshop.ActionDescriptor" )

#Fuzziness value
idFzns = objApp.CharIDToTypeID( "Fzns" )
desc71.PutInteger( idFzns, 0 )

#Define the minimum colours
idMnm = objApp.CharIDToTypeID( "Mnm " )

#Create an action descriptor
desc72 = comtypes.client.CreateObject( "Photoshop.ActionDescriptor" )

#Luminance value
idLmnc = objApp.CharIDToTypeID( "Lmnc" )
desc72.PutDouble( idLmnc, 87.840000 )

#A and B colors
idA = objApp.CharIDToTypeID( "A   " )
desc72.PutDouble( idA, -79.040000 )
idB = objApp.CharIDToTypeID( "B   " )
desc72.PutDouble( idB, 79.380000 )

#Define the colour type. In this case 'Lab color'
idLbCl = objApp.CharIDToTypeID( "LbCl" )

#Assemble the instructions with the action descriptor.
desc71.PutObject( idMnm, idLbCl, desc72 )

#Define the maximum colors using the same steps.
idMxm = objApp.CharIDToTypeID( "Mxm " )
desc73 = comtypes.client.CreateObject( "Photoshop.ActionDescriptor" )

#L A B
idLmnc = objApp.CharIDToTypeID( "Lmnc" )
desc73.PutDouble( idLmnc, 87.840000 )
idA = objApp.CharIDToTypeID( "A   " )
desc73.PutDouble( idA, -79.040000 )
idB = objApp.CharIDToTypeID( "B   " )
desc73.PutDouble( idB, 79.380000 )

#Color type
idLbCl = objApp.CharIDToTypeID( "LbCl" )

#Assemble action
desc71.PutObject( idMxm, idLbCl, desc73 )

#Call the color model
idcolorModel = objApp.StringIDToTypeID( "colorModel" )
desc71.PutInteger( idcolorModel, 0 )

#Execute the action with 'Defined color, Fuzziness, no dialog
objApp.ExecuteAction( idClrR, desc71, dialogMode )


Executing this script from the console will select all of 255 green in the active document. Awesome! But... still, kinda useless without being able to define specific values. Unless, you know, you always want it to select 255 green. Then you are set! Go forth!

But I'm not satisfied yet. The script works at this point, but is both fairly unreadable and totally inflexible. I want to be able to define colors, and I want to be able to define them MY way. I don't use Lab color, and all my other scripts use RGBA already.

Meanwhile, back in Photoshop...

Quickly setting an RGB value in Photoshop gives me enough info in the script listener on how to define an RGB color rather than Lab. Looking at the results gives me a clear idea on how to replace the Lab color in my script with these values instead.

Because I know what I am looking for I don't bother too much about the rest of the junk. I only want the idRGBC code on line 15 and and all the individual channel value IDs on lines 6, 9 and 12.

DIM idT
    idT = objApp.CharIDToTypeID( "T   " )
        DIM desc83
        SET desc83 = CreateObject( "Photoshop.ActionDescriptor" )
        DIM idRd
        idRd = objApp.CharIDToTypeID( "Rd  " )
        Call desc83.PutDouble( idRd, 0.000000 )
        DIM idGrn
        idGrn = objApp.CharIDToTypeID( "Grn " )
        Call desc83.PutDouble( idGrn, 255.000000 )
        DIM idBl
        idBl = objApp.CharIDToTypeID( "Bl  " )
        Call desc83.PutDouble( idBl, 0.000000 )
    DIM idRGBC
    idRGBC = objApp.CharIDToTypeID( "RGBC" )
    Call desc82.PutObject( idT, idRGBC, desc83 )
Call objApp.ExecuteAction( idsetd, desc82, dialogMode )

Makin' it purdy... 

So lets clean it up and make it a little flexible. We know where the colors are defined and where they are used. I'm going to replace the Lab options with some RGB ones. After some playing around, here is the (somewhat) tidier Python code I came out with, along with RGB options hacked in:

#Our access to the photoshop application
#Get this COM module from:
#http://sourceforge.net/projects/comtypes/
import comtypes.client

#Set dialog mode to none
dialogMode = 3

#Set my maximum and minimum RGB values
minR = 0.0
minG = 255.0
minB = 0.0

maxR = 0.0
maxG = 255.0
maxB = 0.0

#Define the fuzziness
fuzz = 0

#Begin!
psApp = comtypes.client.CreateObject('Photoshop.Application')

selColRange = psApp.CharIDToTypeID( "ClrR" )
selColRangeOptions = comtypes.client.CreateObject( "Photoshop.ActionDescriptor" )
fuzziness = psApp.CharIDToTypeID( "Fzns" )
selColRangeOptions.PutInteger( fuzziness, fuzz )

defineMinCol = psApp.CharIDToTypeID( "Mnm " )

#Create an action descriptor for the minimum color values using RGB
min_color_values = comtypes.client.CreateObject( "Photoshop.ActionDescriptor" )

color_type = psApp.CharIDToTypeID( "RGBC" )

red = psApp.CharIDToTypeID( "Rd  " )
min_color_values.PutDouble( red, minR )

green = psApp.CharIDToTypeID( "Grn " )
min_color_values.PutDouble( green, minG )

blue = psApp.CharIDToTypeID( "Bl  " )
min_color_values.PutDouble( blue, minB )

#Assemble the instructions with the action descriptor.
selColRangeOptions.PutObject( defineMinCol, color_type, min_color_values )

#Define the maximum colors using the same steps.
defineMaxCol = psApp.CharIDToTypeID( "Mxm " )

#Create an action descriptor for the minimum color values using RGB
max_color_values = comtypes.client.CreateObject( "Photoshop.ActionDescriptor" )

red = psApp.CharIDToTypeID( "Rd  " )
max_color_values.PutDouble( red, maxR )

green = psApp.CharIDToTypeID( "Grn " )
max_color_values.PutDouble( green, maxG )

blue = psApp.CharIDToTypeID( "Bl  " )
max_color_values.PutDouble( blue, maxB )

#Assemble the instructions with the action descriptor.
selColRangeOptions.PutObject( defineMaxCol, color_type, max_color_values )

#Call the color model
idcolorModel = psApp.StringIDToTypeID( "colorModel" )
selColRangeOptions.PutInteger( idcolorModel, 0 )

#Execute selectColRange with our options and no dialog
psApp.ExecuteAction( selColRange, selColRangeOptions, dialogMode )

#Done!

Brain... cooling... crispy...

And there we have it. By adding variables up the top that are called throughout the script, it will be pretty straight forward from here to make this into a function and what have you. If you made it this far, kudos!

Just to reiterate, follow these simple guidelines:

  • Delete output file before recording a new action to start with a clean slate. 
  • Use the VB output log, as it most easily translates into Python.
  • Record single actions to increase readability of the output code.
  • Put easily identifiable values in on the Photoshop side and the output will be much more legible. 

Whoo! I selected a color! With CODE!
Although this example might not yield the most ground breaking results, that part is up to you. In a production environment Photoshop scripting can produce some spectacular time savings, as well as automating some mindbogglingly complex tasks demanded of current generation game projects. But apart from all that, its pretty fun watching Photoshop do your work for you. Right on.

-Pete

Creating Guides in a Photoshop document using Python- Part Three

Its been pretty straight forward to setup a function that allows a user defined number of guides to be added to an active document. I've plugged it into the Texture Toolset, so that when a user clicks the 'add guides' button, they get presented with:

'One million divisions!'


As an example, asking for '1' division will give you a reasonable result. 

  
 
Ask for a stupid amount and it will give you a stupid result...

Whoo! Cyan for the win. But seriously, making the guides script has been a good excuse to use the script listener, and I'm pretty happy with the results.