**Photoshop C++ Recipes** Hi. If you read the title of this document and are wondering if I'm going to be introducing some delicious recipes for your next weekend brunch, the answer is no. Instead, I'll be going over some seemingly innocuous tasks that you might find yourself doing should you ever have the unfortunate task of having to work on native Photoshop plugins. Just so you can know at a glance if this tutorial is for you, I'll be covering the following topics: * Getting started creating native (C++) Photoshop plugins in a minimalist manner; * Reading layer data directly within Photoshop entirely through C++; * Combining an Automation to run a "hidden" Filter plugin; * Communicating between a traditional CEP panel in Photoshop and a native C++ plugin using Adobe native CSXS events; Some of these recipes are already in the samples included in the SDK, but they're pretty obtuse and not necessarily implemented in an easy-to-understand manner (at least, it took me a while to grok them). Hopefully the recipes here will be far less bloated. # Code Repository # All the source code for this tutorial is available [here](https://github.com/sonictk/ps_cpp_recipes). Feel free to inspect the source code while following along with this tutorial, since the sources have more comments to help document what certain code is meant to accomplish; the sources here are slightly more slimmed-down in order to enhance readability. A word of caution: this code is written in a very tutorial-focused, non-production manner. This means that there is very little error-checking/handling going on, with brevity and readability favoured over other options that might be more performant and/or scalable. The code is available as a reference, and should not be taken as an indicator of what good robust code shipped to end-users should look like. # Motivation # ## Why not use CEP instead? ## If you've had any experience working with Photoshop development before, you'll likely be familar with the [Adobe CEP framework](https://github.com/Adobe-CEP), which is basically's Adobe idea of allowing developers to extend Photoshop through the "modern" set of web technologies, and being able to share those creations across the entire Adobe suite. Unfortunately (as with most over-complicated things in programming), while this sounds very nice in theory, it's an unmitigated disaster in practice. While I'll explain in detail later on, here's a brief list of bullet points: * For reasons which will be explained later, CEP is _slow_. It's to the point where for certain operations (not even at scale), it's so slow in execution as to be unusable (e.g. try checking which layers are locked in a document with >200 layers.) * Debugging the CEP JavaScript/Adobe JSX layers is incredibly time-consuming and sometimes also impossible, depending on the exposed API calls you're attempting to make. Also, if there's a bug in the CEP/JS layer itself, you likely will have no chance of solving it on your own without Adobe's help (and good luck with that!). On the compiled side of things, as you'll see later on, there's some seemingly insurmountable problems that _can_ be solved with a bit of ingenuity. * It's JavaScript. Which probably sounded like a good idea to some product manager back in the day, but now just serves to be a major pain even when programming in it under normal circumstances. I'll take having an optimizing compiler over a JIT language any day, thank you very much. # Requirements # This tutorial will focus on writing only the Windows version of any plugins, since Photoshop is only supported on Windows/OSX and really, the concepts I'm going to illustrate are OS-agnostic. However, in the event that I find it useful to cover information regarding those platforms, it will appear in the following format: !!! tip Crossing the platforms Platform-specific information goes here. ## What you will need ## - Windows 10 and Photoshop CC 2019/2020 or later. - A **C/C++ development environment** set up and ready to go. (If you want to see what my Emacs setup looks like, it's available [here](https://github.com/sonictk/lightweight-emacs).) - MSVC 2017 (Photoshop CC 2019/2020). !!! warning MSVC Compiler versions You should use the recommended MSVC compiler that Photoshop was built with. Information on this is available in the SDK documentation, under: `pluginsdk/documentation/html/buildinfo.html`. - The Adobe Photoshop SDK for your particular version of Photoshop. They can be downloaded from the following location: https://console.adobe.io/downloads (You'll need an Adobe account.) Versions prior to CC 2014 may be downloaded from the following location: https://www.adobe.com/devnet/photoshop/sdk.html - The Adobe CEP SDK for your particular version of Photoshop. This can also be downloaded from the [same earlier location](https://console.adobe.io/downloads) as the Photoshop SDK. ## What you should know ## - **Competency with C/C++**. This tutorial will not focus on basics, and is meant to be more of a quickstart guide for programmers already experienced with C++ to dive into the specifics of Photoshop plugin development. - **Basic knowledge of JavaScript**. We will be writing a little JavaScript code here and there (simply to demonstrate usage of CEP, since that is still to date the only officially recommended way to create GUIs in Photoshop), so you'll need to be able to grok the syntax. - **Basic knowledge of Photoshop**. Since we're going to be writing plugins for it, it would be helpful if you knew what a layer was, for example, or what a filter is. Let's get started with the various recipes. Feel free to skip ahead if you're not interested in a particular topic. # Recipe: Minimalist approach to Photoshop plug-ins # This recipe will teach you how to create a native C++ Photoshop plugin from scratch and without involving an IDE; this will be as minimalist a setup as possible. ## Getting the documentation ## The documentation for the Photoshop SDK is fairly old and hasn't been updated regularly in a while, but it's still useful in some ways. Your copy of the documentation (`documentation.html`) is included in the SDK that you downloaded. For initial reading, find the page on **Types of plug-in modules** and go over the different types of Photoshop plug-ins that are supported, and their corresponding limitations. Basically, Photoshop segregates the different types of plug-ins that one can write to extend the functionality of the host program, and each type of plug-in has a different entry point that Photoshop is expecting to call, along with a different defined file extension. For example, **filter** plug-ins (the kind that show up in the **Filter** menu, which basically run math operations on your layer's image pixels) have the entry point signature of: ~~~~~~ DLLExport MACPASCAL void PluginMain(const int16_t selector, FilterRecordPtr filterRecord, long long *data, int16_t *result); ~~~~~~ and a file extension of `.8bf`, whereas **automation** plug-ins have a different entry point that Photoshop calls: ~~~~~~ DLLExport SPAPI SPErr AutoPluginMain(const char* caller, const char* selector, void* message); ~~~~~~ and a file extension of `.8li` instead. If you're attempting to write one of the other types of plugins, I suggest taking a look at the sample code, also provided with the documentation. ## Hello, world ## Let's go ahead and create our first Photoshop plugin. To make things simple, we'll be making an **Automation** plug-in, which is generally meant for tools or other, as the name implies, automated processes to occur on Photoshop documents. You can refer to the various samples within the SDK to see more about Automation plugins in general, but for the purposes of this tutorial, go ahead and create `tutorial_automation_main.cpp`, and fill it with the following code: ~~~~~~cpp SPBasicSuite *sSPBasic = NULL; SPErr UninitializePlugin() { PIUSuitesRelease(); return status; } DLLExport SPAPI SPErr AutoPluginMain(const char* caller, const char* selector, void* message) { SPErr status = kSPNoError; SPMessageData *basicMessage = (SPMessageData *)message; sSPBasic = basicMessage->basic; if (sSPBasic->IsEqual(caller, kSPInterfaceCaller)) { if (sSPBasic->IsEqual(selector, kSPInterfaceAboutSelector)) { DoAbout(basicMessage->self, AboutID); } if (sSPBasic->IsEqual(selector, kSPInterfaceStartupSelector)) { return kSPNoError; } if (sSPBasic->IsEqual(selector, kSPInterfaceShutdownSelector)) { status = UninitializePlugin(); } } if (sSPBasic->IsEqual(caller, kPSPhotoshopCaller)) { if (sSPBasic->IsEqual(selector, kPSDoIt)) { PSActionsPlugInMessage *tmpMsg = (PSActionsPlugInMessage *)message; } } return status; } ~~~~~~ While things here should be fairly self-explanatory, I'll go over a couple of slightly important bits that are useful to keep in mind as you continue development: ~~~~~~cpp SPBasicSuite *sSPBasic = NULL; ~~~~~~ This seems rather innocuous, but is actually required; it's used for the new/delete operators that are used whenever you accquire or release what Adobe refers to as **suites**. Suites are essentially libraries of functionality that are specific to a product i.e. Photoshop would have its own suite, along with Illustrator. As mentioned, you accquire and release suites whenever you use the functionality from a specific suite. In this case, we release the suite when we unload the plugin, in the `UninitializePlugin` call. The next few lines of code are also fairly straightforward: ~~~~~~cpp SPMessageData *basicMessage = (SPMessageData *)message; sSPBasic = basicMessage->basic; if (sSPBasic->IsEqual(caller, kSPInterfaceCaller)) { if (sSPBasic->IsEqual(selector, kSPInterfaceAboutSelector)) { DoAbout(basicMessage->self, AboutID); } if (sSPBasic->IsEqual(selector, kSPInterfaceStartupSelector)) { return kSPNoError; } if (sSPBasic->IsEqual(selector, kSPInterfaceShutdownSelector)) { status = UninitializePlugin(); } } if (sSPBasic->IsEqual(caller, kPSPhotoshopCaller)) { if (sSPBasic->IsEqual(selector, kPSDoIt)) { PSActionsPlugInMessage *tmpMsg = (PSActionsPlugInMessage *)message; } } ~~~~~~ When Photoshop calls our `AutoPluginMain` entry point, you can see that it passes in a `message` parameter. This message is basically used to tell the plugin what context it's being called in, and thus allow the plugin to react accordingly. So we see that we check against `kSPInterfaceAboutSelector`, `kSPInterfaceStartupSelector`, `kSPInterfaceShutdownSelector`, and `kPSPhotoshopCaller` in order to decide what to do (where, of course, that last one means that the user just activated the plug-in from the Automation menu in the Photoshop UI). Nothing should be too mystical or obtuse here. You'll notice here that we actually haven't done any code that will do printing of "Hello world" or anything of the sort yet. That's because Photoshop is not a console application on Windows, and thus we won't actually see anything if we just `printf` our way around. So for now, let's just write some simple code to launch a message dialog with the necessary text: ~~~~~~cpp #include #include #include #include #include #include #include #include #include #include #include #include #include #define WIN32_MAX_CLASS_NAME_LENGTH 256 SPBasicSuite *sSPBasic = NULL; HWND globalPSMainWindowHwnd = NULL; BOOL CALLBACK getPSMainWindowCB(HWND hwnd, LPARAM lParam) { char windowClassName[WIN32_MAX_CLASS_NAME_LENGTH]; GetClassNameA(hwnd, (LPSTR)windowClassName, WIN32_MAX_CLASS_NAME_LENGTH); if (strncmp(windowClassName, "Photoshop", WIN32_MAX_CLASS_NAME_LENGTH) == 0) { globalPSMainWindowHwnd = hwnd; } return TRUE; } SPErr UninitializePlugin() { PIUSuitesRelease(); return status; } DLLExport SPAPI SPErr AutoPluginMain(const char* caller, const char* selector, void* message) { SPErr status = kSPNoError; SPMessageData *basicMessage = (SPMessageData *)message; sSPBasic = basicMessage->basic; if (sSPBasic->IsEqual(caller, kSPInterfaceCaller)) { if (sSPBasic->IsEqual(selector, kSPInterfaceAboutSelector)) { DoAbout(basicMessage->self, AboutID); } if (sSPBasic->IsEqual(selector, kSPInterfaceStartupSelector)) { return kSPNoError; } if (sSPBasic->IsEqual(selector, kSPInterfaceShutdownSelector)) { status = UninitializePlugin(); } } if (sSPBasic->IsEqual(caller, kPSPhotoshopCaller)) { if (sSPBasic->IsEqual(selector, kPSDoIt)) { PSActionsPlugInMessage *tmpMsg = (PSActionsPlugInMessage *)message; BOOL status = EnumWindows(getPSMainWindowCB, (LPARAM)NULL); assert(status != 0); MessageBoxA(globalPSMainWindowHwnd, "Hello World!", "Tutorial Dialog", MB_OK|MB_ICONINFORMATION); } } return status; } ~~~~~~ So the code exploded a little bit, but none of this should be unfamilar to anyone who's written Win32 code before; we basically enumerate the open windows of the desktop to see which window matches the Photoshop one (since Photoshop disallows itself from running multiple instances, this hack works fine**, and launch a message dialog with our text. Ok, so far the code is fairly straightforward. Now what combination of compiler flags and post-build steps do we need to make this a DLL that Photoshop can load? ## Overview of the build process ## Before we start writing the build script, there's a few wrenches in the works that Adobe has thrown in when it comes to building our plug-in. Here's the summary of what you'll need to do in order to get your plug-in to load in Photoshop, illustrated in the diagram below: ******************************************************************** * +---------------------+ * * | | (C code that just tells Photoshop * * | PiPL source file | how to load your plug-in and other * * | | metadata associated with it) * * +----------+----------+ * * | * * | preprocessor (MSVC) * * | * * | * * +----------v------------+ * * | | * * | .rr file | * * | | (Your actual plug-in code) * * +-----------+-----------+ * * | +----------------------+ * * | Cvntpipl.exe | | * * | | .c/.cpp source(s) | * * | | | * * +-----------v------------+ +-----------+----------+ * * | | | * * | .rc Windows Resource | | * * | script | | MSVC compiles * * +-----------+------------+ | * * | | * * | Resource Compiler | * * | (rc.exe) +-----------v-----------+ * * | | | * * +-----------v------------+ | .obj files | * * | | | | * * | .res resource file | +------------+----------+ * * | | | * * +----------+-------------+ | * * | | * * | | * * +-----------------+-----------------+ * * | * * | MSVC linker * * +----------------v------------------+ * * | | * * | DLL plug-in (.8li, .8bf etc) | * * | that actually gets loaded into PS | * * +-----------------+-----------------+ * * +----------------------+ | * * | | | * * | .p12 certificate +----+ ZXPSignCmd.exe * * +----------------------+ | * * | * * +----------------v-------------------+ * * | | * * | .zxp archive | * * | that actually gets distributed | * * +------------------------------------+ * * * ******************************************************************** [Figure [diagram]: An overview of what it takes to create a Photoshop plug-in.] It's rather involved, as you can see, which is unfortunate. You'll also notice that I reference some executables which are probably unfamiliar, notably `Cvntpipl.exe` and `ZXPSignCmd.exe`. The former is available in the Photoshop SDK, while the latter is available in the CEP SDK. I won't go into a discussion of _why_ things are this way; for now, we'll just accept it as a by-product of Photoshop's legacy. (Also because if I _did_ go into a discussion about it, I'd probably never finish writing this document.) There's also something here that might seem unfamilar, namely the PiPL properties file and `.res` resource file that the **Resource Compiler** creates. Shorthand for **Plug-in Property Lists**, the former is basically a C file that contains various types of information about your plug-in (i.e. what type of plug-in it is, who the author is, how Photoshop should load it), that eventually gets transformed into a `.res` resource file and then built into the final DLL through the (needlessly convoluted) steps listed above. Really, the entire branch on the left is all about just adding plug-in metadata so that Photoshop knows how to treat our plug-in. That's it. !!! tip Crossing the platforms On OSX, you'll make use of [Rez](https://www.unix.com/man-page/osx/1/Rez/) to compile the PiPL file instead. I'll not be covering it in this tutorial, since the steps are essentially the same, just using different OS tools to get the job done. ## Writing the PiPL ## So what exactly does a PiPL file look like, then? Luckily, the samples included in the documentation provide most of the starting points we need. First, let's define some globals in a file called `tutorial_automation_globals.h`: ~~~~~~cpp #define TUTORIAL_AUTOMATION_PLUGINNAME "AutomationTutorial" #define TUTORIAL_AUTOMATION_PLUGINDESC "An example automation plug-in for Adobe Photoshop." #define TUTORIAL_AUTOMATION_UUID "267c8093-d35c-4fb7-b0ae-7b224c7fc1ce" #define TUTORIAL_AUTOMATION_RESOURCE_ID 18601 #define TUTORIAL_AUTOMATION_SUITE_ID 'exam' #define TUTORIAL_AUTOMATION_CLASS_ID TUTORIAL_AUTOMATION_SUITE_ID #define TUTORIAL_AUTOMATION_EVENT_ID 'ExAm' #define TUTORIAL_AUTOMATION_VENDORNAME "memyselfandi" ~~~~~~ You can generate the UUID using any tool of your choice. On Windows, you can use ``uuidgen.exe``. The PiPL resource file then follows, like so: ~~~~~~cpp #include "PIDefines.h" #include "tutorial_automation_globals.h" #ifdef __PIMac__ #include "PIGeneral.r" #include "PIUtilities.r" #elif defined(__PIWin__) #define Rez #include "PIGeneral.h" #include "PIUtilities.r" #endif #include "PITerminology.h" #include "PIActions.h" resource 'PiPL' ( TUTORIAL_AUTOMATION_RESOURCE_ID, TUTORIAL_AUTOMATION_PLUGINNAME, purgeable) { { Kind { Actions }, Name { TUTORIAL_AUTOMATION_PLUGINNAME }, Category { "AdobeSDK" }, Version { (latestActionsPlugInVersion << 16) | latestActionsPlugInSubVersion }, Component { ComponentNumber, TUTORIAL_AUTOMATION_PLUGINNAME }, #ifdef __PIMac__ CodeMacIntel64 { "AutoPluginMain" }, #else #if defined(_WIN64) CodeWin64X86 { "AutoPluginMain" }, #else CodeWin32X86 { "AutoPluginMain" }, #endif #endif EnableInfo { "true" }, HasTerminology { TUTORIAL_AUTOMATION_CLASS_ID, TUTORIAL_AUTOMATION_EVENT_ID, TUTORIAL_AUTOMATION_RESOURCE_ID, TUTORIAL_AUTOMATION_UUID }, Persistent{}, // Only relevant if Persistent is set. Messages { startupRequired, doesNotPurgeCache, shutdownRequired, acceptProperty }, } }; //------------------------------------------------------------------------------- // Dictionary (scripting) resource //------------------------------------------------------------------------------- resource 'aete' (TUTORIAL_AUTOMATION_RESOURCE_ID, TUTORIAL_AUTOMATION_PLUGINNAME " dictionary", purgeable) { 1, 0, english, roman, /* aete version and language specifiers */ { TUTORIAL_AUTOMATION_VENDORNAME, /* vendor suite name */ TUTORIAL_AUTOMATION_PLUGINDESC, /* optional description */ TUTORIAL_AUTOMATION_SUITE_ID, /* suite ID */ 1, /* suite code, must be 1 */ 1, /* suite level, must be 1 */ { /* structure for automation */ TUTORIAL_AUTOMATION_PLUGINNAME, /* name */ TUTORIAL_AUTOMATION_PLUGINDESC, /* optional description */ TUTORIAL_AUTOMATION_CLASS_ID, /* class ID, must be unique or Suite ID */ TUTORIAL_AUTOMATION_EVENT_ID, /* event ID, must be unique */ NO_REPLY, /* never a reply */ IMAGE_DIRECT_PARAMETER, /* direct parameter, used by Photoshop */ { // filter or selection class here: } }, {}, /* non-filter/automation plug-in class here */ {}, /* comparison ops (not supported) */ { // Enumerations go here: } /* end of any enumerations */ } }; ~~~~~~ The eagle-eyed (or incredibly bored) viewer might have noticed the addition of the lines: ~~~~~~cpp EnableInfo { "true" }, ... Persistent{}, // Only relevant if Persistent is set. Messages { startupRequired, doesNotPurgeCache, shutdownRequired, acceptProperty }, ~~~~~~ The former basically disables the plugin in the menu unless an actual Photoshop document is currently open (which is nice sometimes, as you don't want to trick users into thinking that they can execute when they actually can't). The latter basically allows us to keep the plug-in loaded into memory on startup (which is what I usually do, since I initialize a bunch of things the moment my plugin loads into Photoshop.) You can omit this if you'd like. ## Package signing ## There's another optional step after building the plug-in that also involves something called a `.p12` certificate, which is used by `ZXPSignCmd.exe`. What exactly is that? If we take a look at the [documentation](https://wwwimages2.adobe.com/content/dam/acom/en/devnet/creativesuite/pdfs/SigningTechNote_CC.pdf), we see that it's basically Adobe's way of allowing developers to certify that their code has not been altered before being deployed to end-users (presumably because most extensions are written in JSX/JavaScript and people are scared about that sort of thing? Who knows.) Either way, this means that you'll need to generate one of these certifcates as well if you want to be able to deploy your extension to other end-users through the Extension Manager or Adobe marketplace. If you actually just want to make a DLL and deploy it to end-users manually (by copying and pasting the plug-in to the appropriate location), then you can skip this step entirely. ### Generating a certificate ### To generate the certificate, we'll make a self-signed one for now. You can, of course, pay money to a Certificate Authority (CA) and get one there, but for personal usage, a self-signed one will suffice. Thankfully, we can do this as well using the `ZXPSignCmd.exe` tool. From the [documentation](https://github.com/Adobe-CEP/Getting-Started-guides/tree/master/Package%20Distribute%20Install#acquiring-a-signed-certificate): ~~~~~~batch ZXPSignCmd -selfSignedCert [options] ~~~~~~ You will then get a `.p12` file which you can for creating your packaged native extension later on. ## Writing the build script ## Create a new file called `build.bat`, and let's get started writing up the previous diagram into code: ~~~~~~batch @echo off setlocal set PhotoshopSDKRoot=C:\Users\sonict\src\thirdparty\adobe\cc2019\photoshop\pluginsdk set PhotoshopPluginsDeployPath=C:\Users\sonictk\psplugins set ZXPSignCmdExe=C:\Users\sonictk\Git\adobe\CEP-Resources\ZXPSignCMD\4.0.7\win64\ZXPSignCmd.exe set ZXPCert=C:\Users\sonictk\cert.p12 set ZXPCertPassword=password123 call "%vs2017installdir%\VC\Auxiliary\Build\vcvarsall.bat" x64 set BuildDir=%~dp0msbuild if not exist %BuildDir% mkdir %BuildDir% pushd %BuildDir% ~~~~~~ Yes, I'm putting the signing password into the build script since the certificate is self-signed and of no practical use in terms of being a security device. For now, let's just move on and set up the compiler flags we're going to use. ~~~~~~batch set ProjectName=tutorial_automation set EntryPoint=%~dp0src\%ProjectName%_main.cpp set ResourcePiPL=%~dp0src\%ProjectName%_pipl.r set ResourceRC=%BuildDir%\%ProjectName%_pipl.rc set ResourceRES=%BuildDir%\%ProjectName%_pipl.res set ThirdPartyDirPath=%~dp0..\thirdparty set OutBin=%BuildDir%\%ProjectName%.8li set CommonLinkerFlags=/dll /incremental:no /machine:x64 /nologo /defaultlib:Kernel32.lib /defaultlib:User32.lib /defaultlib:Shell32.lib /nodefaultlib:LIBCMTD.lib /nodefaultlib:LIBCMT.lib "%ResourceRES%" set DebugLinkerFlags=%CommonLinkerFlags% /opt:noref /debug /pdb:"%BuildDir%\%ProjectName%.pdb" set ReleaseLinkerFlags=%CommonLinkerFlags% /opt:ref set RelWithDebInfoLinkerFlags=%CommonLinkerFlags% /opt:ref /debug /pdb:"%BuildDir%\%ProjectName%.pdb" set PSPreprocessorDefines=/DISOLATION_AWARE_ENABLED=1 /DWIN32=1 /D_CRT_SECURE_NO_DEPRECATE /D_SCL_SECURE_NO_DEPRECATE /D_WINDOWS /D_USRDLL /D_WINDLL /D_MBCS set PSCompilerFlags=/EHsc set CommonIncludePaths=/I "%ThirdPartyDirPath%" /I "%ThirdPartyDirPath%\psapi\common\includes" /I "%ThirdPartyDirPath%\psapi\common\resources" /I "%ThirdPartyDirPath%\psapi\common\sources" /I "%ThirdPartyDirPath%\psapi\photoshop" /I "%ThirdPartyDirPath%\psapi\pica_sp" /I "%ThirdPartyDirPath%\psapi\resources" /I "%ThirdPartyDirPath%\psapi\ai" /I "%BuildDir%" set CommonCompilerFlags=/nologo /W3 /WX %CommonIncludePaths% /Zc:__cplusplus /arch:AVX2 %PSCompilerFlags% %PSPreprocessorDefines% set CompilerFlagsDebug=%CommonCompilerFlags% /Od /Zi /D_DEBUG /MDd set CompilerFlagsRelease=%CommonCompilerFlags% /Ox /DNDEBUG /MD set CompilerFlagsRelWithDebInfo=%CommonCompilerFlags% /Ox /Zi /DNDEBUG /MD ~~~~~~ You can see that we have a bunch of extra preprocessor defines in there, some of which might make no sense (or be even spurious). Leave it all be for now, and let's set up the compilation commands themselves. ~~~~~~batch echo. echo Compiling resources (command follows below)... set ResourceRR=%BuildDir%\%ProjectName%_pipl.rr echo %BuildRRCommand% %BuildRRCommand% > "%ResourceRR%" if %errorlevel% neq 0 goto error echo. echo Converting PiPL to Windows resource file format... %CnvtPiPLExePath% "%ResourceRR%" "%ResourceRC%" if %errorlevel% neq 0 goto error echo. echo Compiling Windows Resources... rc /v /fo "%ResourceRES%" "%ResourceRC%" if %errorlevel% neq 0 goto error echo. echo Compiling source files for automation filter (command follows below)... echo %BuildCommand% echo. echo Output from compilation: %BuildCommand% if %errorlevel% neq 0 goto error ~~~~~~ For Photoshop, there's one thing that helps us when it comes to deploying our extensions: the normal path where you're supposed to deploy extensions to is the `Plug-ins` folder located in your Photoshop installation directory (i.e. `%programfiles%\Adobe\Adobe Photoshop 2020\Plug-ins`). However, this directory is searched recursively for suitable plug-in files, along with following shortcuts as well. Thus, what I usually do is make a shortcut link in the folder itself to a deploy location (which is why we define `PhotoshopPluginsDeployPath` above), and just copy the plug-ins there, like so: ~~~~~~batch echo. echo Deploying built binaries and symbols... copy /Y "%OutBin%" "%PhotoshopPluginsDeployPath%" if %errorlevel% neq 0 goto error copy /Y "%BuildDir%\%ProjectName%.pdb" "%PhotoshopPluginsDeployPath%" if %errorlevel% neq 0 goto error ~~~~~~ We copy the `.pdb` file as well, since it'll help the MSVC debugger when we want to end up debugging the plug-in later on if there's a problem. Finally, we build and sign the `.zxp` extension package for deployment to end-users, and we're done with the build script! ~~~~~~batch set OutZXP=%BuildDir%\%ProjectName%.zxp set BuildZXPCommand=%ZXPSignCmdExe% -sign "%~dp0msbuild" "%OutZXP%" "%ZXPCert%" %ZXPCertPassword% echo. echo Building ZXP Photoshop extension package (command follows below)... echo %BuildZXPCommand% %BuildZXPCommand% if %errorlevel% neq 0 goto error if %errorlevel% == 0 goto success :error echo. echo *************************************** echo * !!! An error occurred!!! * echo *************************************** goto end :success echo. echo *************************************** echo * Build completed successfully. * echo *************************************** goto end :end echo. echo Build script finished execution at %time%. popd endlocal exit /b %errorlevel% ~~~~~~ !!! tip The full version of this build script is available in the accompanying code repository for this tutorial. Run ``build.bat`` and wait for the process to spit out the `.8li` DLL. After you've made sure that you've deployed the plugin in a suitable location that Photoshop can find, go ahead and open Photoshop, then launch your plug-in from the Automation menu. You should see the following message box appear: *************************************** * * * +--------------------------+---+ * * | Tutorial Dialog | X | * * +--------------------------+---+ * * | | * * | ----- | * * | ( i ) Hello World! | * * | ----- | * * +------------------------------+ * * | +----------+ | * * | | OK | | * * | +----------+ | * * +------------------------------+ * * * *************************************** Congratulations, you've just made your first native plug-in in Photoshop! ## Debugging your plug-in ## Let's make sure we're able to debug our plug-in first before we get carried away. If you've been following along so far, run `build.bat debug` in order to generate a version of the `.8li` with optimizations disabled and an accompanying `.pdb` file generated alongside it as well. After that, launch Photoshop and Visual Studio. You should be able to attach to the Photoshop process, set a breakpoint, then run your Automation action; the breakpoint in Visual Studio should get triggered and allow you to step through your plug-in. !!! warning Threading in Photoshop plug-ins Basically, **no**. The Photoshop API is **not thread-safe**, so threading in plug-ins is generally not advisable. There's a whole host of other restrictions that Adobe has laid out, which you can read in the FAQ section of the SDK documentation. (`pluginsdk/documentation/html/faqs.html#a313`) Now that we have a basic build-deploy-debug workflow going, we can finally start to do something mildly useful with it. # Why bother with different Photoshop plug-in types? # So now that we have an Automation plug-in, I'd like to pause for a second and go over the various signatures of few various types of plug-in entry point types in Photoshop. We're now familiar with the **Automation** type of entry point: ~~~~~~cpp DLLExport SPAPI SPErr AutoPluginMain(const char* caller, const char* selector, void* message) ~~~~~~ We see that it actually doesn't offer us that much to play with alone, since all we really get is just some very basic metadata about the caller of our plugin, and what ``selector`` case triggered the call (which is basically Photoshop-speak for what caused the caller to call our entry point in the first place). We don't get access to anything else here unless the rest of the Photoshop API exposes it to us. And once you start looking more at the documentation included in the SDK, you'll come to realize that the amount of functionality exposed to third-party developers (which is who we are) is, quite frankly, rather pitiful. In order to even access the data in the active Photoshop document, we'll need to consider other plug-in types instead. If you read the **Types of plug-in modules** page located in the Photoshop SDK documentation, you'll be able to see that each plug-in's entry point delivers more information to you so that you can make the appropriate data transformations. For example, if we wanted to write an exporter plug-in for Photoshop that exported our layers in the document to some custom data format for use in an engine or whatnot, we'd go ahead and make a **Export** type of plug-in: ~~~~~~cpp DLLExport MACPASCAL void PluginMain (const int16 selector, ExportRecordPtr exportParamBlock, intptr_t *data, int16 *result) ~~~~~~ If we take a look at what `ExportRecordPtr` gives us (in `PIExport.h`), we can see that it's a _ton_ of document data that could be of potential use to us (along with some stuff that's legacy/deprecated/reserved and that we shouldn't pay any attention to): ~~~~~~cpp // Comments in original source file stripped for brevity. typedef struct ExportRecord { int32 serialNumber; TestAbortProc abortProc; ProgressProc progressProc; int32 maxData; int16 imageMode; Point imageSize; int16 depth; int16 planes; Fixed imageHRes; Fixed imageVRes; LookUpTable redLUT; LookUpTable greenLUT; LookUpTable blueLUT; Rect theRect; int16 loPlane; int16 hiPlane; void * data; int32 rowBytes; Str255 filename; int16 vRefNum; Boolean dirty; Rect selectBBox; OSType hostSig; HostProc hostProc; Handle duotoneInfo; int16 thePlane; PlugInMonitor monitor; void * platformData; BufferProcs * bufferProcs; ResourceProcs * resourceProcs; ProcessEventProc processEvent; DisplayPixelsProc displayPixels; HandleProcs *handleProcs; ColorServicesProc colorServices; GetPropertyProc getPropertyObsolete; AdvanceStateProc advanceState; int16 layerPlanes; int16 transparencyMask; int16 layerMasks; int16 invertedLayerMasks; int16 nonLayerPlanes; ImageServicesProcs *imageServicesProcs; int16 tileWidth; int16 tileHeight; Point tileOrigin; PropertyProcs *propertyProcs; PIDescriptorParameters *descriptorParameters; Str255 *errorString; ChannelPortProcs *channelPortProcs; ReadImageDocumentDesc *documentInfo; SPBasicSuite *sSPBasic; void *plugInRef; int32 transparentIndex; Handle iCCprofileData; int32 iCCprofileSize; int32 canUseICCProfiles; int32 lutCount; int32 HostSupports32BitCoordinates; int32 PluginUsing32BitCoordinates; VPoint imageSize32; VRect theRect32; VRect selectBBox32; char reserved [106]; /**< Reserved for future use. Set to zero */ } ExportRecord, *ExportRecordPtr; ~~~~~~ I'll leave you to read the comments in the original source file if you want to find out about everything you now have access to, but the most important member here (assuming you really were writing an exporter) is the `data` member, which contains the raw bytes that you're trying to transform. Of course, you could also take a look at the `documentInfo` pointer, which being a pointer to a `ReadImageDocumentDesc` structure gives you access to basically the entire Photoshop document's layers, channels, metadata, etc. My advice is to work out what exactly you're attempting to do with your plug-in, and then take a look at the documentation regarding various Photoshop plug-in types to see which might be most appropriate, while also referring to the SDK samples to see what each plug-in type has access to in terms of data that Photoshop exposes to it. # Recipe: Reading layer data through C++ # This recipe will focus on a very simple operation: reading layer data from a Photoshop document for each layer in the document, and then exporting separate images of these layers to disk. However, I'll also demonstrate (in a simpler fashion) one of the examples of combining a Photoshop Automation plug-in to execute a Filter plug-in, thus allowing an Automation plug-in to essentially trigger modifications on an existing open document's pixels. !!! tip Prerequisite knowledge You should already be familar with making Automation plug-ins and building Photoshop plug-ins in general. If not, read the first recipe of this tutorial in Section [Recipe: Minimalist approach to Photoshop plug-ins]. ## Additional requirements ## Because we'll be writing JPG images to disk, I'll be making use of `stb_image_write.h` to handle that functionality (since this tutorial isn't about implementing a JPG format writer). You can get it from: https://github.com/nothings/stb/blob/master/stb_image_write.h Place that in a folder that you're including into your compilation settings, as per your build script. ## The filter plug-in entry point ## I'll skip the research here and get to the point: we'll be utilizing a **Filter** plugin to accomplish this, since it gives us access to what we need in order to read the document pixels. If you take a look at the documentation to see what the entry point for one of those is like, it looks like this: ~~~~~~cpp DLLExport MACPASCAL void PluginMain(const int16 selector, FilterRecordPtr filterRecord, intptr_t * data, int16 * result) ~~~~~~ And if we look at what a `FilterRecordPtr` is in `PIFilter.h`, we see that it is also another ginormous struct with all the information we need (I've left the source unmodified this time with all the comments for quick reference, since we're _actually_ going to be working with this struct in this tutorial): ~~~~~~cpp typedef struct FilterRecord { int32 serialNumber; /**< \deprecated Formerly the host serial number. The host now reports zero for the \c serialNumber. Plug-ins should use the @ref PropertySuite by using the @ref FilterRecord::propertyProcs callback, specifying the property \c propSerialString2 to get the serial string. */ TestAbortProc abortProc; /**< A pointer to the \c TestAbortProc callback. */ ProgressProc progressProc; /**< A pointer the the \c ProgressProc callback. */ Handle parameters; /**< Parameters the plug-in can request from a user. Photoshop initializes this handle to NULL at startup. If the plug-in filter has any parameters that the user can set, it should allocate a relocatable block in the \c filterSelectorParameters handler, store the parameters in the block, and store the block’s handle in this field. */ Point imageSize; /**< \deprecated Use \c BigDocumentStruct::imageSize32. The width and height of the image in pixels. If the selection is floating, this field instead holds the size of the floating selection. */ int16 planes; /**< The number of planes in the image. For version 4+ filters, this field contains the total number of active planes in the image, including alpha channels. The image mode should be determined by looking at \c imageMode. For version 0-3 filters, this field is equal to 3 if filtering the RGB channel of an RGB color image, or 4 if filtering the CMYK channel of a CMYK color image. Otherwise it is equal to 1. */ Rect filterRect; /**< \deprecated Use \c BigDocumentStruct::filterRect32. The area of the image to be filtered. This is the bounding box of the selection, or if there is no selection, the bounding box of the image. If the selection is not a perfect rectangle, Photoshop automatically masks the changes to the area actually selected (unless the plug-in turns off this feature using autoMask). This allows most filters to ignore the selection mask, and still operate correctly. */ PSRGBColor background; /**< \deprecated Use @ref FilterRecord::backColor. */ PSRGBColor foreground; /**< \deprecated Use @ref FilterRecord::foreColor. */ int32 maxSpace; /**< \deprecated Use @ref FilterRecord::maxSpace64. */ int32 bufferSpace; /**< \deprecated Use @ref FilterRecord::bufferSpace64. */ Rect inRect; /**< \deprecated Use \c BigDocumentStruct::inRect32. The area of the input image to access. The plug-in should set this field in the \c filterSelectorStart and \c filterSelectorContinue handlers to request access to an area of the input image. The area requested must be a subset of the image’s bounding rectangle. After the entire \c filterRect has been filtered, this field should be set to an empty rectangle. */ int16 inLoPlane; /**< The first input plane to process next. The \c filterSelectorStart and \c filterSelectorContinue handlers should set this field. */ int16 inHiPlane; /**< The last input plane to process next. The \c filterSelectorStart and \c filterSelectorContinue handlers should set this field. */ Rect outRect; /**< \deprecated Use \c BigDocumentStruct::outRect32. The area of the output image to access. The plug-in should set this field in its \c filterSelectorStart and \c filterSelectorContinue handlers to request access to an area of the output image. The area requested must be a subset of \c filterRect. After the entire \c filterRect has been filtered, this field should be set to an empty rectangle. */ int16 outLoPlane; /**< The first output plane to process next. The \c filterSelectorStart and \c filterSelectorContinue handlers should set this field. */ int16 outHiPlane; /**< The last output plane to process next. The \c filterSelectorStart and \c filterSelectorContinue handlers should set this field. */ void * inData; /**< A pointer to the requested input image data. If more than one plane has been requested (see \c inLoPlane and \c inHiPlane), the data is interleaved. */ int32 inRowBytes; /**< The offset between rows of the input image data. The end of each row may or may not include pad bytes. */ void * outData; /**< A pointer to the requested output image data. If more than one plane has been requested (see \c outLoPlane and \c outHiPlane), the data is interleaved. */ int32 outRowBytes; /**< The offset between rows of the output image data. The end of each row may or may not include pad bytes. */ Boolean isFloating; /**< Indicates if the selection is floating. Set to TRUE if and only if the selection is floating. */ Boolean haveMask; /**< Indicates if the selection has a mask. Set to true if and only if non-rectangular area has been selected. */ Boolean autoMask; /**< Enables or disables auto-masking. By default, Photoshop automatically masks any changes to the area actually selected. If \c isFloating=FALSE, and \c haveMask=TRUE, the plug-in can turn off this feature by setting this field to FALSE. It can then perform its own masking.

If the plug-in has set the PiPL bit \c writesOutsideSelection, this will always be FALSE and the plug-in must supply its own mask, if needed. */ Rect maskRect; /**< \deprecated Use \c BigDocumentStruct::maskRect32. Provides a mask rectangle. If \c haveMask=TRUE, and the plug-in needs access to the selection mask, the plug-in should set this field in your \c filterSelectorStart and \c filterSelectorContinue handlers to request access to an area of the selection mask. The requested area must be a subset of \c filterRect. This field is ignored if there is no selection mask. */ void * maskData; /**< A pointer to the requested mask data. The data is in the form of an array of bytes, one byte per pixel of the selected area. The bytes range from (0...255), where 0=no mask (selected) and 255=masked (not selected). Use \c maskRowBytes to iterate over the scan lines of the mask. */ int32 maskRowBytes; /**< The offset between rows of the mask data. */ FilterColor backColor; /**< The current background color, in the color space native to the image.*/ FilterColor foreColor; /**< The current foreground color, in the color space native to the image.*/ OSType hostSig; /**< The signature of the host, provided by the host. The signature for Photoshop is signature is 8BIM. */ HostProc hostProc; /**< A pointer to a host-defined callback procedure. May be NULL. */ int16 imageMode; /**< The mode of the image being filtered, for example, Gray Scale, RGB Color, and so forth. See @ref ImageModes "Image Modes" for values. The \c filterSelectorStart handler should return \c filterBadMode if it is unable to process this mode of image. */ Fixed imageHRes; /**< The horizontal resolution of the image in terms of pixels per inch. These are fixed point numbers (16.16). */ Fixed imageVRes; /**< The vertical resolution of the image in terms of pixels per inch. These are fixed point numbers (16.16). */ Point floatCoord; /**< \deprecated Use \c BigDocumentStruct::floatCoord32. The coordinate of the top-left corner of the selection in the main image’s coordinate space. */ Point wholeSize; /**< \deprecated Use \c BigDocumentStruct::wholeSize32. The size in pixels of the entire main image. */ PlugInMonitor monitor; /**< Monitor setup information for the host. */ void *platformData; /**< A pointer to platform specific data. Not used under Mac OS. See PlatformData in PITypes.h. */ BufferProcs *bufferProcs; /**< A pointer to the Buffer suite if it is supported by the host, otherwise NULL. See @ref BufferSuite. */ ResourceProcs *resourceProcs; /**< A pointer to the Pseudo-Resource suite if it is supported by the host, otherwise NULL. See chapter @ref ResourceSuite. */ ProcessEventProc processEvent; /**< A pointer to the \c ProcessEventProc callback if it is supported by the host, otherwise NULL. */ DisplayPixelsProc displayPixels;/**< A pointer to the \c DisplayPixelsProc callback if it is supported by the host, otherwise NULL. */ HandleProcs *handleProcs; /**< A pointer to the Handle callback suite if it is supported by the host, otherwise NULL. See @ref HandleSuite. */ // New in 3.0. Boolean supportsDummyChannels; /**< Indicates whether the host supports the plug-in requesting nonexistent planes. (See @ref dummyPlaneValue, @ref inPreDummyPlanes, @ref inPostDummyPlanes, @ref outPreDummyPlanes, @ref outPostDummyPlanes.) */ Boolean supportsAlternateLayouts; /**< Indicates whether the host support data layouts other than rows of columns of planes. This field is set by the plug-in host to indicate whether it respects the \c wantLayout field. */ int16 wantLayout; /**< The desired layout for the data. The plug-in host only looks at this field if it has also set \c supportsAlternateLayouts. See @ref LayoutConstants "Layout Constants" for values. */ int16 filterCase; /**< The type of data being filtered. Flat, floating, layer with editable transparency, layer with preserved transparency, with and without a selection. A zero indicates that the host did not set this field, and the plug-in should look at \c haveMask and \c isFloating. See @ref FilterCaseIdentifiers for values. */ int16 dummyPlaneValue; /**< The value to store into any dummy planes. Values from 0 to 255 indicates a specific value. A value of -1 indicates to leave undefined. All other values generate an error. */ void * premiereHook; /**< \deprecated */ AdvanceStateProc advanceState; /**< \c AdvanceState callback. */ Boolean supportsAbsolute; /**< Indicates whether the host supports absolute channel indexing. Absolute channel indexing ignores visibility concerns and numbers the channels from zero starting with the first composite channel. If existing, transparency follows, followed by any layer masks, then alpha channels. */ Boolean wantsAbsolute; /**< Enables absolute channel indexing for the input. This is only useful if supportsAbsolute=TRUE. Absolute indexing is useful for things like accessing alpha channels. */ GetPropertyProc getPropertyObsolete; /**< The \c GetProperty callback. This direct callback pointer has been superceded by the Property callback suite, but is maintained here for backwards compatibility. See @ref PropertySuite. */ Boolean cannotUndo; /**< Indicates whether a filter plug-in makes changes that the user cannot undo. If the filter makes a change that cannot be undone, then setting this field to TRUE prevents Photoshop from offering undo for the filter. This is rarely needed and usually frustrates users. */ Boolean supportsPadding; /**< Indicates whether the host supports requests outside the image area. If so, see padding fields below. */ int16 inputPadding; /**< Instructions for padding the input. The input can be padded when loaded. See @ref FilterPadding "Filter Padding Constants." \n\n The options for padding include specifying a specific value (0...255), specifying \c plugInWantsEdgeReplication, specifying that the data be left random (\c plugInDoesNotWantPadding), or requesting that an error be signaled for an out of bounds request (\c plugInWantsErrorOnBoundsException). Default value: \c plugInWantsErrorOnBoundsException. */ int16 outputPadding; /**< Instructions for padding the output. The output can be padded when loaded. See @ref FilterPadding "Filter Padding Constants." \n\n The options for padding include specifying a specific value (0...255), specifying \c plugInWantsEdgeReplication, specifying that the data be left random (\c plugInDoesNotWantPadding), or requesting that an error be signaled for an out of bounds request (\c plugInWantsErrorOnBoundsException). Default value: \c plugInWantsErrorOnBoundsException. */ int16 maskPadding; /**< Padding instructions for the mask. The mask can be padded when loaded. See @ref FilterPadding "Filter Padding Constants." \n\n The options for padding include specifying a specific value (0...255), specifying \c plugInWantsEdgeReplication, specifying that the data be left random (\c plugInDoesNotWantPadding), or requesting that an error be signaled for an out of bounds request (\c plugInWantsErrorOnBoundsException). Default value: \c plugInWantsErrorOnBoundsException. */ char samplingSupport; /**< Indicates whether the host support non- 1:1 sampling of the input and mask. Photoshop 3.0.1+ supports integral sampling steps (it will round up to get there). This is indicated by the value #hostSupportsIntegralSampling. Future versions may support non-integral sampling steps. This will be indicated with #hostSupportsFractionalSampling. */ char reservedByte; /**< For alignment. */ Fixed inputRate; /**< The sampling rate for the input. The effective input rectangle in normal sampling coordinates is inRect * inputRate. For example, (inRect.top * inputRate, inRect.left * inputRate, inRect.bottom * inputRate, inRect.right * inputRate). The value for \c inputRate is rounded to the nearest integer in Photoshop 3.0.1+. Since the scaled rectangle may exceed the real source data, it is a good idea to set some sort of padding for the input as well. */ Fixed maskRate; /**< Like \c inputRate, but as applied to the mask data. */ ColorServicesProc colorServices; /**< Function pointer to access color services routine. */ /* Photoshop structures its data as follows for plug-ins when processing layer data: target layer channels transparency mask for target layer layer mask channels for target layer inverted layer mask channels for target layer non-layer channels When processing non-layer data (including running a filter on the layer mask alone), Photoshop structures the data as consisting only of non-layer channels. It indicates this structure through a series of short counts. The transparency count must be either 0 or 1. */ int16 inLayerPlanes; /**< The number of target layer planes for the input data. If all the input plane values are zero, then the plug-in should assume the host has not set them.

@note When processing layer data, Photoshop structures its input and output data for plug-ins as follows: - target layer channels - transparency mask for target layer - layer mask channels for target layer - inverted layer mask channels for target layer - non-layer channels The output planes are a prefix of the input planes. For example, in the protected transparency case, the input can contain a transparency mask and a layer mask while the output can contain just the layerPlanes.

When processing non-layer data (including running a filter on the layer mask alone), Photoshop structures the data as consisting only of non-layer channels. It indicates this structure through a series of short counts. The transparency count must be either 0 or 1. */ int16 inTransparencyMask; /**< The number of transparency masks for the input target layer data. See @ref inLayerPlanes for additional information. */ int16 inLayerMasks; /**< The number of layer mask channels for the input target layer. See @ref inLayerPlanes for additional information.*/ int16 inInvertedLayerMasks; /**< The number of inverted layer mask channels for the input target layer.
With the inverted layer masks, 0 = fully visible and 255 = completely hidden. See @ref inLayerPlanes for additional information.*/ int16 inNonLayerPlanes; /**< The number of non-layer channels for the input data. See @ref inLayerPlanes for additional information. */ int16 outLayerPlanes; /**< The number of target layer planes for the output data. See @ref inLayerPlanes for additional information about the structure of the output data. */ int16 outTransparencyMask; /**< The number of transparency masks for the output data. See @ref inLayerPlanes for additional information about the structure of the output data. */ int16 outLayerMasks; /**< The number of layer mask channels for the output data. See @ref inLayerPlanes for additional information about the structure of the output data. */ int16 outInvertedLayerMasks; /**< The number of inverted layer mask channels for the output data. See @ref inLayerPlanes for additional information about the structure of the output data. */ int16 outNonLayerPlanes; /**< The number of non-layer channels for the output data. See @ref inLayerPlanes for additional information about the structure of the output data. */ int16 absLayerPlanes; /**< The number of target layer planes for the input data, used for the structure of the input data when \c wantsAbsolute is TRUE. See @ref inLayerPlanes for additional information about the structure of the input data. */ int16 absTransparencyMask; /**< The number of transparency masks for the input data, used for the structure of the input data when \c wantsAbsolute is TRUE. See @ref inLayerPlanes for additional information about the structure of the input data. */ int16 absLayerMasks; /**< The number of layer mask channels for the input data, used for the structure of the input data when \c wantsAbsolute is TRUE. See @ref inLayerPlanes for additional information about the structure of the input data. */ int16 absInvertedLayerMasks; /**< The number of inverted layer mask channels for the input data, used for the structure of the input data when \c wantsAbsolute is TRUE. See @ref inLayerPlanes for additional information about the structure of the input data. */ int16 absNonLayerPlanes; /**< The number of target layer planes for the input data, used for the structure of the input data when \c wantsAbsolute is TRUE. See @ref inLayerPlanes for additional information about the structure of the input data. */ /* We allow for extra planes in the input and the output. These planes will be filled with dummyPlaneValue at those times when we build the buffers. These features will only be available if supportsDummyPlanes is TRUE. */ int16 inPreDummyPlanes; /**< The number of extra planes before the input data. Only available if \c supportsDummyChannels=TRUE. Used for things like forcing RGB data to appear as RGBA. */ int16 inPostDummyPlanes; /**< The number of extra planes after the input data. Only available if \c supportsDummyChannels=TRUE. Used for things like forcing RGB data to appear as RGBA. */ int16 outPreDummyPlanes; /**< The number of extra planes before the output data. Only available if \c supportsDummyChannels=TRUE. */ int16 outPostDummyPlanes; /**< The number of extra planes after the output data. Only available if \c supportsDummyChannels=TRUE. */ /* If the plug-in makes use of the layout options, then the following fields should be obeyed for identifying the steps between components. The last component in the list will always have a step of one. */ int32 inColumnBytes; /**< The step from column to column in the input. If using the layout options, this value may change from being equal to the number of planes. If zero, assume the host has not set it. */ int32 inPlaneBytes; /**< The step from plane to plane in the input. Normally 1, but this changes if the plug-in uses the layout options. If zero, assume the host has not set it. */ int32 outColumnBytes; /**< The output equivalent of @ref inColumnBytes. */ int32 outPlaneBytes; /**< The output equivalent of @ref inPlaneBytes. */ // New in 3.0.4 ImageServicesProcs *imageServicesProcs; /**< Image Services callback suite. */ PropertyProcs *propertyProcs; /**< Property callback suite. The plug-in needs to dispose of the handle returned for complex properties (the plug-in also maintains ownership of handles for set properties. */ int16 inTileHeight; /**< Tiling height for the input, set by host. Zero if not set. The plug-in should work at this size, if possible. */ int16 inTileWidth; /**< Tiling width for the input, set by host. Zero if not set. Best to work at this size, if possible. */ Point inTileOrigin; /**< Tiling origin for the input, set by host. Zero if not set. */ int16 absTileHeight; /**< Tiling height the absolute data, set by host. The plug-in should work at this size, if possible.*/ int16 absTileWidth; /**< Tiling width the absolute data, set by host. The plug-in should work at this size, if possible.*/ Point absTileOrigin; /**< Tiling origin the absolute data, set by host. */ int16 outTileHeight; /**< Tiling height for the output, set by host. The plug-in should work at this size, if possible. */ int16 outTileWidth; /**< Tiling width for the output, set by host. The plug-in should work at this size, if possible. */ Point outTileOrigin; /**< Tiling origin for the output, set by host. */ int16 maskTileHeight; /**< Tiling height for the mask, set by host. The plug-in should work at this size, if possible. */ int16 maskTileWidth; /**< Tiling width for the mask, set by host. The plug-in should work at this size, if possible. */ Point maskTileOrigin; /**< Tiling origin for the mask, set by host. */ // New in 4.0 PIDescriptorParameters *descriptorParameters; /**< Descriptor callback suite. */ Str255 *errorString; /**< An error reporting string to return to Photoshop. If the plug-in returns with result=errReportString then this string is displayed as: "Cannot complete operation because " + \c errorString. */ ChannelPortProcs *channelPortProcs; /**< Channel Ports callback suite. */ ReadImageDocumentDesc *documentInfo; /**< The Channel Ports document information for the document being filtered. */ // New in 5.0 SPBasicSuite *sSPBasic; /**< PICA basic suite. Provides the mechanism to access all PICA suites. */ void *plugInRef; /**< Plug-in reference used by PICA. */ int32 depth; /**< Bit depth per channel (1,8,16,32). */ // New in 6.0 Handle iCCprofileData; /**< Handle containing the ICC profile for the image. (NULL if none, read only) Photoshop allocates the handle using its handle suite The handle is unlocked while calling the plug-in. The handle is valid from \c FilterSelectorStart to \c FilterSelectorFinish Photoshop will free the handle after \c FilterSelectorFinish. */ int32 iCCprofileSize; /**< Size of profile. */ int32 canUseICCProfiles; /**< Indicates if the host uses ICC Profiles. If this is zero, do not dereference \c iCCprofileData. */ // New in 7.0 int32 hasImageScrap; /**< Indicates if Photoshop has image scrap; non-zero if it does. The plug-in can ask for the exporting of image scrap by setting the PiPL resource, @ref PIWantsScrapProperty. The document info for the image scrap is chained right behind the targeted document pointed by the @ref documentInfo field. Photoshop sets \c hasImageScrap to indicate that an image scrap is available. A plug-in can use it to tell whether Photoshop failed to export the scrap because some unknown reasons or there is no scrap at all. */ // New in 8.0 BigDocumentStruct *bigDocumentData; /**< Support for documents larger than 30,000 pixels. NULL if host does not support big documents.*/ // New in 10.0 PIDescriptorParameters *input3DScene; /**< support for 3d scene data to be sent into the plug-in */ PIDescriptorParameters *output3DScene; /**< support for 3d scene to come out of the plug-in */ Boolean createNewLayer; /**< set by plug-in this only works for 3D layers */ // New in 13.0 Handle iCCWorkingProfileData; /**< Handle containing the ICC profile for the working profile set via color settings dialog. (NULL if none) Photoshop allocates the handle using its handle suite The handle is unlocked while calling the plug-in. The handle is valid from \c FilterSelectorStart to \c FilterSelectorFinish Photoshop will free the handle after \c FilterSelectorFinish. */ int32 iCCWorkingProfileSize; /**< Size of working profile. */ // New in 18.0 int64 bufferSpace64; /**< Allows the plug-in to specify how much buffer space it needs. If the plug-in is planning on allocating any large internal buffers or tables, it should set this field during the \c filterSelectorPrepare call to the number of bytes it plans to allocate.Photoshop then tries to free up the requested amount of space before calling the \c filterSelectorStart routine.Relocatable blocks should be used if possible. */ int64 maxSpace64; /**< The maximum number of bytes of information the plug-in can expect to be able to access at once (input area size + output area size + mask area size + bufferSpace). Set by Photoshop. */ ///@name Reserved Space for Expansion //@{ char reserved [46]; /**< Reserved for future use. Set to zero. */ //@} } FilterRecord, *FilterRecordPtr; ~~~~~~ Because our objective this time is to export the data from each and every separate layer in the document, we have to make use of the `documentInfo` pointer here, which will direct us to a struct like so: ~~~~~~cpp typedef struct ReadImageDocumentDesc { int32 minVersion; /**< The minimum version needed to interpret this record. */ int32 maxVersion; /**< The maximum version allowable to interpret this record. */ //--------------------------------------------------------------------------- // Version 0 fields: //--------------------------------------------------------------------------- int32 imageMode; /**< Color mode for the document. See @ref ImageModes for supported modes. */ int32 depth; /**< Default bit depth for the document. */ VRect bounds; /**< Document bounds. */ Fixed hResolution; /**< Horizontal resolution for the document. */ Fixed vResolution; /**< Vertical resolution for the document. */ LookUpTable redLUT; /**< Red color table, applicable when \c imageMode is indexed color or duotone. */ LookUpTable greenLUT; /**< Green color table, applicable when \c imageMode is indexed color or duotone. */ LookUpTable blueLUT; /**< Blue color table, applicable when \c imageMode is indexed color or duotone. */ ReadChannelDesc *targetCompositeChannels; /**< Composite channels in the target layer. */ ReadChannelDesc *targetTransparency; /**< Transparency channel for the target layer. */ ReadChannelDesc *targetLayerMask; /**< Layer mask for the target channel. */ ReadChannelDesc *mergedCompositeChannels; /**< Composite channels in the merged data. Merged data is either merged layer data or merged document data. */ ReadChannelDesc *mergedTransparency; /**< Transparency channel for the merged data. */ ReadChannelDesc *alphaChannels; /**< Alpha channels for masks */ ReadChannelDesc *selection; /**< Selection mask, if any. */ // New with version 1. //--------------------------------------------------------------------------- // Version 1 fields //--------------------------------------------------------------------------- // New in version 1 struct SPPlatformFileSpecification_t *fileSpec; /**< The file specification, if any. */ // New in version 2 ReadLayerDesc *layersDescriptor; /**< Descriptor for a linked list of layers, if any. */ int32 documentType; /**< Document type. See @ref DocumentTypes "Document Types." */ struct ReadImageDocumentDesc *next; /**< The next descriptor in the list. */ //--------------------------------------------------------------------------- // Version 3 fields (added in Photoshop CS3/10) //--------------------------------------------------------------------------- // New in version 3 void * iCCprofileData; /**< ICC Profile Data. NULL if there is no profile */ int32 iCCprofileSize; /**< Size of the ICC profile; zero if there is no profile. */ int32 compositeChannelCount; /**< Number of composite color channels in this document. */ int32 layerCount; /**< Total number of layers in this document; ignores visbility, adjustments, etc. */ int32 alphaChannelCount; /**< Number of alpha channels in this document. */ //--------------------------------------------------------------------------- // Version 4 fields (added in Photoshop CS3/10) //--------------------------------------------------------------------------- // Layer mask for the target layer: ReadChannelDesc *targetSmartFilterMask; //--------------------------------------------------------------------------- // Version 5 fields (added in Photoshop CS6/13) //--------------------------------------------------------------------------- // The Unicode file specification, if any. struct SPPlatformFileSpecificationW *fileSpecW; //--------------------------------------------------------------------------- // Version 6 fields (added in Photoshop CC 2015.5 (17.0)) //--------------------------------------------------------------------------- // The file specification, if any. struct XPlatFileSpec *fileSpecX; #ifdef __cplusplus ReadImageDocumentDesc() : minVersion(0), maxVersion(0), imageMode(0), depth(0), hResolution(0), vResolution(0), targetCompositeChannels(NULL), targetTransparency(NULL), targetLayerMask(NULL), mergedCompositeChannels(NULL), mergedTransparency(NULL), alphaChannels(NULL), selection(NULL), fileSpec(NULL), layersDescriptor(NULL), documentType(0), next(NULL), iCCprofileData(NULL), iCCprofileSize(0), compositeChannelCount(0), layerCount(0), alphaChannelCount(0), targetSmartFilterMask(NULL), fileSpecW(NULL), fileSpecX(0) { } #endif // __cplusplus } ReadImageDocumentDesc; ~~~~~~ We see that we have access to a linked list of layers in the document through the `layersDescriptor` pointer in this struct, which will be what we use to iterate through all the layers in the current Photoshop document. ## Writing a filter plug-in ## Similar to how we wrote the automation filter, let's begin with the entry point and the necessary machinery around it. Create a new source file called `tutorial_filter_main.cpp` and fill it with the following: ~~~~~~cpp #include #include #include #include SPBasicSuite *sSPBasic = NULL; void executeFilter(const FilterRecordPtr &filterRecord) { // TODO: implement return; } DLLExport MACPASCAL void PluginMain(const int16_t selector, FilterRecordPtr filterRecord, long long *data, int16_t *result) { switch (selector) { case filterSelectorAbout: sSPBasic = ((AboutRecord *)filterRecord)->sSPBasic; case filterSelectorStart: { sSPBasic = filterRecord->sSPBasic; if (filterRecord->bigDocumentData != NULL) { filterRecord->bigDocumentData->PluginUsing32BitCoordinates = true; } executeFilter(filterRecord); break; } default: break; } return; } ~~~~~~ The code here isn't terribly interesting; basically, at our entry point, we case for whether the filter has been executed from the Photoshop menu, and if so, we check if the current document is considered a "big" document (i.e. a `.psb` document), set the `filterRecord` attribute as appropriate, and call our `executeFilter` function, which is currently stubbed out for now. !!! tip Large Photoshop documents VS standard ones Recall that Photoshop has a very long history, and started out as a 32-bit application. As demands grew, Photoshop documents started to balloon in sizes, to the point where they were starting to brush up against both filesystem limitations and the Photoshop engine as well. As such, Adobe introduced the `.psb` large photoshop document format to complement the existing `.psd` one. The format is essentially shared between the two, but has some subtle differences. The [Photoshop Document File Specification](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/) is a good reference for understanding what some of these differences are. We'll make some global definitions again for this new plug-in, of course, in a file called `tutorial_globals.h`: ~~~~~~cpp #ifndef TUTORIAL_GLOBALS_H #define TUTORIAL_GLOBALS_H #define TUTORIAL_FILTER_PLUGINNAME "FilterTutorial" #define TUTORIAL_FILTER_PLUGINDESC "A filter plug in for Adobe Photoshop that retrieves pixel data info from the current document." #define TUTORIAL_FILTER_UUID "e02828c8-69af-4977-9294-b3221dbcec49" #define TUTORIAL_FILTER_RESOURCE_ID 18008 #define TUTORIAL_FILTER_SUITE_ID 'Filt' #define TUTORIAL_FILTER_CLASS_ID TUTORIAL_FILTER_SUITE_ID #define TUTORIAL_FILTER_EVENT_ID 'filt' #define TUTORIAL_FILTER_VENDORNAME "memyselfandi" #define TUTORIAL_FILTER_TRIGGERNAME "trigger" #define TUTORIAL_FILTER_KEYTRIGGER 'Tgr ' #define TUTORIAL_FILTER_TYPETRIGGER 'TTgr' #define TUTORIAL_FILTER_TRIGGERDESC "Activates the filter." #define TUTORIAL_FILTER_KEYRESULT 'Rslt' #define TUTORIAL_FILTER_TYPERESULT 'TRsl' #define TUTORIAL_FILTER_ENUMINFONAME "Info" #define TUTORIAL_FILTER_ENUMINFO 'Info' #endif ~~~~~~ The resource file (which we will call `tutorial_filter_pipl.r` for the sake of naming conventions) this time will look slightly different, but fairly similar to that of the Automation plug-in we made previously: ~~~~~~cpp #include "PIDefines.h" #include "tutorial_globals.h" #ifdef __PIMac__ #include "PIGeneral.r" #include "PIUtilities.r" #elif defined(__PIWin__) #define Rez #include "PIGeneral.h" #include "PIUtilities.r" #endif #include "PITerminology.h" #include "PIActions.h" resource 'PiPL' ( TUTORIAL_FILTER_RESOURCE_ID, TUTORIAL_FILTER_PLUGINNAME, purgeable ) { { Kind { Filter }, Name { TUTORIAL_FILTER_PLUGINNAME }, Version { (latestFilterVersion << 16) | latestFilterSubVersion }, Component { ComponentNumber, TUTORIAL_FILTER_PLUGINNAME }, #ifdef __PIMac__ CodeMacIntel64 { "PluginMain" }, #else #if defined(_WIN64) CodeWin64X86 { "PluginMain" }, #else CodeWin32X86 { "PluginMain" }, #endif #endif EnableInfo { "true" }, HasTerminology { TUTORIAL_FILTER_CLASS_ID, TUTORIAL_FILTER_EVENT_ID, TUTORIAL_FILTER_RESOURCE_ID, TUTORIAL_FILTER_UUID }, FilterCaseInfo { { inWhiteMat, outWhiteMat, doNotWriteOutsideSelection, filtersLayerMasks, worksWithBlankData, copySourceToDestination, inWhiteMat, outWhiteMat, writeOutsideSelection, filtersLayerMasks, worksWithBlankData, copySourceToDestination, inWhiteMat, outWhiteMat, writeOutsideSelection, filtersLayerMasks, worksWithBlankData, copySourceToDestination, inWhiteMat, outWhiteMat, doNotWriteOutsideSelection, filtersLayerMasks, worksWithBlankData, copySourceToDestination, inWhiteMat, outWhiteMat, writeOutsideSelection, filtersLayerMasks, worksWithBlankData, copySourceToDestination, inWhiteMat, outWhiteMat, doNotWriteOutsideSelection, filtersLayerMasks, worksWithBlankData, copySourceToDestination, inWhiteMat, outWhiteMat, writeOutsideSelection, filtersLayerMasks, worksWithBlankData, copySourceToDestination } }, } }; resource 'aete' ( TUTORIAL_FILTER_RESOURCE_ID, " dictionary", purgeable) { 1, 0, english, roman, /* aete version and language specifiers */ { TUTORIAL_FILTER_VENDORNAME, /* vendor suite name */ TUTORIAL_FILTER_PLUGINDESC, /* optional description */ TUTORIAL_FILTER_SUITE_ID, /* suite ID */ 1, /* suite code, must be 1 */ 1, /* suite level, must be 1 */ { /* structure for filters */ TUTORIAL_FILTER_PLUGINNAME, /* unique filter name */ TUTORIAL_FILTER_PLUGINDESC, /* optional description */ TUTORIAL_FILTER_CLASS_ID, /* class ID, must be unique or Suite ID */ TUTORIAL_FILTER_EVENT_ID, /* event ID, must be unique to class ID */ NO_REPLY, /* never a reply */ IMAGE_DIRECT_PARAMETER, /* direct parameter, used by Photoshop */ { /* parameters here, if any */ TUTORIAL_FILTER_TRIGGERNAME, TUTORIAL_FILTER_KEYTRIGGER, TUTORIAL_FILTER_TYPETRIGGER, TUTORIAL_FILTER_TRIGGERDESC, flagsSingleParameter } }, { /* non-filter plug-in class here */ }, { /* comparison ops (not supported) */ }, { /* any enumerations */ } } }; ~~~~~~ And create the `build.bat` required for building this new plugin: ~~~~~~batch @echo off setlocal echo Build script started executing at %time% ... set BuildType=%1 if "%BuildType%"=="" (set BuildType=release) set PhotoshopSDKRoot=C:\Users\sonictk\src\thirdparty\adobe\photoshop\cc_2019_win\pluginsdk set CnvtPiPLExePath="%PhotoshopSDKRoot%\samplecode\resources\Cnvtpipl.exe" set PhotoshopPluginsDeployPath=C:\Users\sonictk\psplugins call "%vs2017installdir%\VC\Auxiliary\Build\vcvarsall.bat" x64 set BuildDir=%~dp0msbuild if not exist %BuildDir% mkdir %BuildDir% pushd %BuildDir% set ProjectName=tutorial_filter set EntryPoint=%~dp0src\%ProjectName%_main.cpp set ResourcePiPL=%~dp0src\%ProjectName%_pipl.r set ResourceRC=%BuildDir%\%ProjectName%_pipl.rc set ResourceRES=%BuildDir%\%ProjectName%_pipl.res set ThirdPartyDirPath=%~dp0..\thirdparty set OutBin=%BuildDir%\%ProjectName%.8bf set CommonLinkerFlags=/dll /incremental:no /machine:x64 /nologo /defaultlib:Kernel32.lib /defaultlib:User32.lib /defaultlib:Shell32.lib /nodefaultlib:LIBCMTD.lib /nodefaultlib:LIBCMT.lib "%ResourceRES%" set DebugLinkerFlags=%CommonLinkerFlags% /opt:noref /debug /pdb:"%BuildDir%\%ProjectName%.pdb" set ReleaseLinkerFlags=%CommonLinkerFlags% /opt:ref set RelWithDebInfoLinkerFlags=%CommonLinkerFlags% /opt:ref /debug /pdb:"%BuildDir%\%ProjectName%.pdb" set PSPreprocessorDefines=/DISOLATION_AWARE_ENABLED=1 /DWIN32=1 /D_CRT_SECURE_NO_DEPRECATE /D_SCL_SECURE_NO_DEPRECATE /D_WINDOWS /D_USRDLL /D_WINDLL /D_MBCS set PSCompilerFlags=/EHsc set CommonIncludePaths=/I "%ThirdPartyDirPath%" /I "%ThirdPartyDirPath%\psapi\common\includes" /I "%ThirdPartyDirPath%\psapi\common\resources" /I "%ThirdPartyDirPath%\psapi\common\sources" /I "%ThirdPartyDirPath%\psapi\photoshop" /I "%ThirdPartyDirPath%\psapi\pica_sp" /I "%ThirdPartyDirPath%\psapi\resources" /I "%ThirdPartyDirPath%\psapi\ai" /I "%BuildDir%" set CommonCompilerFlags=/nologo /W3 /WX %CommonIncludePaths% /Zc:__cplusplus /arch:AVX2 %PSCompilerFlags% %PSPreprocessorDefines% set CompilerFlagsDebug=%CommonCompilerFlags% /Od /Zi /D_DEBUG /MDd set CompilerFlagsRelease=%CommonCompilerFlags% /Ox /DNDEBUG /MD set CompilerFlagsRelWithDebInfo=%CommonCompilerFlags% /Ox /Zi /DNDEBUG /MD if "%BuildType%"=="debug" ( set BuildCommand=cl %CompilerFlagsDebug% "%EntryPoint%" /Fe:"%OutBin%" /link %DebugLinkerFlags% set BuildRRCommand=cl %CompilerFlagsDebug% /EP /DMSWindows=1 /Tc "%ResourcePiPL%" ) else if "%BuildType%"=="relwithdebinfo" ( set BuildCommand=cl %CompilerFlagsRelWithDebInfo% "%EntryPoint%" /Fe:"%OutBin%" /link %RelWithDebInfoLinkerFlags% set BuildRRCommand=cl %CompilerFlagsRelWithDebInfo% /EP /DMSWindows=1 /Tc "%ResourcePiPL%" ) else ( set BuildCommand=cl %CompilerFlagsRelease% "%EntryPoint%" /Fe:"%OutBin%" /link %ReleaseLinkerFlags% set BuildRRCommand=cl %CompilerFlagsRelease% /EP /DMSWindows=1 /Tc "%ResourcePiPL%" ) echo. echo Compiling resources (command follows below)... set ResourceRR=%BuildDir%\%ProjectName%_pipl.rr echo %BuildRRCommand% %BuildRRCommand% > "%ResourceRR%" if %errorlevel% neq 0 goto error echo. echo Converting PiPL to Windows resource file format... echo %CnvtPiPLExePath% "%ResourceRR%" "%ResourceRC%" %CnvtPiPLExePath% "%ResourceRR%" "%ResourceRC%" if %errorlevel% neq 0 goto error echo. echo Compiling Windows Resources... rc /v /fo "%ResourceRES%" "%ResourceRC%" if %errorlevel% neq 0 goto error echo. echo Compiling source files for filter (command follows below)... echo %BuildCommand% echo. echo Output from compilation: %BuildCommand% if %errorlevel% neq 0 goto error echo. echo Deploying built binaries and symbols... copy /Y "%OutBin%" "%PhotoshopPluginsDeployPath%" if %errorlevel% neq 0 goto error if "%BuildType%"=="debug" ( copy /Y "%BuildDir%\%ProjectName%.pdb" "%PhotoshopPluginsDeployPath%" if %errorlevel% neq 0 goto error ) if %errorlevel% == 0 goto success :error echo. echo *************************************** echo * !!! An error occurred!!! * echo *************************************** goto end :success echo. echo *************************************** echo * Build completed successfully. * echo *************************************** goto end :end echo. echo Build script finished execution at %time%. popd endlocal exit /b %errorlevel% ~~~~~~ You'll notice that we're skipping creating a `.zxp` package and signing it in this one; you can figure that out if you need to actually end up deploying by referring to the earlier Section [Package signing]. For now, let's just focus on development and faster builds. You should be able to now build this and get a `.8bf` file (which is the extension for Photoshop filter plug-ins). ## Accessing layer data ## Let's actually start doing something with that `FilterRecordPtr` we have and start filling up our `executeFilter` function with some meat. First, we'll get a pointer to the `ReadImageDocumentDesc` struct that is passed in from our `FilterRecordPtr`, which essentially gives me access to the entire Photoshop document's data (at least, the data that is publicly exposed to us through the API), and then we get access to the layers in the document through the `ReadLayerDesc` pointer, which, as we can see, is a linked list of `ReadLayerDesc` structs containing information about each layer. ~~~~~~cpp ReadImageDocumentDesc *docInfo = filterRecord->documentInfo; assert(docInfo != NULL); if (docInfo->layerCount == 0) { return; } ReadLayerDesc *layerDesc = docInfo->layersDescriptor; if (layerDesc == NULL) { return; } ~~~~~~ We then create a temporary directory to prepare for storing our exported images, and walk the linked list of layers to start extracting out data from them. ~~~~~~cpp // NOTE: (sonictk) Create a temporary directory to store the output images. char *tempDirPath = (char *)malloc(MAX_PATH * sizeof(char)); DWORD lenTempDirPath = GetTempPathA((DWORD)MAX_PATH, (LPSTR)tempDirPath); DWORD fileAttrs = GetFileAttributes((LPCTSTR)tempDirPath); if (!(fileAttrs & FILE_ATTRIBUTE_DIRECTORY)) { CreateDirectory((LPCSTR)tempDirPath, NULL); } ~~~~~~ With the temporary directory in hand and our access point to the layer data, we can now walk the linked list: ~~~~~~cpp int layersLeftToProcess = docInfo->layerCount; int validLayers = 0; int progressTotal = layersLeftToProcess; int progressCounter = 0; do { ++progressCounter; --layersLeftToProcess; const char *layerName = layerDesc->name; if (layerName == NULL || layerName[0] == '\0') { layerDesc = layerDesc->next; continue; } char outPath[MAX_PATH]; int lenPath = snprintf(outPath, 0, "%s%c%s.jpg", tempDirPath, OS_PATH_SEP, layerName); snprintf(outPath, lenPath + 1, "%s%c%s.jpg", tempDirPath, OS_PATH_SEP, layerName); // TODO: (sonictk) Read the image pixels and write the output jpg. ++validLayers; if (layerDesc->next != NULL) { layerDesc = layerDesc->next; } } while (layersLeftToProcess > 0 && layerDesc != NULL); free(tempDirPath); return; ~~~~~~ As you can see, we skip layers with empty names, (since that's not possible through the Photoshop GUI under normal circumstances anyway), format an output filename for each layer, and then we (presumably for now) read the image pixels, write them out to disk, and go to the next layer. Before we continue with reading the image pixels, let's do something quick and hook up the built-in Photoshop progress indicator: ~~~~~~cpp // More code here // TODO: (sonictk) Read the image pixels and write the output jpg. gFilterRecord->progressProc(progressCounter, progressTotal); if (gFilterRecord->abortProc()) { break; } ++validLayers; // More code here ~~~~~~ This will cause Photoshop to display a progress bar automatically should the export process take a "long time" (which Photoshop gets to determine, unfortunately), but more importantly, allows the end-user to cancel the operation by virtue of us checking the `abortProc** calback function. So now that we've got that squared away, let's continue with the process of reading the layer data. But first, a little primer on how Photoshop stores layer data. ### Planar-order and pixel-order ### For reasons (not unreasonable ones), Photoshop stores its pixel data differently from what you might expect. We're very used to image APIs that read and write data in **pixel-order format**, where we generally read the red pixel's value, then followed by the green one, and finally the blue one (and alpha as well if present). However, because Photoshop's design allows for any arbitrary number of channels to be present in a document, this access pattern quickly falls apart for certain use cases. Thus, the data for each layer is stored in a linked list of **channels**, which are basically `ReadChannelDesc` structs (found in `PIGeneral.h`): ~~~~~~cpp typedef struct ReadChannelDesc { int32 minVersion; /**< The minimum version needed to interpret this record. */ int32 maxVersion; /**< The maximum version allowable to interpret this record. */ //--------------------------------------------------------------------------- // Version 0 fields: //--------------------------------------------------------------------------- struct ReadChannelDesc *next; /**< Next descriptor in the list. */ PIChannelPort port; /**< Port to use for reading. Provided by host. */ VRect bounds; /**< Bounds of the channel data. */ int32 depth; /**< Depth of the data; horizontal and vertical resolution. */ VPoint tileSize; /**< Size of the tiles, set by the host. The plug-in should use this size if possible to optimize and match the host memory scheme. */ VPoint tileOrigin; /**< Origin for the tiles, set by host. */ Boolean target; /**< Indicates if this a target channel. TRUE if so. */ Boolean shown; /**< Indicates if this channel is visible. TRUE if so. */ int16 channelType; /**< Channel type. See @ref ChannelTypes. */ void *contextInfo; /**< Pointer to additional information dependent on context. */ const char *name; /**< Name of the channel as a C string. */ //--------------------------------------------------------------------------- // Version 1 fields: //--------------------------------------------------------------------------- // New in version 1 PIChannelPort writePort; /**< Port to use for writing if writing is allowed. This generally does not point back to the same collection of pixels. */ //--------------------------------------------------------------------------- // Version 2 fields: //--------------------------------------------------------------------------- // New in version 2 unsigned32 alphaID; /**< Persistent ID for the channel. Only set for alpha channels. */ // New in version 3 const uint16 *unicodeName; /**< Name of the channel as a Unicode string. (platform endianess) */ //--------------------------------------------------------------------------- // Version 4 fields: (added in Photoshop CS3/10) //--------------------------------------------------------------------------- // New in version 4 Boolean isEnabled; /**< Indicates if the read channel is enabled. Used for layer masks and vector masks that can be disabled. */ //--------------------------------------------------------------------------- // Version 5 fields: //--------------------------------------------------------------------------- // New in version 5 VRect limitBounds; /**< Bounds trimmed to transparency. */ #ifdef __cplusplus ReadChannelDesc() : minVersion(0), maxVersion(0), next(NULL), port(NULL), depth(0), target(false), shown(false), channelType(0), contextInfo(NULL), name(NULL), writePort(NULL), alphaID(0), unicodeName(NULL), isEnabled(true) {} #endif // __cplusplus /**< ReadChannelDesc constructor. */ } ReadChannelDesc; ~~~~~~ We see that there is a `port` member here that implies we should be using this to read the data from the channel. We'll deal with this later. But more importantly, this also implies that Photoshop stores layer data in a more channel, or *plane-oriented* manner, also known as in **planar-order format**, where each separate plane of pixels is stored continguously. To illustrate this better, I've provided a diagram below: *********************************************************** * * * Pixel-order format (RGB, RGB, RGB, ...) * * * * +-------------------------+ * * | RGB RGB RGB RGB RGB RGB | * * | RGB RGB RGB RGB RGB RGB | * * | RGB RGB RGB RGB RGB RGB | 6 x 6 x 3 bytes * * | RGB RGB RGB RGB RGB RGB | (1 byte-per-channel) * * | RGB RGB RGB RGB RGB RGB | * * | RGB RGB RGB RGB RGB RGB | * * +-------------------------+ * * * * * * Planar-order format (R, R, ... G, G, ... B, B) * * * * * * +-------------+ Each plane is 6 x 6 bytes * * | R R R R R R | (1 byte-per-channel) * * | R R R R R R | * * | R R R R R R +------+ * * | R R R R R R | | * * | R R R R R R | | * * | R R R R R R | | * * +-------------+ | Image planes representation * * | * * +-------------+ | +-------------+ * * | G G G G G G | | | B B B B B B | * * | G G G G G G | | +-------------+ B | * * | G G G G G G | | | G G G G G G | B | * * | G G G G G G +------+ +-------------+ G | B | * * | G G G G G G | | | R R R R R R | G | B | * * | G G G G G G | +--> | R R R R R R | G | B | * * +-------------+ | | R R R R R R | G |---+ * * | | R R R R R R | G | * * +-------------+ | | R R R R R R |---+ * * | B B B B B B | | | R R R R R R | * * | B B B B B B | | +-------------+ * * | B B B B B B | | * * | B B B B B B +------+ | * * | B B B B B B | | * * | B B B B B B | | * * +-------------+ | * * v * * * * +-------------+ * * | R R R R R R | * * | R R R R R R | * * | R R R R R R | * * | R R R R R R | * * Actual memory layout | R R R R R R | * * we will get if we just | R R R R R R | * * read the planes without | G G G G G G | * * transforming into | G G G G G G | * * pixel-order format | G G G G G G | * * | G G G G G G | * * | G G G G G G | * * | G G G G G G | * * | B B B B B B | * * | B B B B B B | * * | B B B B B B | * * | B B B B B B | * * | B B B B B B | * * | B B B B B B | * * +-------------+ * * * *********************************************************** [Figure [diagram]: An overview of two common types of pixel layouts used to represent an image.] Because we'll be making use of `stb_image_write` to handle writing out our data, this means that we'll need to transform the data from planar-order to pixel-order in order to do so. Let's now access the different channels/planes of our various layers. In each `ReadLayerDesc` structure, there is a `compositeChannelsList` pointer to a linked list of `ReadChannelDesc` structures, each of which contains the data for a single channel in the current layer. Since we're going to only be writing out JPGs for now, we'll focus on the red, green and blue channels. Adding some code to the function above in order to figure out which channels we're interested in will look like this: ~~~~~~cpp #define PS_CHANNEL_ID_RED 1 #define PS_CHANNEL_ID_GREEN 2 #define PS_CHANNEL_ID_BLUE 3 void findRGBChannelsForLayer(const ReadLayerDesc *layerDesc, ReadChannelDesc **rChn, ReadChannelDesc **gChn, ReadChannelDesc **bChn) { ReadChannelDesc *curChn = layerDesc->compositeChannelsList; while (curChn != NULL) { int32_t tileHeight = curChn->tileSize.v; int32_t tileWidth = curChn->tileSize.h; if (tileHeight == 0 || tileWidth == 0) { curChn = curChn->next; continue; } switch (curChn->channelType) { case PS_CHANNEL_ID_RED: *rChn = curChn; break; case PS_CHANNEL_ID_GREEN: *gChn = curChn; break; case PS_CHANNEL_ID_BLUE: *bChn = curChn; break; default: break; } curChn = curChn->next; } return; } // ... snprintf(outPath, lenPath + 1, "%s%c%s.jpg", tempDirPath, OS_PATH_SEP, layerName); ReadChannelDesc *rChn = NULL; ReadChannelDesc *gChn = NULL; ReadChannelDesc *bChn = NULL; findRGBChannelsForLayer(layerDesc, &rChn, &gChn, &bChn); int chnWidth = rChn->bounds.right - rChn->bounds.left; int chnHeight = rChn->bounds.bottom - rChn->bounds.top; gFilterRecord->progressProc(progressCounter, progressTotal); // ... ~~~~~~ "How do I know what the channel IDs are off the top of my head?", you might ask. The answer is unfortunately _just by debugging Photoshop itself manually_. (These channel IDs do not match that of the [Adobe Photoshop file format specification](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_75067), in case you were wondering.) In contrast the code itself is thankfully fairly simple; we keep walking the linked list of layers to find channels that match known channel IDs for red/green/blue channels, and update the pointers to them so that we can read from them next in the process. !!! warning The "non-production" code disclaimer reminder Again, remember, this is _all_ code that is written purely to serve as an example, so if you _were_ looking to try and make this code more robust, I'd first and foremost be checking things like whether the document had all 3 R/G/B channels, whether those channels had non-null data pointers, etc. In order to read from the channel port, we'll be making use of some of the **PICA** functions that Photoshop exposes through the SDK to read from the port. ### PICA and Ports ### Ports in the PICA architecture are essentially Adobe's interfaces to reading/writing data from channels in Photoshop. There are read/write ports that you utilize, which under-the-hood basically do all sorts of things to pass the memory back-and-forth between API boundaries (yours, and Adobe's). While slightly annoying, it's not that big of a deal, as we will see in the next section. !!! note What is PICA? The name Adobe gives to its API for working with external plug-ins is called **Plug-in Component Architecture**, or **PICA** for short. So if you see headers in the SDK that begin with the `PI` prefix, you know where that comes from now! More information about PICA and the other `SP` API "suites", as Adobe calls them, are also available in the SDK in the document called `PICA.pdf`. Reading data from a channel descriptor is relatively straightforward, but before we do that, let's go over how Photoshop exposes the data to you. ### Tiled data ### Because Photoshop documents can get _really_ large in dimensions (and thus memory usage), Photoshop itself will perform tiling in order to only load the specific regions it needs into memory (you can see this yourself when you zoom into a large document and start panning around; tile blocks load in as needed. This is also controllable in your Photoshop preferences, under the **Performance** category.) This is also reflected when reading the data from the port; you retrieve tiles of data from a read-only port, transform the data, and then either write it back to a write-only port (or take that data elsewhere and do things with it, as we will be performing in our example. Therefore, reading the tiled data looks like this: ********************************************************************************************* * * * <------- 512px -----------> 4 tiles total * * * * ^ +-------------------------+ +----------+ ^ +----------+ * * | |////////// | |//////////| | | next | * * | |////////// | |//////////| | | tile |----+ * * | |////////// | |//////////| 128px | to be | | * * | |////////// | |//////////| | | copied | |---+ * * | |bbbbbbbbbb <--------------------------|bbbbbbbbbb| | | | | | * * 512px | | +----------+ v +----------+ | | * * | | copy stride-by-stride | | | | * * | | | <--128px --> +----------+ | * * | | | | | * * | | | +----------+ * * | | | * * | | | * * | | | * * v +-------------------------+ * * large buffer (document size) * * * * * ********************************************************************************************* [Figure [diagram]: An overview of how reading from channel port descriptors into our final image buffer works.] And in code, it looks like this: ~~~~~~cpp void readPSChannelDataIntoBuffer(const ReadChannelDesc *chn, uint8_t *buf, const int bufWidth, int *bytesRead) { int tileHeight = chn->tileSize.v; int tileWidth = chn->tileSize.h; int chnHeight = chn->bounds.bottom - chn->bounds.top; int chnWidth = chn->bounds.right - chn->bounds.left; int numTilesVert = (tileHeight - 1 + chnHeight) / tileHeight; int numTilesHoriz = (tileWidth - 1 + chnWidth) / tileWidth; int chnSize = tileWidth * tileHeight * chn->depth / 8; int numTiles = numTilesVert * numTilesHoriz; int totalBytesRead = 0; VRect curRect; int curX = 0; int curY = 0; for (int vertTile = 0; vertTile < numTilesVert; ++vertTile) { for (int horizTile = 0; horizTile < numTilesHoriz; ++horizTile) { curY = tileHeight * vertTile; PixelMemoryDesc pxMemDesc; pxMemDesc.data = sPSBuffer->New(NULL, chnSize); pxMemDesc.colBits = chn->depth; pxMemDesc.bitOffset = 0; pxMemDesc.depth = chn->depth; curRect.top = vertTile * tileHeight; curRect.left = horizTile * tileWidth; curRect.bottom = curRect.top + tileHeight; curRect.right = curRect.left + tileWidth; if (curRect.bottom > chnHeight) { curRect.bottom = chnHeight; } if (curRect.right > chnWidth) { curRect.right = chnWidth; } pxMemDesc.rowBits = (curRect.right - curRect.left) * chn->depth; int stride = pxMemDesc.rowBits / PS_NUM_OF_BITS_IN_ONE_BYTE; sPSChannelProcs->ReadPixelsFromLevel(chn->port, 0, &curRect, &pxMemDesc); int curRectWidth = curRect.right - curRect.left; int curRectHeight = curRect.bottom - curRect.top; int numBytesRead = curRectWidth * curRectHeight * (chn->depth / PS_NUM_OF_BITS_IN_ONE_BYTE); for (int h=0; h < curRectHeight; ++h) { memcpy(&buf[(curY * bufWidth) + curX], &((uint8_t *)(pxMemDesc.data))[curRectWidth * h], curRectWidth * sizeof(uint8_t)); curY++; } totalBytesRead += numBytesRead; sPSBuffer->Dispose((Ptr *)&pxMemDesc.data); curX = (curX + curRectWidth) % bufWidth; } } *bytesRead = totalBytesRead; return; } ~~~~~~ First, you see that we make use of a new struct known as a `PixelMemoryDesc` descriptor to describe the tile that we are about to read from Photoshop. You can then see that we make use of a new suite function, the `sPSBuffer->New` one, to handle allocation of the memory to read the pixel data for each tile into. We then use the `sPSChannelProcs->ReadPixelsFromLevel` function to acutally do the read, tile by tile. The rest of it is just basic math to copy, stride-by-stride, each row of pixels to its correct position in a combined buffer large enough to store all the tiles together. We'll do this for each of the red, green and blue channels: ~~~~~~cpp int bytesRead = 0; readPSChannelDataIntoBuffer(rChn, rgbPxDataPlanar, chnWidth, &bytesRead); int totalBytesRead = bytesRead; readPSChannelDataIntoBuffer(gChn, rgbPxDataPlanar + totalBytesRead, chnWidth, &bytesRead); totalBytesRead += bytesRead; readPSChannelDataIntoBuffer(bChn, rgbPxDataPlanar + totalBytesRead, chnWidth, &bytesRead); ~~~~~~ ### Writing out the JPGs ### And now comes the annoying part: converting the planar-order data to pixel-order format: ~~~~~~cpp // remapValue() is baiscally just converting a 8 bit integer to its 16/32 bit equivalent. uint8_t remapValue(const uint16_t &val) { uint32_t valTypeRange = (uint32_t)(powf(2.0f, (float)(sizeof(uint16_t) * PS_NUM_OF_BITS_IN_ONE_BYTE) - 1)); uint32_t destTypeRange = (uint32_t)(powf(2.0f, (float)(sizeof(uint8_t) * PS_NUM_OF_BITS_IN_ONE_BYTE) - 1)); float factor = (float)destTypeRange / (float)valTypeRange; uint8_t result = (uint8_t)(factor * (float)val); return result; } uint8_t remapValue(const uint32_t &val) { uint64_t valTypeRange = (uint64_t)(pow(2.0f, (float)(sizeof(uint32_t) * PS_NUM_OF_BITS_IN_ONE_BYTE) - 1)); uint64_t destTypeRange = (uint64_t)(pow(2.0f, (float)(sizeof(uint8_t) * PS_NUM_OF_BITS_IN_ONE_BYTE) - 1)); double factor = (double)destTypeRange / (double)valTypeRange; uint8_t result = (uint8_t)(factor * (double)val); return result; } void convertPlanarToPixelRGB(uint8_t *dest, const uint8_t *const src, const int width, const int height, const int bytesPerChannel) { int numPixels = width * height; int curPixelIdx = 0; int i = 0; for (; i < (numPixels * 3); i+=3) { for (int chn=0; chn < 3; ++chn) { switch (bytesPerChannel) { case 1: dest[i + chn] = src[(curPixelIdx * bytesPerChannel) + (numPixels * chn * bytesPerChannel)]; break; case 2: { uint16_t *buffer16 = (uint16_t *)src; uint16_t curVal = buffer16[curPixelIdx + (numPixels * chn)]; uint8_t remappedVal = remapValue(curVal); dest[i + chn] = remappedVal; break; } case 4: { uint32_t *buffer32 = (uint32_t *)src; uint32_t curVal = buffer32[curPixelIdx + (numPixels * chn)]; uint8_t remappedVal = remapValue(curVal); dest[i + chn] = remappedVal; break; } default: return; } } ++curPixelIdx; } return; } ~~~~~~ Now that we have the data in pixel-order format, we can write out the JPG images. We're making use of [`stb_image_write`](https://github.com/nothings/stb/blob/master/stb_image_write.h) for this, thanks to Sean Barrett (you can, of course, use whatever method you want to write out the data, and in whatever format you'd like as well): ~~~~~~cpp stbi_write_jpg(jpgPath, chnWidth, chnHeight, 3, rgbPxDataPixel, 100); ~~~~~~ (If you're stuck, feel free to refer to the final code example in the repository.) Once you build and deploy the plugin, you should now see it available for use under the **Filter > Other** menu in Photoshop. Running it on a document with layers should result in them being exported to your `%TMP%` directory as JPGs, and (most likely) at a fraction of the time taken than it would have taken had you written this as a CEP panel calling into JSX to perform the export. Cool, huh? # Recipe: Using Automation plug-ins to call filters # So now that we've got our filter plug-in, let's combine our Automation plug-in from the first recipe with it so that artists can call this code to export their layers from the Automation menu, where they might be more accustomed to seeing their plug-in. !!! note Deja vu? This is also illustrated in the SDK with the Automation/Hidden filter plug-in example, but I'll be going over the minimalist version here, minus all the extra stuff it does within the plug-in (UI, randomizing image pixels, etc.) and focusing only on the core problem of calling the filter from the automation plug-in itself. For now, go ahead and copy-paste the source code from the first recipe to act as our automation plug-in for this purpose. Let's modify the build script a little bit to build both plug-ins now: ## The build script ## ~~~~~~batch @echo off setlocal echo Build script started executing at %time% ... set BuildType=%1 if "%BuildType%"=="" (set BuildType=release) set PhotoshopSDKRoot=C:\Users\sonictk\src\thirdparty\adobe\photoshop\cc_2019_win\pluginsdk set CnvtPiPLExePath="%PhotoshopSDKRoot%\samplecode\resources\Cnvtpipl.exe" set PhotoshopPluginsDeployPath=C:\Users\sonictk\psplugins call "%vs2017installdir%\VC\Auxiliary\Build\vcvarsall.bat" x64 set BuildDir=%~dp0msbuild if not exist %BuildDir% mkdir %BuildDir% pushd %BuildDir% set ProjectName=tutorial set AutoEntryPoint=%~dp0src\%ProjectName%_auto_main.cpp set AutoResourcePiPL=%~dp0src\%ProjectName%_auto_pipl.r set AutoResourceRC=%BuildDir%\%ProjectName%_auto_pipl.rc set AutoResourceRES=%BuildDir%\%ProjectName%_auto_pipl.res set FilterEntryPoint=%~dp0src\%ProjectName%_filter_main.cpp set FilterResourcePiPL=%~dp0src\%ProjectName%_filter_pipl.r set FilterResourceRC=%BuildDir%\%ProjectName%_filter_pipl.rc set FilterResourceRES=%BuildDir%\%ProjectName%filter__pipl.res set ThirdPartyDirPath=%~dp0..\thirdparty set AutoOutBin=%BuildDir%\%ProjectName%_auto.8li set FilterOutBin=%BuildDir%\%ProjectName%_filter.8bf set CommonLinkerFlags=/dll /incremental:no /machine:x64 /nologo /defaultlib:Kernel32.lib /defaultlib:User32.lib /defaultlib:Shell32.lib /nodefaultlib:LIBCMTD.lib /nodefaultlib:LIBCMT.lib set DebugLinkerFlags=%CommonLinkerFlags% /opt:noref /debug set ReleaseLinkerFlags=%CommonLinkerFlags% /opt:ref set RelWithDebInfoLinkerFlags=%CommonLinkerFlags% /opt:ref /debug set FilterCommonLinkerFlags=/dll /incremental:no /machine:x64 /nologo /defaultlib:Kernel32.lib /defaultlib:Shlwapi.lib /defaultlib:User32.lib /defaultlib:Shell32.lib /defaultlib:Ole32.lib /defaultlib:Comctl32.lib /nodefaultlib:LIBCMTD.lib /nodefaultlib:LIBCMT.lib set FilterDebugLinkerFlags=%FilterCommonLinkerFlags% /opt:noref /debug /pdb:"%BuildDir%\%ProjectName%_filter.pdb" "%FilterResourceRES%" set FilterReleaseLinkerFlags=%FilterCommonLinkerFlags% /opt:ref "%FilterResourceRES%" set FilterRelWithDebInfoLinkerFlags=%FilterCommonLinkerFlags% /opt:ref /debug /pdb:"%BuildDir%\%FilterProjectName%.pdb" "%FilterResourceRES%" set PSPreprocessorDefines=/DISOLATION_AWARE_ENABLED=1 /DWIN32=1 /D_CRT_SECURE_NO_DEPRECATE /D_SCL_SECURE_NO_DEPRECATE /D_WINDOWS /D_USRDLL /D_WINDLL /D_MBCS set PSCompilerFlags=/EHsc set CommonIncludePaths=/I "%ThirdPartyDirPath%" /I "%ThirdPartyDirPath%\psapi\common\includes" /I "%ThirdPartyDirPath%\psapi\common\resources" /I "%ThirdPartyDirPath%\psapi\common\sources" /I "%ThirdPartyDirPath%\psapi\photoshop" /I "%ThirdPartyDirPath%\psapi\pica_sp" /I "%ThirdPartyDirPath%\psapi\resources" /I "%ThirdPartyDirPath%\psapi\ai" /I "%BuildDir%" set CommonCompilerFlags=/nologo /W3 /WX %CommonIncludePaths% /Zc:__cplusplus /arch:AVX2 %PSCompilerFlags% %PSPreprocessorDefines% set CompilerFlagsDebug=%CommonCompilerFlags% /Od /Zi /D_DEBUG /MDd set CompilerFlagsRelease=%CommonCompilerFlags% /Ox /DNDEBUG /MD set CompilerFlagsRelWithDebInfo=%CommonCompilerFlags% /Ox /Zi /DNDEBUG /MD set AutoLinkerFlagsDebug=%DebugLinkerFlags% /pdb:"%BuildDir%\%ProjectName%_auto.pdb" "%AutoResourceRES%" set AutoLinkerFlagsRelease=%ReleaseLinkerFlags% "%AutoResourceRES%" set AutoLinkerFlagsRelWithDebinfo=%RelWithDebInfoLinkerFlags% /pdb:"%BuildDir%\%ProjectName%_auto.pdb" "%AutoResourceRES%" if "%BuildType%"=="debug" ( set AutoBuildCommand=cl %CompilerFlagsDebug% "%AutoEntryPoint%" /Fe:"%AutoOutBin%" /link %AutoLinkerFlagsDebug% set AutoBuildRRCommand=cl %CompilerFlagsDebug% /EP /DMSWindows=1 /Tc "%AutoResourcePiPL%" set FilterBuildCommand=cl %CompilerFlagsDebug% "%FilterEntryPoint%" /Fe:"%FilterOutBin%" /link %FilterDebugLinkerFlags% set FilterBuildRRCommand=cl %CompilerFlagsDebug% /EP /DMSWindows=1 /Tc "%FilterResourcePiPL%" ) else if "%BuildType%"=="relwithdebinfo" ( set AutoBuildCommand=cl %CompilerFlagsRelWithDebInfo% "%AutoEntryPoint%" /Fe:"%AutoOutBin%" /link %AutoLinkerFlagsRelWithDebInfo% set AutoBuildRRCommand=cl %CompilerFlagsRelWithDebInfo% /EP /DMSWindows=1 /Tc "%AutoResourcePiPL%" set FilterBuildCommand=cl %CompilerFlagsRelWithDebInfo% "%FilterEntryPoint%" /Fe:"%FilterOutBin%" /link %FilterRelWithDebInfoLinkerFlags% set FilterBuildRRCommand=cl %CompilerFlagsRelWithDebInfo% /EP /DMSWindows=1 /Tc "%FilterResourcePiPL%" ) else ( set AutoBuildCommand=cl %CompilerFlagsRelease% "%AutoEntryPoint%" /Fe:"%AutoOutBin%" /link %AutoLinkerFlagsRelease% set AutoBuildRRCommand=cl %CompilerFlagsRelease% /EP /DMSWindows=1 /Tc "%AutoResourcePiPL%" set FilterBuildCommand=cl %CompilerFlagsRelease% "%FilterEntryPoint%" /Fe:"%FilterOutBin%" /link %FilterReleaseLinkerFlags% set FilterBuildRRCommand=cl %CompilerFlagsRelease% /EP /DMSWindows=1 /Tc "%FilterResourcePiPL%" ) echo. echo Compiling resources (command follows below)... set AutoResourceRR=%BuildDir%\%ProjectName%_auto_pipl.rr set FilterResourceRR=%BuildDir%\%ProjectName%_filter_pipl.rr echo %AutoBuildRRCommand% %AutoBuildRRCommand% > "%AutoResourceRR%" if %errorlevel% neq 0 goto error echo %FilterBuildRRCommand% %FilterBuildRRCommand% > "%FilterResourceRR%" if %errorlevel% neq 0 goto error echo. echo Converting Automation PiPL to Windows resource file format... echo %CnvtPiPLExePath% "%AutoResourceRR%" "%AutoResourceRC%" %CnvtPiPLExePath% "%AutoResourceRR%" "%AutoResourceRC%" if %errorlevel% neq 0 goto error echo. echo Converting Filter PiPL to Windows resource file format... echo %CnvtPiPLExePath% "%FilterResourceRR%" "%FilterResourceRC%" %CnvtPiPLExePath% "%FilterResourceRR%" "%FilterResourceRC%" if %errorlevel% neq 0 goto error echo. echo Compiling Windows Resources for automation plug-in... rc /v /fo "%AutoResourceRES%" "%AutoResourceRC%" if %errorlevel% neq 0 goto error echo. echo Compiling Windows Resources for filter plug-in... rc /v /fo "%FilterResourceRES%" "%FilterResourceRC%" if %errorlevel% neq 0 goto error echo. echo Compiling source files for automation plug-in (command follows below)... echo %AutoBuildCommand% echo. echo Output from compilation: %AutoBuildCommand% if %errorlevel% neq 0 goto error echo. echo Compiling source files for filter plug-in (command follows below)... echo %FilterBuildCommand% echo. echo Output from compilation: %FilterBuildCommand% if %errorlevel% neq 0 goto error echo. echo Deploying built binaries and symbols... copy /Y "%AutoOutBin%" "%PhotoshopPluginsDeployPath%" if %errorlevel% neq 0 goto error echo. echo Deploying built binaries and symbols... copy /Y "%FilterOutBin%" "%PhotoshopPluginsDeployPath%" if %errorlevel% neq 0 goto error if "%BuildType%"=="debug" ( copy /Y "%BuildDir%\%ProjectName%_auto.pdb" "%PhotoshopPluginsDeployPath%" if %errorlevel% neq 0 goto error copy /Y "%BuildDir%\%ProjectName%_filter.pdb" "%PhotoshopPluginsDeployPath%" if %errorlevel% neq 0 goto error ) if %errorlevel% == 0 goto success :error echo. echo *************************************** echo * !!! An error occurred!!! * echo *************************************** goto end :success echo. echo *************************************** echo * Build completed successfully. * echo *************************************** goto end :end echo. echo Build script finished execution at %time%. popd endlocal exit /b %errorlevel% ~~~~~~ Nothing really fancy; we're just duplicating the steps twice and compiling the filter and automation plug-ins separately (along with a bunch of options for the `.pdb` files depending on what build configuration we're using.) We're also sharing a single header file between the two that stores the various identifiers that the plug-ins use when registering with Photoshop, for reasons which will become clear later on. !!! warning We also re-generate all UUIDs for these two new plug-ins, even if they are functionally equivalent to the previous ones covered in earlier recipes, If you don't, Photoshop might get confused as to which plug-in to register and very odd behaviour can result. ## Hiding the filter plug-in from the UI ## Now that we are building both plug-ins, let's make one change to the PIPL file for the filter plug-in: ~~~~~~cpp // ... Name { TUTORIAL_FILTER_PLUGINNAME }, Category { "**Hidden**" }, Version { (latestFilterVersion << 16) | latestFilterSubVersion }, // ... ~~~~~~ We do this so that the filter itself doesn't show up in the Photoshop UI, allowing us to ensure that the end-user only has a single entry point via the UI to access our logic. This can be helpful if you, for example, need to do some sort of initialization or startup before running the filter plug-in. An example we're going to show right now is controlling the undo stack before activating the filter plug-in itself. ## Calling the filter from an Automation plug-in ## So now, we'll actually have our automation plug-in do something other than print out "hello world". How exciting! Let's create a dummy function that will be called from the automation plug-in's entry point first: ~~~~~~cpp // ... if (sSPBasic->IsEqual(caller, kPSPhotoshopCaller)) { if (sSPBasic->IsEqual(selector, kPSDoIt)) { PSActionsPlugInMessage *tmpMsg = (PSActionsPlugInMessage *)message; status = MenuExecuteCB(); } } // ... ~~~~~~ And `MenuExecuteCB` looks like this: ~~~~~~cpp SPErr MenuExecuteCB() { SPErr status = kSPNoError; Auto_Ref reference; ASZString nameAsZString = NULL; sPSUIHooks->GetPluginName(message->d.self, &nameAsZString); sPSActionReference->PutEnumerated(reference.get(), classDocument, typeOrdinal, enumTarget); sPSActionControl->SuspendHistory(reference.get(), ::ExecuteCB, NULL, nameAsZString); sASZString->Release(nameAsZString); return gError; } ~~~~~~ Now this will probably be unfamilar to you. What exactly is happening here? Well, as it turns out, the Automation plug-in is really Adobe's way of exposing the [Photoshop Actions](https://helpx.adobe.com/photoshop/using/actions-actions-panel.html) suite to you. If you're unfamiliar with Photoshop Actions, the lowdown is that they're basically a GUI feature that allows artists to batch together actions quickly by recording the steps that they've taken in a document (changing layer opacity, deleting a layer, making a selection, filling said selection etc.) and then playing them back again. Each Action has a **descriptor** that basically is metadata stored about the Action so that Photoshop knows what to actually _do_. This metadata varies between different types of actions, and can be anything from string parameters, floats, lists, etc. !!! tip More about Photoshop Actions Because there is so little documentation on Photoshop Actions within the SDK, my advice is to build the Listener plug-in within the SDK and use it to record various types of Actions that you're interested in performing within the scope of your plug-in. (It's also worth noting that actions can _only_ be performed from within Automation plug-ins) For now, the actions we're performing above basically "freeze" the history stack that Photoshop maintains while the ``ExecuteCB`` call is executing, and once it's done, a single history item is stored with the exact name we specify. This improves the performance of your filter, and also reduces confusion for the end-user. You'll also note that we make use of various suites to perform these actions, which unfortunately isn't really well-documented either; you'll just have to get used to using the Listener plug-in and reading the various header files to get a sense of which functions are available under the various suites and which ones are appropriate for the Photoshop functionality that you're attempting to make use of. Let's implemente `ExecuteCB` now: ~~~~~~cpp SPErr ExecuteCB(void *params) { SPErr stat = kSPNoError; Auto_Desc descriptor; Auto_Desc result(false); DescriptorTypeID workFilterEventID; DescriptorEnumID workFilterEnumID = TUTORIAL_FILTER_ENUMINFO; DescriptorEnumTypeID workFilterTypeID; sPSActionDescriptor->PutEnumerated(descriptor.get(), TUTORIAL_FILTER_KEYTRIGGER, TUTORIAL_FILTER_TYPETRIGGER, 0); sPSActionControl->StringIDToTypeID(TUTORIAL_FILTER_UUID, &workFilterEventID); sPSActionControl->Play(&result, workFilterEventID, descriptor.get(), plugInDialogSilent); if (result.get() != NULL) { sPSActionDescriptor->GetEnumerated(result.get(), TUTORIAL_FILTER_KEYRESULT, &workFilterTypeID, &workFilterEnumID); } else { BOOL status = EnumWindows(getPSMainWindowCB, (LPARAM)NULL); assert(status != 0); MessageBoxA(globalPSMainWindowHwnd, "Error occurred!", "Error", MB_OK|MB_ICONERROR); return stat; } return stat; } ~~~~~~ This is the actual meat right here; you can see why now we share the header files between the two plug-ins: we need to give access to the Automation plug-in to call the UUID of the filter plug-in through the actions suite. Additional IDs can be passed into the filter plug-in as well, in order to read/write data and communicate between the two plug-ins (for example, if you wanted a Plain-Old-Data (POD) result from the filter once it had finished executing). We'll not be making use of that here in this example. Compile, deploy, run Photoshop, and you should be able to see the Automation plug-in show up in your Automation menu in the main menu, while the Filter plug-in should have disappeared from the Filter menu. You should then be able to run the Automation plug-in and have it generate the JPGs like before instead! # Recipe: Communicating between CEP panels and C++ plug-ins # The last recipe I'll be focusing on today involves a tiny bit of JavaScript/JSX (which is necessary when creating CEP panels). I will _not_ be going over how to create CEP panels, so please refer to the source code example in the repository if you need guidance. You can also refer to the [CEP documentation](https://github.com/Adobe-CEP/Getting-Started-guides), which is _much_ better documented than the C++ SDK. This recipe will show you how to trigger an Automation plug-in (which you can extend to anything, as we've seen from our previous example) from a CEP panel. You might find this useful, for example, if you needed functionality from the CEP suite (or vice versa), or if you needed your UI to have native "look and feel" of the Photoshop interface (or dockable**. How this is going to work, in a diagram: ************************************************************************************* * +----------------------+ * * | | * * | CEP Panel (HTML) | * * | | * * +-------------+--------+ * * | * * | * * | * * +--------v---------+ * * | JavaScript | * * | (.js) | * * +---------------------+ | | * * | | +---------+--------+ * * | AutomationPlugin | | * * | (.8li dll) | | * * +-------^-------------+ | * * | | * * | +--------------------+ +------v-----------+ * * | subscribes | | dispatches | Adobe | * * +----------------> PlugPlugOwl.dll <-------------+ ExtendScript | * * to events | | events | (.jsx) | * * +--------^-----------+ +------------------+ * * | * * | * * | * * +--------v---------------+ * * | | * * | CSXS event | * * | (metadata) | * * +------------------------+ * ************************************************************************************* [Figure [diagram]: An overview of CSXS communication between the CEP panel and native plug-in.] It's also worth noting here that the JSX layer can _also_ subscribe to events; it doesn't need to be limited to just one-way traffic, as we will see later. ## Additonal requirements ## Before we begin, you'll need to download the [Adobe Illustrator SDK](https://console.adobe.io/downloads) as well from Adobe's website. We'll be modifying copies of the `SDKPlugPlug.h` and `SDKPlugPlug.cpp` sources within the SDK; these are not available in the Photoshop SDK itself. These sources normally live at `samplecode\common\source` and `samplecode\common\includes` in the Illustrator SDK. Make copies of these two files and place them in your current project's sources. !!! note What is CSXS? CSXS, or the Adobe **Creative Suites Extended Services**, is the name given to the set of functionalities that Adobe exposes to allow different Creative Suite products (Photoshop, Premiere Pro, Illustrator, After Effects etc.) to communicate with each other. CEP is the intended successor of this moniker, but for both historical and practical reasons (CEP does not encompass all previous functionality), you'll find the legacy moniker scattered around the codebase as you look around Photoshop's SDK. I also won't bother with signing the extension in this recipe (since we already covered how to do that in Section Package signing), so make sure that you [follow the instructions](https://github.com/Adobe-CEP/Getting-Started-guides/blob/master/Client-side%20Debugging/readme.md#set-the-debug-mode) for how to enable running unsigned extensions within Photoshop on your development machine. ## A sample CEP panel ## As stated, I'm going to skip how to create a panel. If you're familiar with how to create UI in a declarative-style manner, you'll grok this in no time if you take a look at the actual CEP documentation. Besides, as you will see, it's really just HTML markup: ~~~~~~html

Export layers

Exports layers from the current document.

~~~~~~ !!! note Where can I get `CSInterface.js`? Again, this is available as part of the Adobe CEP SDK. The CSS used is available in the repository, but could easily just be omitted as well, since it's just for styling purposes. The logic driving the UI is also dirt-simple as well: ~~~~~~javascript var globalCSInterface = new CSInterface(); var extensionRootDirPath = globalCSInterface.getSystemPath(SystemPath.EXTENSION); function ExportLayersCB() { globalCSInterface.evalScript("ESPSExportLayers();"); return; } function ClosePanelCB() { globalCSInterface.closeExtension(); return; } btnAccept.onclick = ExportLayersCB; btnClose.onclick = ClosePanelCB; globalCSInterface.evalScript("ESPSActivateAutomationPlugin();"); ~~~~~ Yes, it's JavaScript, or rather, a specific dialect of JavaScript that Adobe calls **ExtendScript**. It's used for scripting in Adobe products (well, apart from Flash, but that's dead) and is generally the "supported" route when it comes to extending Photoshop these days. You can see that we're basically hooking up the buttons to fire off JavaScript functions, which make use of the `CSInterface` singleton that we acquire in the beginning. The **Close** button will, as the name implies, close the panel. The other button, however, will run something called `ESPSExportLayers()`, while there's something at the end of the script that calls `ESPSActivateAutomationPlugin()`. `ESPSExportLayers` looks like this: ~~~~~~javascript function ESPSExportLayers() { // The library might not exist in some Photoshop versions. try { var xLib = new ExternalObject("lib:\PlugPlugExternalObject"); } catch (e) { alert(e); return; } if (xLib) { var eventObj = new CSXSEvent(); eventObj.type = EXPORT_LAYERS_CSXS_EVENT_ID; eventObj.data = true; eventObj.dispatch(); return; } return; } ~~~~~~ ## `SDKPlugPlug` to the rescue ## Now, in order to actually make use of the CSXS functionality, we're going to need to modify the source files from the Adobe Illustrator SDK in order for them to work in Photoshop. First of all, if we take a look at `SDKPlugPlug.cpp`: ~~~~~~cpp AIErr SDKPlugPlug::Load(AIFoldersSuite* sAIFolders) { AIErr err = kNoErr; do { #ifdef WIN_ENV AIFolderType folderType = kAIApplicationFolderType; ai::UnicodeString libName("PlugPlugOwl.dll"); #else AIFolderType folderType = kAIContentsFolderType; ai::UnicodeString libName("/Frameworks/PlugPlugOwl.framework"); #endif ai::FilePath path; err = sAIFolders->FindFolder(folderType, false, path); if(err) { break; } path.AddComponent(libName); if(!path.Exists(false)) { err = kCantHappenErr; break; } #ifdef WIN_ENV fhModule = LoadLibraryW(path.GetFullPath().as_WCHARStr().as_LPCWSTR()); if(!fhModule) { err = kCantHappenErr; break; } pFnLoadExtension = (PlugPlugLoadExtensionFn) GetProcAddress(fhModule, "PlugPlugLoadExtension"); if(!pFnLoadExtension) { err = kCantHappenErr; break; } pFnUnloadExtension = (PlugPlugUnloadExtensionFn) GetProcAddress(fhModule, "PlugPlugUnloadExtension"); if(!pFnUnloadExtension) { err = kCantHappenErr; break; } pFnAddEventListener = (PlugPlugAddEventListenerFn) GetProcAddress(fhModule, "PlugPlugAddEventListener"); if(!pFnAddEventListener) { err = kCantHappenErr; break; } pFnRemoveEventListener = (PlugPlugRemoveEventListenerFn) GetProcAddress(fhModule, "PlugPlugRemoveEventListener"); if(!pFnRemoveEventListener) { err = kCantHappenErr; break; } pFnDispatchEvent = (PlugPlugDispatchEventFn) GetProcAddress(fhModule, "PlugPlugDispatchEvent"); if(!pFnDispatchEvent) { err = kCantHappenErr; break; } // ... More code below ~~~~~~ This is pretty much what we're looking for. You can see that on Windows, the code looks for the presence of a DLL called `PlugPlugOwl.dll` (and the corresponding framework on OSX) and then attempts to find a couple of functions within it, functions that we will be making use of in our native C++ plugin to subscribe to events that we emit from our CEP panel. This DLL ships with Photoshop as well with all the same functionality of the Illustrator one, so we don't need to worry about that part, at least. However, in order to actually be able to use the functionality with Photoshop, we're going to have to modify this file and the header as well. Let's add these lines: ~~~~~~cpp #ifdef WIN_ENV #include #define SDKPLUGPLUG_MAX_PATH MAX_PATH #define SDKPLUGPLUG_OWL_DLL_FILENAME "PlugPlugOwl.dll" #endif // ... #include #include #include int getCurrentProcessFileName(char *filename, int size) { DWORD resultDW = GetModuleFileNameA(NULL, (LPTSTR)filename, (DWORD)size); int result = (int)resultDW; return result; } std::string PSFindPlugPlugDLLFileName() { char plugPlugOwlDLLFileName[SDKPLUGPLUG_MAX_PATH]; int processFilePathLength = getCurrentProcessFileName(plugPlugOwlDLLFileName, SDKPLUGPLUG_MAX_PATH); assert(processFilePathLength > 0); std::string result = std::string(plugPlugOwlDLLFileName); size_t lastPathSepIdx = result.rfind("\\"); result.erase(lastPathSepIdx + 1); result += SDKPLUGPLUG_OWL_DLL_FILENAME; return result; } ~~~~~~ Nothing particularly fancy; we're basically just looking for a `PlugPlugOwl.dll` file based off the current Photoshop executable's directory, and then continue to load it into Photoshop. After that, we continue the same way by looking for the addresses of the symbols that we're interested in within the DLL. While we're at it, because we're actually making Photoshop plug-ins and not Illustrator ones, we also probably should change those `AIErr` return status codes to `SPErr` ones that we actually have available: ~~~~~~cpp SPErr SDKPlugPlug::Load() { SPErr err = kNoErr; do { // NOTE: (sonictk) Find the DLL location dynamically instead of having it hard-coded // to a specific file path, since this can change depending on the Photoshop install location // in various deployment systems. std::string libName = PSFindPlugPlugDLLFileName(); #ifdef WIN_ENV fhModule = LoadLibraryA(libName.c_str()); if(!fhModule) { err = kCantHappenErr; break; } pFnLoadExtension = (PlugPlugLoadExtensionFn) GetProcAddress(fhModule, "PlugPlugLoadExtension"); if(!pFnLoadExtension) { err = kCantHappenErr; break; } pFnUnloadExtension = (PlugPlugUnloadExtensionFn) GetProcAddress(fhModule, "PlugPlugUnloadExtension"); if(!pFnUnloadExtension) { err = kCantHappenErr; break; } pFnAddEventListener = (PlugPlugAddEventListenerFn) GetProcAddress(fhModule, "PlugPlugAddEventListener"); if(!pFnAddEventListener) { err = kCantHappenErr; break; } pFnRemoveEventListener = (PlugPlugRemoveEventListenerFn) GetProcAddress(fhModule, "PlugPlugRemoveEventListener"); if(!pFnRemoveEventListener) { err = kCantHappenErr; break; } pFnDispatchEvent = (PlugPlugDispatchEventFn) GetProcAddress(fhModule, "PlugPlugDispatchEvent"); if(!pFnDispatchEvent) { err = kCantHappenErr; break; } #else // ... More code below ~~~~~~ If you need more help, or just want to use the final result, The full example is available in the `thirdparty/psapi/ai` folder in the accompanying [source repository](https://github.com/sonictk/ps_cpp_recipes). !!! note Why isn't this in the Photoshop SDK natively? Your guess is as good as mine. But according to Tom Ruark of Adobe: _"Using those files in a Photoshop plugin and Photoshop as the host is perfectly fine. I never added them to the Photoshop SDK as I never had an official plugin to exercise them with. The CEP SDK was a separate thing and I didn't want to tie them together."_ So there you have it. The reason is...the lack of example documentation from themselves? I'm honestly not sure what to make of it other than "we don't have time." ## CEP to C++ ## So now that our native plug-in is ready to communicate, let's test it out. We'll go ahead and subscribe to an event of our choosing in the native plug-in: ~~~~~~cpp #define EXPORT_LAYERS_CSXS_EVENT_ID "com.examples.exportlayers.exportlayers" static SDKPlugPlug *globalSDKPlugPlug = NULL; static bool globalEventListenerRegistered = false; void CSXSEventExportLayersCB(const csxs::event::Event *const event, void *const context) { if (event->data == NULL) { return; } BOOL status = EnumWindows(getPSMainWindowCB, (LPARAM)NULL); assert(status != 0); MessageBoxA(globalPSMainWindowHwnd, "Hello World from CEP!", "Tutorial Dialog", MB_OK|MB_ICONINFORMATION); return; } if (!globalSDKPlugPlug) { globalSDKPlugPlug = new SDKPlugPlug; status = globalSDKPlugPlug->Load(); if (status != kSPNoError) { delete globalSDKPlugPlug; return status; } } if (!globalEventListenerRegistered) { csxs::event::EventErrorCode csxsStat; csxsStat = globalSDKPlugPlug->AddEventListener(EXPORT_LAYERS_CSXS_EVENT_ID, CSXSEventExportLayersCB, NULL); if (csxsStat != csxs::event::EventErrorCode::kEventErrorCode_Success) { return kSPLogicError; } globalEventListenerRegistered = true; ~~~~~~ Now, go ahead and open the panel in Photoshop, and click on the button. You should get your dialog pop up back. Congratulations, you've now executed C++ code from your CEP panel? ## C++ to CEP ## Of course, if you needed to trigger events in your CEP panel from C++ (such as updating a progress bar, logging, changing UI elements etc.), you can use the CSXS mechanism to do this as well. Simply dispatch the event from C++: ~~~~~~cpp #define DONE_CSXS_EVENT_ID "com.examples.exportlayers.doneevent" csxs::event::Event pluginDoneEvent; pluginDoneEvent.type = DONE_CSXS_EVENT_ID; pluginDoneEvent.scope = csxs::event::kEventScope_Application; pluginDoneEvent.appId = CSXS_PHOTOSHOP_APPID; pluginDoneEvent.extensionId = TUTORIAL_CSXS_CEP_COMMUNICATION_PLUGINNAME; pluginDoneEvent.data = "Hello back from C++!"; globalSDKPlugPlug->DispatchEvent(&pluginDoneEvent); ~~~~~~ And in your JavaScript layer, subscribe to that event: ~~~~~~javascript var DONE_CSXS_EVENT_ID = "com.examples.exportlayers.doneevent"; globalCSInterface.addEventListener(DONE_CSXS_EVENT_ID, function(event) { AlertDialog(event.data); return; }); function AlertDialog(msg) { globalCSInterface.evalScript("alert(\"" + msg + "\");"); return; } ~~~~~~ Now, once you run your native plug-in to the point where the event is dispatched, you should see Photoshop launch a dialog with your text back from the native plug-in. Again, if you need additional help or have trouble grokking this, the full source code for both the panel and the native plug-in are available in the [repository](https://github.com/sonictk/ps_cpp_recipes). # Closing thoughts # It's been a pretty long road to get to the end, but hopefully by now you should have more confidence about working with the Photoshop C++ SDK, and have a better appreciation for the legacy of an ageing codebase that still sees widespread use today, even as the SDK has been left to rot for quite a while in favour of the CEP layer and all sorts of newfangled web tech stacks. We have tons of artists using Photoshop, but the tooling available at the moment is pretty limited. Hopefully if you're called upon to in the future, you'll be able to make a difference with your newfound knowledge. If you see any errors or have any questions about this tutorial, feel free to [file an issue](https://github.com/sonictk/ps_cpp_recipes/issues) on the Github repo. # Credits # * Tom Ruark of Adobe, for helping to assist with some of my queries. * [Sean Barrett](https://github.com/nothings/stb), for the `stb` libraries which we make use of in this tutorial. * [Morgan Mcguire](https://casual-effects.com/markdeep/), for the Markdeep format. * Aras Pranckevicius, for writing the original Markdeep theme used in this tutorial.