Maybe someday I'll make this look better but for now this is it.

Contents

Introduction
Accessing the Photoshop CS Interface via COM
Hello Photoshop
Tougher Stuff
Needle in a Haystack
Interpreting ScriptingListenerVB.log
Decoding Parameters to CharIDToTypeID()
Using the Adobe Photoshop 8.0 Type Library
A Cleaner Camera Raw File Load
Rosetta Stone
Photoshop and .NET Unleashed
Further Collaboration
Links

Introduction

There's a positive dearth of information on how to control Photoshop CS via its COM interface, particularly if you don't intend to use AppleScript, JavaScript or VB. Yet anything you can do in those languages, you can do via any .NET language.

I'm not going to do a full-on tutorial, but I will lay out some of the things I've found as I've started to explore programming PhotoShop. Part of this comes because I'm a confirmed .NET user, and would rather write my workflow apps in C# than in VB or JavaScript. Part of it comes because Photoshop is one heck of a workhorse, but automating it can make it a real timesaver.

The first documentation you come to regarding using the COM objects exposed by Photoshop is, uh, well, nothing. So at least this might give someone a little help.

top

Accessing the Photoshop CS Interface via COM

I'm going to tell you how I did it in Microsoft Visual C# .NET using Microsoft .NET Framework 1.1.

I personally prefer command-line programs for the tasks I set myself, but it's not really important. So the first thing, after you've created your project, is to add references to the Adobe DLLs to your project. Using Solution Explorer, right-click on "References" under your project's name. The Add Reference dialog should pop up. Across the top of the box, there are three tabs (.NET, COM and Projects). Click the COM tab (Adobe doesn't supply a PIA for Photoshop -- if you don't know what that is, don't worry about it).

On the COM tab you'll find a list of COM objects available to your .NET applications. The ones you want, of course, are from Adobe. On my machine, they're listed as "Adobe Photoshop 8.0 Object Library" and "Adobe Photoshop 8.0 Type Library". Which one do you want? Well, you can do pretty much everything with just the Object Library, but the Type Library is handy too. Highlight them both, click select, then OK and you've access to the objects.

If you now build your application, you'll see two additional DLLs produced in your bin directories: Interop.Photoshop.dll and Interop.PhotoshopTypeLibrary.dll. You'll need to keep these for your app to run, and redistribute them with your executable if you're sharing with anyone else.

top

Hello Photoshop

Here is minimal source to show Photoshop access in action. All it does is access Photoshop (it'll start it if it's not running) and list the physical dimensions of all open documents.

using System;
using ps = Photoshop;

ps.ApplicationClass app = new ps.ApplicationClass();
app.Preferences.RulerUnits = ps.PsUnits.psPixels; // don't tell me this in centimeters
ps.Documents doc_arr = app.Documents;
foreach (ps.Document doc in doc_arr)
{
    Console.WriteLine(doc.FullName);
    Console.WriteLine(  "{0} x {1}",doc.Width,doc.Height);
}
Using the ps alias for the Photoshop namespace, you can access all the objects in that namespace.

top

Tougher Stuff

I wanted to load a .NEF image created by my Nikon D70. Sure, I could code this:
ps.Application app = new ps.ApplicationClass();
app.Preferences.RulerUnits = ps.PsUnits.psPixels;
app.DisplayDialogs = ps.PsDialogModes.psDisplayNoDialogs;
app.Load("D:\\pcpix\\__shoots\\ABC\\myfiles\\RawImages\\ABC0274.NEF");
But in spite of configuring Photoshop not to show me dialogs, still the Camera Raw dialog box popped up.

There is, of course, no documentation I found on scripting the Camera Raw dialog box more effectively. So thank goodness for ScriptListener.

There are better tutorials on this subject, but the quick upshot is, shut down Photoshop and add the ScriptListener plug-in to Photoshop. On my machine, the command line's as simple as this (your machine may not use the same paths, of course):

copy "C:\Program Files\Adobe\Photoshop CS\Scripting Guide\Utilities\ScriptListener.8li" "C:\Program Files\Adobe\Photoshop CS\Plug-Ins\Adobe Photoshop Only\Automate"
Then restart Photoshop, and every single thing you do in the UI will end up recorded to two files in the root of the drive that holds Photoshop. So on my machine, they were recorded as C:\ScriptingListenerJS.log and C:\ScriptingListenerVB.log. The first is a record of your actions as expressed in JavaScript, and the second is in VB syntax.

By the way, don't leave that plug-in installed all the time! It creates very large files very quickly.

top

Needle in a Haystack

Before we look at an excerpt from such a log, here's a quick tip that might save you some time.

Photoshop produces a huge amount of data when you record all its actions. Even if you only do the minimum you need to in order to capture your critical action so you can figure out how to do it programmatically, you may find it hard to locate among all the other text. Goodness help you if you "scrub" a value, like layer opacity, with the mouse. I generated over 200K of text scrubbing that value from 33% to 100% back to 33% again. So how was I able to easily locate the code snippet reproduced below? There's a simple way to put little markers into the log file.

In addition to the document you're really working on, keep a small trivial 8-bit unlayered image around, and just before you do the operation you're really targeting, do a quick Save As using the Photoshop UI, and give the file you create a name like SequencePoint01. Do the action whose code you're trying to examine, then to another quick SaveAs of your trivial file, and call it SequencePoint02. If you're wanting to reproduce a whole series of steps, just continue making sequence points, and write down what you do between each of them; you'll find it quite easy to determine exactly which actions correspond to which code.

So let's look at some actual code recorded by ScriptListener.

top

Interpreting ScriptingListenerVB.log

Because this interface is really just recording the sorts of data the Actions tools use, it's not readable at all. Here is the output for the single action of opening my .NEF file via Camera Raw:

REM =======================================================
DIM objApp
SET objApp = CreateObject("Photoshop.Application")
REM Use dialog mode 3 for show no dialogs
DIM dialogMode
dialogMode = 3
DIM id15
id15 = objApp.CharIDToTypeID( "Opn " )
    DIM desc4
    SET desc4 = CreateObject( "Photoshop.ActionDescriptor" )
    DIM id16
    id16 = objApp.CharIDToTypeID( "null" )
    Call desc4.PutPath( id16, "D:\pcpix\__shoots\ABF\myfiles\RawImages\ABF0008.NEF" )
    DIM id17
    id17 = objApp.CharIDToTypeID( "As  " )
        DIM desc5
        SET desc5 = CreateObject( "Photoshop.ActionDescriptor" )
        DIM id18
        id18 = objApp.CharIDToTypeID( "CMod" )
        Call desc5.PutString( id18, "Nikon D70" )
        DIM id19
        id19 = objApp.CharIDToTypeID( "Sett" )
        DIM id20
        id20 = objApp.CharIDToTypeID( "Sett" )
        DIM id21
        id21 = objApp.CharIDToTypeID( "Img " )
        Call desc5.PutEnumerated( id19, id20, id21 )
        DIM id22
        id22 = objApp.CharIDToTypeID( "ClrS" )
        DIM id23
        id23 = objApp.CharIDToTypeID( "ClrS" )
        DIM id24
        id24 = objApp.CharIDToTypeID( "SMPT" )
        Call desc5.PutEnumerated( id22, id23, id24 )
        DIM id25
        id25 = objApp.CharIDToTypeID( "BtDp" )
        DIM id26
        id26 = objApp.CharIDToTypeID( "BtDp" )
        DIM id27
        id27 = objApp.CharIDToTypeID( "BD16" )
        Call desc5.PutEnumerated( id25, id26, id27 )
        DIM id28
        id28 = objApp.CharIDToTypeID( "Sz  " )
        Call desc5.PutInteger( id28, 3008 )
        DIM id29
        id29 = objApp.CharIDToTypeID( "Rslt" )
        Call desc5.PutDouble( id29, 240.000000 )
        DIM id30
        id30 = objApp.CharIDToTypeID( "ReUn" )
        DIM id31
        id31 = objApp.CharIDToTypeID( "ReUn" )
        DIM id32
        id32 = objApp.CharIDToTypeID( "PpIn" )
        Call desc5.PutEnumerated( id30, id31, id32 )
    DIM id33
    id33 = objApp.StringIDToTypeID( "Adobe Camera Raw" )
    Call desc4.PutObject( id17, id33, desc5 )
Call objApp.ExecuteAction( id15, desc4, dialogMode )
I don't find it easy to understand, so though I could probably copy the actual script out or even translate it to C#, knowing how to do something slightly different could be rather difficult.

Aside from the obscuring, hard-to-understand variable names (well, how could a tool like ScriptListener really be much smarter?) like id22 and id25, the data from which they come is pretty much as dense. When they call desc5.PutEnumerated(id22,id23,id24), it's not much clearer even if I don't worry about the bad variable names. How's this for comprehensibility?

MyActionDescriptor.PutEnumerated(  objApp.CharIDToTypeID( "ClrS" ),
                                   objApp.CharIDToTypeID( "ClrS" ),
                                   objApp.CharIDToTypeID( "SMPT" ));

top

Decoding Parameters to CharIDToTypeID()

Well, it turns out that this call can be recoded as the following:
MyActionDescriptor.PutEnumerated(  (int)K.phTypeColorSpace,
                                   (int)K.phTypeColorSpace,
                                   (int)K.phEnumAdobeRGB1998);
It's the same call, the same information, but honestly I understand one and don't understand the other.

top

Using the Adobe Photoshop 8.0 Type Library

In the earlier code snippet, we added one using statement:
using ps = Photoshop;
If we add two more such statements to access the type library, we gain access to an enumerated value that defines many constants used with various Photoshop function calls:
using ps = Photoshop;
using      PhotoshopTypeLibrary;
using K  = PhotoshopTypeLibrary.PSConstants;
This gives you access to pst.PSConstants (with an alias of K), an enum with many, many (2003!) enumerated names. You can explore them with Visual Studio's object browser, but I found absolutely no documentation of any of these values or their uses. So how can you use them to clean up the output of ScriptListener?

It turns out that each of these values, like all enums, maps to a numeric value. And the enum value of PSConstants.phTypeColorSpace is the same as the number returned by objApp.CharIDToTypeID("ClrS").

Decoding these is as simple as taking the value returned by the call to CharIDToTypeID() and using the .NET Enum functions to convert that value back into the enum string for PSConstants.

It turns out that this only works sometimes. Many values that are be supplied to CharIDToTypeID() and StringIDToTypeID() map to a valid PSConstants ID, but many do not ( like StringIDToTypeID("Adobe Camera Raw") ). In those cases, you cannot use a PSConstants value to replace the output of the function as used in the ScriptListener output.

top

A Cleaner Camera Raw File Load

Here is the code after I did the translations and got rid of extraneous variables:
ps.ActionDescriptorClass ad = new Photoshop.ActionDescriptorClass();

// specify the filename
ad.PutPath(         (int)K.phEnumNull,
          "D:\\pcpix\\__shoots\\ABF\\myfiles\\RawImages\\ABF0008.NEF");

ps.ActionDescriptorClass ad2 = new Photoshop.ActionDescriptorClass();
ad2.PutString(      app.CharIDToTypeID("CMod"),              // camera model
                    "Nikon D70");
ad2.PutEnumerated(  (int)K.phTypeColorSpace,                 // color space
                    (int)K.phTypeColorSpace,
                    (int)K.phEnumAdobeRGB1998);
ad2.PutEnumerated(  (int)K.phTypeBitDepth,                   // bit depth
                    (int)K.phTypeBitDepth,
                    (int)K.phEnumBitDepth16);
ad2.PutInteger(     (int)K.phKeySizeKey,                     // the width... why?
                    3008);
ad2.PutDouble(      (int)K.phKeyResolution,                  // DPI
                    240.0);
int ReUnID = app.CharIDToTypeID("ReUn");                     // ???
ad2.PutEnumerated(  ReUnID,                                  // maybe this forces it
                    ReUnID,                                  // to close without interaction?
                    app.CharIDToTypeID("PpIn"));             // ...or use file's settings?

ad.PutObject(       (int)K.phKeyAs,                          // identifies the target object
                    app.StringIDToTypeID("Adobe Camera Raw"),
                    ad2);
app.ExecuteAction(  (int)K.phEventOpen,                      // what to do
                    ad,
                    (int)ps.PsDialogModes.psDisplayNoDialogs); // maybe this suppresses the dialog
Now, I've not got it all figured out (mostly because I don't need to solve the remaining questions), but I can certainly parameterize the things likely to differ from invocation to invocation and wrap this in a clean function. More particularly, I know how to modify the things I don't like as they are... like changing the bit depth to phEnumBitDepth8.

top

Rosetta Stone

Once I realized how much I could learn from the ScriptListener output if I could understand it better, I decided to decode all the strings I could. Fortunately, it's easy with C# to programmatically obtain an exhaustive list of the names in an enum and their associated values, so I wrote a program that lists all of the enum names and values. For each enum value, the program also calls TypeIDToCharID() and TypeIDToStringID() to find out what string each enum corresponds to.

As you will see, some enums share a common value; they are therefore equivalent. Here is a small excerpt from this list:

                         PSConstants ->      value -> CharID  TypeIDToStringID
                          ---------- ->      ----- -> ------  ----------------
...
                     phEnumBitDepth1 -> 1111765280 ->   BD1   bitDepth1
                    phEnumBitDepth16 -> 1111765302 ->   BD16  
                    phEnumBitDepth24 -> 1111765556 ->   BD24  bitDepth24
                    phEnumBitDepth32 -> 1111765810 ->   BD32  
                     phEnumBitDepth4 -> 1111766048 ->   BD4   bitDepth4
                     phEnumBitDepth8 -> 1111767072 ->   BD8   bitDepth8
...
                phEnumChannelOptions -> 1130917455 ->   ChnO  channelOptions
        phEnumChannelsPaletteOptions -> 1130917456 ->   ChnP  channelsPaletteOptions
                      phClassChannel -> 1130917484 ->   Chnl  channel
                        phKeyChannel
                       phTypeChannel
                       phKeyChannels -> 1130917491 ->   Chns  channels
                   phKeyCharcoalArea -> 1130918465 ->   ChrA  charcoalArea
                     phEventCharcoal -> 1130918499 ->   Chrc  charcoal
...
From this you can see that the enum K.phEnumChannelOptions has the value 1130917455, which is also returned by CharIDToTypeID("ChnO"), and StringIDToTypeID("channelOptions"). (I got the strings by calling TypeIDToCharID(1130917455) and TypeIDToStringID(1130917455), thus the column headers).

You can also see that the value 1130917484, corresponding to CharIDToTypeID("Chnl") and StringIDToTypeID("channel"), has three different PSConstants enum names that all produce the same value: phClassChannel, phKeyChannel, and phTypeChannel. Use the one that seems best to you.

I've sorted this list four different ways and have sometimes found each one handy. Here they are sorted by PSConstants enum name, PSConstants enum value, result of TypeIDToCharID(), and result of TypeIDToStringID().

top

Photoshop and .NET Unleashed

C# with .NET is a useful, powerful combination. If you want to really put Photoshop through its paces, the very useful (if a bit hard to parse) output of ScriptListener will give you access to anything you can do with Photoshop Actions. I hope this information makes this just a tiny bit easier for you.

top

Further Collaboration

There don't seem to be too many people leveraging Photoshop with .NET, but it can be a potent combination. I'd welcome any feedback or suggestions -- my email is john, at the domain listed at the bottom of this page.

top


Links

Photoshop Constants Rosetta Stone sorted by PSConstants
enum, PSConstants value, TypeIDToCharID(), TypeIDToStringID()
Contents copyright 2005 John Deurbrouck and pcpix.com.
Use and distribute freely with notice intact.