Driving Photoshop from C#

Fri, Jun 25, 2004

Update: 8/29/04 -- There is an updated version of this tool described here.

If you've been a regular reader, you know that I've been having a blast playing with making digital panoramas.  The problem that I've been hitting is twofold:

  1. When I want to use enblend to do the blending, I don't get the component layers in photoshop.  I get a flat blended tiff file that usually looks great.  However, if I want to tweak it I have to manually find the tiff file and import that to a layer so that I can paint the original over the endblended result.
  2. PanoTools can work with 16bit files just fine but it doesn't know how to create a Photoshop PSD file.  (16 bit layers were only added in Photoshop CS.

Both of these require the fairly manual process of loading up a bunch of tif files and copying them in to one composite file with masks.

Being a dork, I wrote a program to automate this from the outside.  This is a command line utility that drives photoshop from the outside.  It would be cool if this were a plugin that would go in the "Automate" menu but I didn't have time to figure that out. It just grabs a bunch of files, opens them up one by one and puts them in to a new PS file.   This works well with the individual .tif files that PanoTools creates.  If you have feature suggestions let me know and I'll see what I can do.

In any case, here is the binary, source and license.  Please be aware that this requires the version 1.1 .Net runtime.  You can get this from windows update if you don't already.  Here is more info on getting the runtime.

Interesting notes on programming to Photoshop:

There are two ways to automate photoshop (that I've found).  The first is using "Actions".  This is the built in way to record a bunch of steps and replay them.  There is a bunch of UI for this and it is good for simple things.  Actions are to limiting for this problem as anything that iterates over multiple files or has "if" statements is super hard to do with Actions.

The second way to program is via a set of COM interfaces.  Adobe installs a type library that provides some nice object model to do quite a few things.  My understanding is that this is exposed via a built in javascript engine and via applescript on Macs.  Since COM is language neutral, you can drive it (for example) via the scripting host (JavaScript, VBScript),  perl, VB or C#.  I choose to use C#.

The programmable interface has some pretty glaring holes in its support.  For example, you can't do anything with the tools such as the magic wand.  There is also no easy programmatic access to creation of layer masks.  You can get around this by installing a plugin called the "ScriptListener."  This essentially creates funky unreadable code that (I'm thinking) uses the same hooks as the Actions code.  On windows this is written out as VB or JavaScript.  Here is an example from "c:\ScriptingListenerJS.log" to create a layer mask on the current layer.

// =======================================================
var id375 = charIDToTypeID( "Mk  " );
    var desc84 = new ActionDescriptor();
    var id376 = charIDToTypeID( "Nw  " );
    var id377 = charIDToTypeID( "Chnl" );
    desc84.putClass( id376, id377 );
    var id378 = charIDToTypeID( "At  " );
        var ref66 = new ActionReference();
        var id379 = charIDToTypeID( "Chnl" );
        var id380 = charIDToTypeID( "Chnl" );
        var id381 = charIDToTypeID( "Msk " );
        ref66.putEnumerated( id379, id380, id381 );
    desc84.putReference( id378, ref66 );
    var id382 = charIDToTypeID( "Usng" );
    var id383 = charIDToTypeID( "UsrM" );
    var id384 = charIDToTypeID( "RvlA" );
    desc84.putEnumerated( id382, id383, id384 );
executeAction( id375, desc84, DialogModes.NO );

As you can see, this isn't the easiest code to read. The solution is to wrap it in functions that you can call at will. Here is how I created a C# function out of this:

void CreateLayerMask()
{
    int id375 = app.CharIDToTypeID( "Mk  " );
    PS.ActionDescriptor desc84 = new PS.ActionDescriptor();
    int id376 = app.CharIDToTypeID( "Nw  " );
    int id377 = app.CharIDToTypeID( "Chnl" );
    desc84.PutClass( id376, id377 );
    int id378 = app.CharIDToTypeID( "At  " );
    PS.ActionReference ref66 = new PS.ActionReference();
    int id379 = app.CharIDToTypeID( "Chnl" );
    int id380 = app.CharIDToTypeID( "Chnl" );
    int id381 = app.CharIDToTypeID( "Msk " );
    ref66.PutEnumerated( id379, id380, id381 );
    desc84.PutReference( id378, ref66 );
    int id382 = app.CharIDToTypeID( "Usng" );
    int id383 = app.CharIDToTypeID( "UsrM" );
    int id384 = app.CharIDToTypeID( "RvlA" );
    desc84.PutEnumerated( id382, id383, id384 );
    app.ExecuteAction( id375, desc84, PS.PsDialogModes.psDisplayNoDialogs );
}

This is a little bit of a pain but at least you can get stuff done.  It would be cool if we could build a library of these things.  If anyone else is interested in driving photoshop let me know and I'll help bring this stuff together.