Advanced Image Editing in the Browser

When building any type of content management system or application with user generated content, image uploads are typically a requirement. Writing a blog entry, for example, may require the author to upload one or more different images to go along with the content. Unfortunately, the images we have to work with aren’t always optimized for the web.

When I blog, I can typically handle this myself. After all, I am a web professional. (Seriously, it says so somewhere in my bio.) But, at most, that ends up being either a crop or a resize and nothing more. For people who aren’t technical, even something like Microsoft Paint may be too much.

In this article I’m going to discuss one such way we can provide image editing tools within the browser itself. Obviously more options exist and I’d love to hear what you’ve used on your own sites in the comments below.

Our Weapon of Choice

For this article, we’re going to look at Adobe’s CreativeSDK. Before I continue, a quick disclaimer: I used to work for Adobe. Also, the Creative Cloud suite is a commercial product. As of the time of this writing, the SDK is free, but it’s not clear what Adobe intends do in the future. Ok, enough with the disclaimer.

The Creative SDK is a library that you can add to your mobile application to tie into various aspects of the Creative Cloud. While that part is interesting, it’s not what I’m covering today. Instead, we’ll discuss a part of the SDK that is entirely web-based.

Some time ago Adobe purchased Aviary. Aviary had various tools related to photo editing, including a JavaScript library that integrated with their servers. This library is now part of the SDK and is pretty much 100% of what is considered the “Web” part of the Creative SDK (see more here).

Using the Image Editing Component

The “Image Editing Component” provides a set of browser based tools that can edit images on the fly. An example will make things clearer. (As of the time of writing this article, you did not have to actually sign up for an Adobe account or even create a CreativeSDK application registration. The test keys provided in the docs actually worked just fine for my demos. If that has changed, then simply go through the process of signing up to create your own application key.)

Listing 1: /demos/test1.html

<!-- Load widget code -->
<script type="text/javascript" src="http://feather.aviary.com/imaging/v2/editor.js"></script>

<!-- Instantiate the widget -->
<script type="text/javascript">

    var featherEditor = new Aviary.Feather({
        apiKey: '1234567',
        onSave: function(imageID, newURL) {
            var img = document.getElementById(imageID);
            img.src = newURL;
        }
    });

    function launchEditor(id, src) {
        featherEditor.launch({
            image: id,
            url: src
        });
        return false;
    }

</script>

<!-- Add an edit button, passing the HTML id of the image
    and the public URL to the image -->
<a href="#" onclick="return launchEditor('editableimage1',
    'https://upload.wikimedia.org/wikipedia/commons/thumb/0/0b/Cat_poster_1.jpg/1200px-Cat_poster_1.jpg');">Edit!</a>

<!-- original line of HTML here: -->
<img id="editableimage1" src="https://upload.wikimedia.org/wikipedia/commons/thumb/0/0b/Cat_poster_1.jpg/1200px-Cat_poster_1.jpg"/>

This example is taken straight from the Creative SDK’s docs, but has been modified to use an image that actually works.

The first thing to point out is that in order for the image editing component to work, you must have an image in the DOM. This becomes important rather soon. Right now the image is hard coded, but how you would handle a client-uploaded image? We’ll get there in a bit.

The first thing you’ll notice in the JavaScript above is a simple configuration setting for the editor. As mentioned above, the test key seems to work just fine for now.

Next, a link with an onclick (gross, yes, I know, but deal with it) calls the launchEditor function. This calls the image component’s API to launch an editor. You provide the ID value for the image in the DOM and the source of the image. The source is only required if the image is on a different host than the page itself.

The onSave function is called when the image editing component is done. All that happens here is that the DOM is updated with the new image.

How does it work? The image component speaks to a remote server that will store your image for (approximately) 72 hours. As far as I know there is no way to call the component and have it delete the image when you’re done with it, so if you’re a government agency working on ancient alien mysteries and the such you may wish to avoid this service.

What does it look like? Let’s look at a few shots.

Initial widget load

Beautiful, right? Also a bit overwhelming perhaps. We’ll get to that too in a minute. You can now go crazy and edit away.

Edited image

In the screen shot above, I applied a vignette. I put some stickers on. And I did other stuff. This is why I typically don’t edit images.

Additional Options

At this point, the image in the DOM points to the temporary one stored by AviaryAdobe. You can right click and download if you want, but that’s going to be cumbersome when editing content and might also be too much for some users. Before even addressing that, let’s look at some of the options you have for the widget.

  • First and foremost, you can enable/disable any of the tools. As you can see in the screenshot, there are quite a few and, frankly, the “Stickers” one probably shouldn’t be used by anyone over 13.
  • You can specify a set of crop sizes. For example, if your blog has standard image sizes for article headers, you can specify what those sizes are. What’s even nicer is that you can give them nice names like “Blog Header”. You can even force the user to pick one of these presets. Since all of this is configured via JavaScript, you could specify different rules based on your user’s permissions. So your “content writers” who aren’t technical could be locked into a set of presets while admins can have the presets but not be forced to use them.
  • You have control over what happens when the editor is opened, when an image is saved, when it is closed, and obviously an error handler as well.
  • There are two main themes (light and dark, with dark being the default) but you can also specify a “minimum” theme and customize a lot more via CSS.
  • Finally, there is a more advanced high resolution service you can use for greater clarity images.

As an example of some of the things you can modify, here is a Spanish version using the light theme with much fewer tools enabled.

var featherEditor = new Aviary.Feather({
    apiKey: '1234567',
    theme:'light',
    language:'es',
    tools:'crop,resize,color,sharpness,text',
    onSave: function(imageID, newURL) {
        var img = document.getElementById(imageID);
        img.src = newURL;
    }
});

And the result:

Configured widget

All in all, this is a very well done client-side widget, but obviously there are things missing here. First, how do we handle editing an image that the client has selected to upload but doesn’t exist on your server yet? You could force them to upload their images in a separate tab, but that isn’t condusive to the editing process. Secondly, how do you handle getting the edited image and storing it along with the rest of your data?

The Server-Side to the Rescue!

cue dramatic music here…

In order to get a feel for how this component could be used in a real-world application, I whipped up an incredibly simple Node.js/Express server. Obviously everything I’m going to show here could be done in other server-side languages like PHP or ColdFusion.

I began by creating a form. Even though, for purposes of the sample app, I don’t care about the other data included in the additional form fields, they were added to make it resemble a production application.

The Form

Not very pretty – but you get the point.

Here’s where things get interesting. I need to make it so that when you select an image, we provide a preview. For today I’m taking a snippet from a good Stack Overflow answer:

$('#img').on('change', function(e) {
    //http://stackoverflow.com/a/18457508
    if(this.files && this.files[0]) {

        var reader = new FileReader();

        reader.onload = function (e) {
            $('#previewImg').attr('src', e.target.result).attr('title', 'Click to edit.');
        }

        reader.readAsDataURL(this.files[0]);

        //remove hidden url since they are starting over
        $('#newImg').val('');

    }

});

Basically, on change, if the browser supports the ability to read the files property, we read in the data and create a data URL from it. This could be made a bit more backwards compatible for older browsers, but I’m going to leave that as an exercise for the reader.

The end result is that when an image is selected, it will be displayed in the DOM. Don’t worry about the whole hidden URL thing, we’ll get to that in a moment.

An example.

Next, we need to make that image preview clickable to launch the editor. We could use a button too, or some other form of interaction, but I personally liked tieing it to the click of the image. Now let’s look at the rest of the code.

var featherEditor = new Aviary.Feather({
     apiKey: '1234567',
     onSave: function(imageID, newURL) {
        var img = document.getElementById(imageID);
        $(imageID).attr('src', newURL);
        //copy the url to the hidden form field
        $('#newImg').val(newURL);
        img.src = newURL;
     }
});

function launchEditor(id, src) {
     featherEditor.launch({
         image: id,
         url: src
     });
     return false;
}

The code that launches the editor is exactly the same as before. The code used when the image is saved is now a bit more complex. First, we update the DOM with the new image. Then we update a form field with the ID newImg to the URL as well. Why?

When we submit our form, we’re going to include the two text fields you saw in the screen shot above. We’re also going to include the image you selected. But here is where things get tricky. How do we “send” the new image? What if you don’t edit the image at all?

When posting, we’ll always have a file. (The demo code doesn’t have any validation, but just go with me for a moment.) By telling the image component to update a hidden form field with the new URL, we can use that as a check to see if the user edited the image. This means we have two scenarios:

  1. A new URL exists in a form field – use that.
  2. Use the uploaded file.

Processing the Image on the Server

Ok, so far so good. Let’s look at the form processing to see how we handle this.

app.post('/save', function(req, res) {

    /*
    We need to get the 2 form fields and then:

    a) if form.newImage has a value, it is the remote url, we need to save it
    b) else we get the image uploaded and use it. the client didn't mod anything

    */
    console.log('doing save');
    var form = new formidable.IncomingForm();
    form.parse(req, function(err, fields, files) {
        var newUrl = '/uploads/';
        if(fields.newImg !== '') {

            console.log('We need to download an image.');
            var name = fields.newImg.split('/').pop();
            var file = fs.createWriteStream('./public/uploads/'+name);
            var request = https.get(fields.newImg, function(response) {
                var stream = response.pipe(file);
                stream.on('finish', function() {
                    newUrl += name;
                    res.render('save', {image:newUrl});
                });
            });

        } else {

            console.log('We need to use the image the user sent.');
            //copy to our uploads folder, should validate and make unique
            //http://stackoverflow.com/a/11295106
            var stream = fs.createReadStream(files.img.path).pipe(fs.createWriteStream('./public/uploads/'+files.img.name));
            stream.on('finish', function() {
                newUrl += files.img.name;
                res.render('save', {image:newUrl});
            });

        }
    });
});

First – I process the form using the formidable package. If you haven’t seen it, it is probably the easiest way to handle forms with file uploads in Node.js. As you can see I tell it to use the current form and parse it into a set of fields and files.

If the hidden form field has a value, it must be the remote image. We use the https package to store it and copy it a folder. (And to be clear, this is simply a demo and not secure. We should download the file to a folder outside of the web root and ensure it is an image before copying it over.)

If the form field was blank, we instead use the image file that was uploaded. We can then just copy it over. (And yes, this too is insecure and won’t use unique file names.)

In both cases we create a variable representing the new image and pass it to the next view.

The saved image.

Beautiful. I know.

Conclusion

Adding image editing capabilities can be an incredibly powerful feature for user generated content and, as this article showed, it can be relatively easy to implement using this API. You can find this application in the demos/server folder of the zip file associated with this article. I hope you found this interesting, and as I said, if you’ve got other examples of how you’ve accomplished, I’d love to hear your comments.

Comments