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
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
top
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):
By the way, don't leave that plug-in installed all the time! It creates very large files very quickly.
top
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
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?
top
top
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
top
As you will see, some enums share a common value; they are therefore equivalent. Here is
a small excerpt from this list:
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
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.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.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.
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.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.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.
MyActionDescriptor.PutEnumerated( objApp.CharIDToTypeID( "ClrS" ),
objApp.CharIDToTypeID( "ClrS" ),
objApp.CharIDToTypeID( "SMPT" ));
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.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?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.
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.
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).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.
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.
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.