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!
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).
- 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.
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
(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.)
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.
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.
JS ⇔ AS3
- 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
__HTMLScriptArraytypes. 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
var result:SyncResult = CSXSInterface.instance.evalScript('jsxFunction');
If needed, you can pass along arguments:
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.
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.
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:
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.
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”.
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:
||View is cleared when the user signs in.|
||View is cleared when the user signs out.|
||“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.
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.
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.
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
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.
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!