How We Built a Photoshop Extension with HTML, CSS, and JS.

How We Built a Photoshop Extension with HTML, CSS, and JS.

Earlier this week, we released the Creative Market Photoshop Extension — a new way to buy, download, and install awesome design resources without leaving Photoshop! Gerren shared a few high-level design considerations from the process of planning the app; now let’s focus on the nerd guts of it.

The Creative Market Photoshop Extension is a Backbone.js web app that lives inside of Photoshop. We can update it without you having to install a new update. How’s that work? Read on!

Architecture

Creative Suite Extensions are really AIR applications that link a few special libraries from Adobe’s Creative Suite SDK.

We developed the initial prototype as a traditional Flex application, entirely using Flex UI controls. Quickly we realized we wanted something more flexible—a platform that would let us iterate like we iterate on the web. The rigidity of the code combined with the pains of recompiling and issuing updates for the smallest of changes just wasn’t going to work.

At that point, we chose to build the core of the application as a web app that lives on our servers that gets loaded into a mx:HTML control (Webkit).

Advantages

  • Developing with HTML/CSS/JS is fast.
  • Rich open-source ecosystem. Using and contributing to OS projects like jQuery, Handlebars.js, Backbone.js, Backbone.Validation, RequireJS, and Almond makes our lives easier.
  • Share components made for the web. Components like the credit card widget built by Levi for the web purchase process can be dropped in without modification.

Certain functions like downloading & installing products, checking for updates, and general interactions with the filesystem still happen in AS3, but are invoked via Javascript—explained more later.

Native Feel

An experience that felt like a webpage wasn’t an option—we did everything we could to make it snappy & feel like it was a part of Photoshop.

HTML / Asset Caching

The mx:HTML control is great for online use, but there’s little to no control of cache use–which makes offline usage nearly impossible. If you’re disconnected from the Internet, the window will hang until the connection becomes available or it times out (even if the page exists in cache).

We developed a custom OfflineHTML control that extends mx:HTML. It parses HTML and CSS stylesheets, picks out all the static resources (like images and javascript files), downloads them, and rewrites all references to the local copies. That way, when the user opens up the extension again, everything is immediately available and served off disk—even if disconnected from the internet.

(If there were a good way to proxy requests within the HTML control, we would have just used that… but there’s not. Sad panda.)

Live Re-skinning

We generate & apply CSS stylesheets on-the-fly when the extension receives reskin events that contain current UI colors. To generate the stylesheets and add them to the DOM, we use jquery-stylist.

Note: On Photoshop CS5.0, the rendered brightness is off, so we have to lighten every color channel by 0x08 to get it to match Photoshop’s UI.

Image Preloading

When the application’s compiled, we generate a “manifest.json” file that lists all image assets used. As the app starts, we prefetch each in the list so that when the user gets to a view that needs an image… it’s available.

Communication

JS ⇔ AS3

Getting the Javascript web application to talk to the Flex container (and vice versa) happens through a sandbox bridge. It allows you to safely isolate the two domains and treat objects and methods on the other side as if they're not from another language. With that comes a few caveats:

  • Objects cannot contain functions. You can pass function references alone to the other side and invoke them, however.
  • Object prototypes will not match. If passing objects, it's best to serialize them then deserialize on the other end. In AS3, objects coming from JS are represented as __HTMLScriptObject and __HTMLScriptArray types. These types are undocumented and wreak havoc with JSON.encode. They do not always behave.

AS3 ⇒ Photoshop

Controlling Photoshop is easy with JSX scripting. With the Scripting Listener plugin installed, Photoshop logs all actions performed as code to "ScriptingListenerJS.log". If taken, wrapped in a function, and dropped in your project's *.jsx file, the code can be executed from AS3 using evalScript:

var result:SyncResult = CSXSInterface.instance.evalScript('jsxFunction');

If needed, you can pass along arguments:

CSXSInterface.instance.evalScript('jsxFunction', 'Yo!');

However, in CS4 products, only one argument is supported. In CS5+, passing strings with certain characters like quotation marks and backslashes in them will cause a PlugPlugRequestFailed result—for no valid reason.

The solution is to serialize and encode the arguments (hexadecimal) in AS3 then decode them in the JSX script. This will prevent failures from mysteriously-forbidden characters and bypass CS4's limit of only one argument.

Front-end Structure

Backbone.js worked wonderfully for defining views tied to data models (and keeping the two decoupled). The UI is made up of many views within views.

Fundamental Views

  • View: The view that all other views are derived from. Contains methods like: "activate", "deactivate", "repaint", "reflow", and methods for setting up form validation via Backbone.Validation.
  • Page: An extensible view for all pages (Home, Account, Product Detail, etc).
  • PageStack: A container for Page views, controlled by push and pop methods. It manages the visibility of its children so that the last item in the stack is the only one visible. Also, it sets the "previousPage" property on children which is useful for displaying a "Back" button in each.
  • Dialog: An extensible view for all modal dialogs. Dialogs are opened by pages: page.openDialog(dialogView);
  • Root: The main view of the application, containing the footer tab strip and two PageStacks: one for the Browse tab and one for the Account tab.

Event Bus

Models and views are connected to a global event bus. Both can declare an event namespace that will cause all emitted events to be prefixed when sent to the global bus. For example, with our Session model, the namespace is "session". Other models and views can listen for its events easily: "session:change:id".

Session Management

When a user signs in or out, all views with user state must be updated to reflect the change. Basic controls listen for "session:change" events and update accordingly.

Pages require slightly more care. For instance, once a user has signed out, it's important to remove the "Account" page. Rather than doing this manually and having handlers strewn about the application, each subclass of Page simply declares whether it needs a session or not in order to exist:

falseView is cleared when the user signs in.
trueView is cleared when the user signs out.
null"Don't care". Nothing is done in either case.

PageStack views listen for session changes and selectively remove child pages that don't match their defined requirement.

Build Tooling

The conventional method of compiling extensions involves Flash Builder with the Extension Builder plugin. It makes getting started easy, but long term, it becomes difficult to work with as a project’s needs diverge from the basics.

We automated it all—compiling, packaging, application signing, and deploys—using node.js. This is cool for two reasons: (1) use any IDE you want, and (2) it’s fast!

All project settings live in a single "package.json" file. The properties get synthesized to various XML & MXI files needed by the Flex compiler and UCF tool.

Compilation

When compiling a release, we compile two separate versions: one for CS5 and one for CS6. These are both packaged up as a single ZXP installer.

Deploys

To deploy, we generate changelogs, automatically tag the version on Github, generate "update.xml" (along with a JSON representation), and push the files to S3. It's all scripted—there's nothing manual to do with sending out new versions.

In the next few days, we’ll be open sourcing the tool for all of this :) The tool is open-source and available at: https://github.com/creativemarket/csxs

Conclusion

Adobe provides a pretty cool platform that makes it quick to develop for their products. The difficulties lie with scattered & incomplete documentation and platform bugs that are out of your control as a 3rd party developer. Sadly there's a "fend for yourself" developer ecosystem right now. I'd love to see that change.

Our Photoshop Extension is still in its infancy—there's still so much room to grow. As we dream, plan, and develop new features and tighter integration, I hope to share what we learn.

Upcoming API

Are you a developer looking to integrate Creative Market content and inline-purchasing in your own website or app? Apply for an API key today!

Send me more stuff like this!

Subscribe to get the weekly blog post digest in your inbox and never miss a post.


13 Comments

  1. John Stevenson

    hi Brian, These are very generous insights. Thanks. Likely you want to be careful about what Adobe might read here, but I'll say (instead!) that though their three Extension toolkits (Packager, Configurator and Flash Builder) have all been improved recently, there are instances where claimed functionalities don't work. And even some which have been broken by the very newest releases of Photoshop (via the Creative Cloud). So, your alternative foundation work could bring Extensions of many types back to meeting what were the original objectives - specialized, but intuitive, shortcuts.

  2. Hamish Macpherson

    Fantastic summary Brian. Thanks for putting this together.

    "Sadly there's a "fend for yourself" developer ecosystem right now. I'd love to see that change."

    I couldn't agree more. Stay tuned. I'm working on something that will hopefully help that. Things like your build script (can't wait for that!) are a huge step in the right direction.

  3. Jonas Stensgaard

    A very useful extension. Thanks for that.. It'd also be cool if you could upload new products in a small program directly from your desktop or photoshop itself. But it's very cool that you can instantly download and install within photoshop. Useful and fast!

  4. William Gallafent

    First, thanks for the CSXS tool!

    Second, a question: How to I add more assets to be installed as part of the extension?

    In the original project (which I started in Adobe Extension Builder), I have lines like this in the mxi file:

    <file destination="$pluginsfolder/My Plugin" file-type="plugin" platform="mac" products="Photoshop,Photoshop32,Photoshop64" source="My Plugin.plugin"/>

    (“My Plugin.plugin” is a folder in the usual Mac way)

    I tried adding these lines into the mxi of my csxs extension project, below the {{{list-files}}} directive, but when I attempt to install the extension I see errors such as “File '/var/folders/zk/twlblahblahblah/T/tmp1004bbaaaa/My Plugin.plugin' can not be found. The extension will not be installed”.

    Any ideas about the cause of this? What's the Right Way to add more asset files to an extension so they are correctly packaged and incorporated in a way that Extension Manager can find and unpack them?

    Thanks again for csxs, it is making things a great deal more sensible for me while packaging this plugin!

  5. William Gallafent

    A bit more information:

    I've been experimenting with an extension which is built into three .zxp containers (one each for CS5, CS6, CC), and at the moment attempting to install on Windows (though the build was done on a Mac).

    I added a file (let's say “foo.exe”) into the “assets” directory of the build. I can see by examining the three .zxp files that this file is present in all three of them!

    /However/, the file is /mentioned/ (as per my post above) in the Foo.mxi file in the outer “containing” zxp. When Extension Manager attempts to find it in the contents of this outer zxp, it is of course not there!

    So, my question becomes: how may I cause the csxs tool to bundle an asset in the outer “containing” zxp? I do not wish these files to be included three times (once in each build of the extension), just the once in the outermost container.

    I may get to the answer before too long, in which case consider these comments a stream-of-consciousness report …

You must be signed in to post a comment.