New Website

For those of you who like video game art my new website is up and running. Its got a whole heap of content from my own personal stuff to renders from my work on DeBlob2. Keen an eye out in the future for an art dump of my current project, but until then, enjoy!

More scribbles.

I'd love to run around this, hopefully without being impaled by some ancient piece of hardware.



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

Quick paintover

A quick paint-over of one of my commute scribbles:


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.

Awesome error...

You know you are doing something right when you get errors like this:

Creating Guides in a Photoshop document using Python- Part Two

Well. That was actually a lot quicker than I expected. Although the initial code looks like junk, its actually pretty easy to read if you know what you are looking for. Here is my very rapid Python translation that works exactly the same way as the VBS output.

#Python translation... and go!
import comtypes.client

#Create a reference to the PS app
psApp = comtypes.client.CreateObject('Photoshop.Application')

#Create a ref to the dialog mode. Use mode 3 to use no dialog
dialogMode = 3

#Start doing crazy shiz
id43 = psApp.CharIDToTypeID( "Mk  " )

desc7 = comtypes.client.CreateObject( "Photoshop.ActionDescriptor" )

#I assume "Nw  " means... no idea. No worries?
id44 = psApp.CharIDToTypeID( "Nw  " )

desc8 = comtypes.client.CreateObject( "Photoshop.ActionDescriptor" )

#This is obviously the resulting position, in pixels, followed by a measuement. 
id45 = psApp.CharIDToTypeID( "Pstn" )
id46 = psApp.CharIDToTypeID( "#Pxl" )
desc8.PutUnitDouble( id45, id46, 256.000000 )

#The orientation of the guideline, In this case 'Vrtc' or 'Vertical'
id47 = psApp.CharIDToTypeID( "Ornt" )
id48 = psApp.CharIDToTypeID( "Ornt" )
id49 = psApp.CharIDToTypeID( "Vrtc" )

#Call the Action descriptor with the Orientation
desc8.PutEnumerated( id47, id48, id49 )

#This is where we call the guide type
id50 = psApp.CharIDToTypeID( "Gd  " )

#Now assemble "Nw  " and "Gd  " as an action
desc7.PutObject( id44, id50, desc8 )

#Finally, tell Photoshop to execute "Mk  " with the action described and no dialog
psApp.ExecuteAction( id43, desc7, dialogMode )

#Done!

So now its pretty straight forward to experiment with simplifying it as much as possible and encapsulating it as a callable function. The most exciting part is that now, knowing how to get into the guts of it, I imagine that the scripting listener gives access to just about every single function in PS. Pretty cool!

Creating Guides in a Photoshop document using Python

When doing FX and sprite sheets I like to rule up my document into the correct UV frames before doing any art. Naturally enough, I thought to myself 'Why don't I just script that?'.

Well... it seems that 'guides' are not very natural to Python/VB Photoshop Scripting. Although exposed to Javascript, the guides functionality in Python/VB is just not there.

So after trying multiple ways of asking Photoshop nicely, I decided to try out the scripting listener with the intention of just hacking it in (now its personal!). After plugging in the listener Photoshop kindly supplied me with the required information written in VBs. Now all I've got to do is figure out how to make it into a usable Python script! Stay tuned!

For anyone who is interested, this is what Photoshop sounds like when it mumbles to itself:

REM =======================================================
DIM objApp
SET objApp = CreateObject("Photoshop.Application")
REM Use dialog mode 3 for show no dialogs
DIM dialogMode
dialogMode = 3
DIM id43
id43 = objApp.CharIDToTypeID( "Mk  " )
    DIM desc7
    SET desc7 = CreateObject( "Photoshop.ActionDescriptor" )
    DIM id44
    id44 = objApp.CharIDToTypeID( "Nw  " )
        DIM desc8
        SET desc8 = CreateObject( "Photoshop.ActionDescriptor" )
        DIM id45
        id45 = objApp.CharIDToTypeID( "Pstn" )
        DIM id46
        id46 = objApp.CharIDToTypeID( "#Pxl" )
        Call desc8.PutUnitDouble( id45, id46, 256.000000 )
        DIM id47
        id47 = objApp.CharIDToTypeID( "Ornt" )
        DIM id48
        id48 = objApp.CharIDToTypeID( "Ornt" )
        DIM id49
        id49 = objApp.CharIDToTypeID( "Vrtc" )
        Call desc8.PutEnumerated( id47, id48, id49 )
    DIM id50
    id50 = objApp.CharIDToTypeID( "Gd  " )
    Call desc7.PutObject( id44, id50, desc8 )
Call objApp.ExecuteAction( id43, desc7, dialogMode )

REM =======================================================
DIM objApp
SET objApp = CreateObject("Photoshop.Application")
REM Use dialog mode 3 for show no dialogs
DIM dialogMode
dialogMode = 3
DIM id43
id43 = objApp.CharIDToTypeID( "Mk  " )
    DIM desc7
    SET desc7 = CreateObject( "Photoshop.ActionDescriptor" )
    DIM id44
    id44 = objApp.CharIDToTypeID( "Nw  " )
        DIM desc8
        SET desc8 = CreateObject( "Photoshop.ActionDescriptor" )
        DIM id45
        id45 = objApp.CharIDToTypeID( "Pstn" )
        DIM id46
        id46 = objApp.CharIDToTypeID( "#Pxl" )
        Call desc8.PutUnitDouble( id45, id46, 256.000000 )
        DIM id47
        id47 = objApp.CharIDToTypeID( "Ornt" )
        DIM id48
        id48 = objApp.CharIDToTypeID( "Ornt" )
        DIM id49
        id49 = objApp.CharIDToTypeID( "Vrtc" )
        Call desc8.PutEnumerated( id47, id48, id4
9 )
    DIM id50
    id50 = objApp.CharIDToTypeID( "Gd  " )
    Call desc7.PutObject( id44, id50, desc8 )
Call 
objApp.ExecuteAction( id43, desc7, dialogMode )


More Sketch

And now for the one and only good thing about my morning/afternoon commute! Random Sketch time!



Python and photoshop- code snippets

I thought I might make a little list of commands that I use *alot* in my Photoshop Python scripts in case they might be useful to other people out there trying to do the same thing. These examples are written using Python 2.7.


An invaluable help is the Photoshop Cs4 Visual basic scripting guide. Although the scripting guide does not specifically deal with Python, the majority of the VB code works straight out of the box.

Before anything else, you need to set up the COM interface! I recommend using the comtypes module. The win32com module can also do the job, but you will find it very limiting if you want to do anything with paths or selection arrays.

This is how I usually create my photoshop COM reference in my scripts:

import comtypes.client

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


The psApp object that I have created is my reference to the photoshop application. From now on, I can call psApp to execute various actions. It's worth noting that if you call your script and Photoshop is not already open, the comtypes client will start the program for you.

Most of my scripts deal with the current active photoshop document- if you have more than one photoshop document open, the reference will point to whichever one is active in the layers window. This is how you create a reference to that object. 

doc = psApp.activeDocument

In some instances you may need to create a new photoshop document to perform work on. Notice that I included setting the default units. If you skip this you might end up making a 1024cm x 1024cm document by mistake! (true story.)

#Set units to pixels.
psApp.Preferences.RulerUnits = 1

#Make a new photoshop document
doc = psApp.Documents.Add(1024, 1024, 72, "new_document", 2, 1, 1)

As you will notice, its a little more complicated and involves a few seemingly random arguments. Here is a breakdown of the arguments in order:

Add(width, height, dpi/resolution, document name, mode, initial-fill, aspect-ratio, bits per channel, color profile)

Most of these are pretty straight forward, but some need a little bit of clarification- I suggest looking at page 71 of the Photoshop Cs4 Visual basic scripting guide for more details, under Methods- Add.

Adding groups and art layers with a script is very handy. It can be a powerful tool in automatically setting up document templates. Here is how to set up a base color, create a new art layer and then fill it with your color.

#Define the color- in this case, flat normal blue
baseColor = comtypes.client.CreateObject('Photoshop.SolidColor')
baseColor.rgb.red = 128
baseColor.rgb.green = 128
baseColor.rgb.blue = 255

#Create a new layer to apply our color to. When you add a new layer, it becomes the active layer. 
new_art_layer = new_layerSet.ArtLayers.Add()
new_art_layer.name = 'flatNormal'
            
#Fill the layer with appropriate color
psApp.activeDocument.selection.selectAll
psApp.activeDocument.selection.Fill(baseColor)



Defining save options is also something I do alot. Depending on which format your project demands, you will find you need to use different methods of defining options. I use the TGA and PSD formats, but Adam Pletcher has a good example of PNG save options.

This is the code to define TGA and PSD save options, and how to execute it:

doc = psApp.activeDocument
# Define 24bit Targa save options
options = comtypes.client.CreateObject('Photoshop.TargaSaveOptions')
PsTargaBitsPerPixels = 24
options.Resolution = PsTargaBitsPerPixels
options.AlphaChannels = False
options.RLECompression = False 

# Define 32bit Targa save options
optionsAlpha = comtypes.client.CreateObject('Photoshop.TargaSaveOptions')
PsTargaBitsPerPixels = 32
optionsAlpha.Resolution = PsTargaBitsPerPixels
optionsAlpha.AlphaChannels = True
optionsAlpha.RLECompression = False 

# Define PSD save options
psdOptions = comtypes.client.CreateObject('Photoshop.PhotoshopSaveOptions')
psdOptions.annotations = False
psdOptions.alphaChannels = True
psdOptions.layers = True
psdOptions.spotColors = True
psdOptions.embedColorProfile = True

#Save in the different ways
#PSD
doc.SaveAs('c:\\yourpath\\filename.psd',psdOptions)

#TGA
doc.SaveAs('c:\\yourpath\\filename.tga', options)

#TGA32
doc.SaveAs('c:\\yourpath\\filename.tga', optionsAlpha)


I have found in photoshop Cs2 that in order to save as a TGA from a layered PSD, you have to flatten the document before saving it, otherwise it will think you want to save it as a copy, which has to be one of the most annoying things ever. Without going into all the hacky work arounds, here is a good chance to bring up one final handy thing... how to undo hacky work-arounds with history states!

Here is how you can save and recall a history state in a document using python:

#save the current setup
saved_state = doc.activeHistoryState

#Do some stuff, like flatten the document and copy it...
doc.flatten()
doc.activeLayer.Copy()

#Now revert to the saved state and paste the merged copy in as a new layer
doc.ActiveHistoryState = saved_state 

doc.Paste()


Finally, calling your script from Photoshop is pretty important if you want the artists not to hate you for making them use the console (more likely they just won't use your tool!).

I have yet to find a way to execute my Python script directly, but you can use intermediate scripts to call them from an action. I will readily admit this is a clumsy way of doing it, but if anyone has a more streamlined approach to this I would love to know!

First, make a .bat file that references your python script... the following script has the handy addition that it references the scripts directory relative to it's current location using %~dp0.

@echo off
call python %~dp0\\yourPythonScript.py

Then, you can have a Photoshop javascript call the bat file...

var script = new File("/c/scriptLocation/run_yourPythonScript.bat");

script.execute();


Finally, you have a photoshop action that calls the javascript.

Pros to this method:
Callable from within Photoshop via an action.
It works!

Cons:
-Convoluted setup.
-Calls up a cmd window on execution (which can be good for error feedback, but is also a little irritating). 

To finish off with, here is a full example of a working Python script that makes use of some of the code listed above. This script creates a new document, complete with pre-named groups and art layers, ready for an artist to work on.

##############################################################################
# 
# Photoshop doc setup script
# Author: Pete Hanshaw, 2012
# http://peterhanshawart.blogspot.com.au/
#
##############################################################################
#
# Creates a new PSD document and sets up placeholder groups and layers 
# ready for texture work.
#
# Creates:
# -'n' group for normal map
# -'s' group for specular map
# -'d' group for diffuse map
#
# Requires comtypes:
# http://sourceforge.net/projects/comtypes/
#
##############################################################################

#Import required modules
import comtypes.client
from sys import exit
import pythoncom

#Set up the document. More groups can be added easily by creating more dict keys
group_names = {}

#Group name : artLayer name
group_names['d'] = 'dif_base'
group_names['s'] = 'spec_base'
group_names['n'] = 'nrm_base'

#Begin the script            
if (__name__ == '__main__'):
    #COM dispatch for Photoshop
    psApp = comtypes.client.CreateObject('Photoshop.Application')

    #Define the fill colors
    #Normal base
    nrm_SolidColor = comtypes.client.CreateObject('Photoshop.SolidColor')
    nrm_SolidColor.rgb.red = 128
    nrm_SolidColor.rgb.green = 128
    nrm_SolidColor.rgb.blue = 255

    #Define the fill colors
    #Spec base
    spec_SolidColor = comtypes.client.CreateObject('Photoshop.SolidColor')
    spec_SolidColor.rgb.red = 0
    spec_SolidColor.rgb.green = 0
    spec_SolidColor.rgb.blue = 0
    
    #Define the fill colors
    #Diffuse base
    dif_SolidColor = comtypes.client.CreateObject('Photoshop.SolidColor')
    dif_SolidColor.rgb.red = 128
    dif_SolidColor.rgb.green = 128
    dif_SolidColor.rgb.blue = 128

    #Set the default unit to pixels!
    psApp.Preferences.RulerUnits = 1
    
    #w, h, res, name, mode, initial-fill, asp-ratio, Bits-P/Ch, ColProfile
    new_doc = psApp.Documents.Add(1024, 1024, 72, "new_source_texture", 2, 1, 1)

    print "Setting up a", new_doc.name
    
    for group, layer in group_names.items():

        new_layerSet = new_doc.LayerSets.Add()
        new_layerSet.name = group

        if group == 'n':
            new_art_layer = new_layerSet.ArtLayers.Add()
            new_art_layer.name = layer
            
            #Fill the layer with appropriate color
            #Filltype, model, opacity, preserveTransparancy
            psApp.activeDocument.selection.selectAll
            psApp.activeDocument.selection.Fill(nrm_SolidColor)
            new_layerSet.Visible = False

        elif group == 's':
            new_art_layer = new_layerSet.ArtLayers.Add()
            new_art_layer.name = layer
            
            #Fill the layer with appropriate color
            #Filltype, model, opacity, preserveTransparancy
            psApp.activeDocument.selection.selectAll
            psApp.activeDocument.selection.Fill(spec_SolidColor)
            new_layerSet.Visible = False

        elif group == 'd':
            new_art_layer = new_layerSet.ArtLayers.Add()
            new_art_layer.name = layer
            
            #Fill the layer with appropriate color
            #Filltype, model, opacity, preserveTransparancy
            psApp.activeDocument.selection.selectAll
            psApp.activeDocument.selection.Fill(dif_SolidColor)

        else:
            new_art_layer = new_layerSet.ArtLayers.Add()
            new_art_layer.name = layer
            
            #Fill the layer with appropriate color
            #Filltype, model, opacity, preserveTransparancy
            psApp.activeDocument.selection.selectAll
            psApp.activeDocument.selection.Fill(dif_SolidColor)

    #Reorder the photoshop layers so that it reads from top down:
    #normal, spec, diffuse
    dGroup = psApp.activeDocument.layerSets['d']

    #See Adobe's photoshop_cs4_vbscript_ref.pdf to make sense of this-
    dGroup.Move(new_doc, 2)

    #Deselect the fill area
    psApp.activeDocument.selection.deselect

    #Set the active layer to the diffuse
    psApp.activeDocument.activeLayer = (psApp.activeDocument.layerSets['d'].
                                        artLayers['dif_base'])
    
    print "All done!"
    exit(1)


So that's just a handful of useful bits and bobs. If you try any of the code here, and it doesn't work, let me know and I'll update the post with a fix.

-Pete