Manipulating images on the Google App Engine with Gaelyk

If you are building a grails application, you can use either imageMagick or the imageTools plugin to resize and crop your images. This is not possible in the Google App Engine because it neither allows you install external packages nor whitelists JAI components.

Fortunately, we can use the Java-based image resizing API provided in the App Engine for simple manipulation.

I’ve written about how to write this in Grails, but since the latest update to the App Engine seems to break all my Grails applications ( which is pretty stupid ), I wanted to try to do this in Gaelyk – the lightweight groovy-based framework for the app engine.

You can play with the application here: http://gaelykresize.appspot.com/upload.groovy

First, we write a simple groovlet to upload our image, call it upload.groovy and put it into the groovy directory of this template.

html.html {
 head {
 title 'Sample to Resize Images on Google App Engine'
 }
 body {
 h1 'Sample to Resize Images on Google App Engine'
 form action: 'changeImage.groovy', method: 'post', enctype: 'multipart/form-data', {
 label 'Photo: ', {
 input type: 'file', name: 'photo'
 }
 br()
 label 'Flip horizontally', {
 input type: 'checkbox', name:'flipHorizontal', checked:true
 }
 br()
 label 'Flip vertically', {
 input type: 'checkbox', name:'flipVertical'
 }
 br()
 label 'Rotate degress clockwise: ', {
 select name: 'rotation', {
 option '0'
 option '90'
 option '180'
 option '270'
 }
 }
 br()
 label "I'm feeling lucky", {
 input type: 'checkbox', name:'feelLucky'
 }
 hr()
 label 'Size: ', {
 select name: 'imageSize', {
 option 'thumb'
 option 'medium'
 option 'large'
 }
 }
 br()
 label 'Custom Size - width', {
 input type:'text', name:'customWidth'
 }
 br()
 label 'Custom Size - height', {
 input type:'text', name:'customHeight'
 }
 br()
 input type: 'submit', name: 'submit', value: 'Resize'
 }
 }
}

This gives us a screen that looks like this:

Then, we write a groovlet called changeImage.groovy like so:

import org.apache.commons.io.IOUtils
import org.apache.commons.fileupload.util.Streams
import org.apache.commons.fileupload.servlet.ServletFileUpload
import com.google.appengine.api.images.Image;
import com.google.appengine.api.images.ImagesService;
import com.google.appengine.api.images.ImagesServiceFactory;
import com.google.appengine.api.images.Transform;
import com.google.appengine.api.images.CompositeTransform;

/* gets image upload. From Mr. Haki's blog - http://mrhaki.blogspot.com/2009/11/add-file-upload-support-to-groovlets-in.html */
uploads = [:]  // Store result from multipart content.
if (ServletFileUpload.isMultipartContent(request)) {
 def uploader = new ServletFileUpload()  // Cannot use diskbased fileupload in Google App Engine!
 def items = uploader.getItemIterator(request)
 while (items.hasNext()) {
 def item = items.next()
 def stream = item.openStream()
 try {
 if (item.formField) {  // 'Normal' form field.
 params[item.fieldName] = Streams.asString(stream)
 } else {
 uploads[item.fieldName] = [
 name: item.name,
 contentType: item.contentType,
 data: IOUtils.toByteArray(stream)
 ]
 }
 } finally {
 IOUtils.closeQuietly stream
 }
 }
}
/* end upload processing */

if (params.submit) {

 // Gaelyk provides a shortcut to the image service, but all the heavy lifting is done by the ImagesServiceFactory.

 // get original image
 Image pic = ImagesServiceFactory.makeImage( uploads['photo'].data )

 // we can use a composite transform to store a series of transformations
 CompositeTransform cp = ImagesServiceFactory.makeCompositeTransform()

 // add a horizontal flip
 if( params.flipHorizontal == 'on'){
 cp.concatenate( ImagesServiceFactory.makeHorizontalFlip() )
 }

 // vertical flip
 if( params.flipVertical == 'on'){
 cp.concatenate( ImagesServiceFactory.makeVerticalFlip() )
 }

 // rotate image - note, this can only happen in increments of 90 degrees
 if( params.rotation ){
 cp.concatenate( ImagesServiceFactory.makeRotate( params.rotation as int ) )
 }

 // I'm feeling lucky - which provides image balancing
 if( params.feelLucky == 'on' )
 {
 cp.concatenate( ImagesServiceFactory.makeImFeelingLucky() )
 }

 // resizing tranformations
 Transform resize

 // first deal with cases where there is no custom width/ height specified
 if( !params.customWidth ){
 switch( params.imageSize ){
 case 'thumb':
 resize = ImagesServiceFactory.makeResize(50,50);
 break;
 case 'medium':
 resize = ImagesServiceFactory.makeResize(200,200);
 break;
 case 'large':
 resize = ImagesServiceFactory.makeResize(400,400);
 break;
 }
 } else {

 // handle case where there is a specified height / width

 resize = ImagesServiceFactory.makeResize( params.customWidth as int, params.customHeight as int)
 }
 cp.concatenate( resize )

 // apply all the transformations to the original image
 // images is a Gaelyk shortcut for imageService. Personally, I think it should map to imagesServiceFactory
 pic = images.applyTransform( cp, pic )

 // set response type
 response.setContentType( uploads['photo'].contentType )

 // render image out
 sout << pic.imageData

}

Here are a few samples:

Original Image:

resized to 400×200, rotate 90 degress.

I’m feeling lucky medium sized, flip vertically.

You can play with the application here: http://gaelykresize.appspot.com/upload.groovy

Notes:

  • you will need to copy the Apache FileUtil libs to your template’s lib directory to get this to work. See Mr. Haki’s blog post for more details.
  • The imageService to image binding by Gaelyk seems pretty useless, since most of the work is actually done by the Factory. Would be nice if the images shortcut provided by Gaelyk mapped to both the service and the Factory.
  • The imageService is very limited. You can do image compositions ( adding one image to another ), crop, rotate in 90 degree angles and flips. For anything more complicated, you should look into integrating an external image service such as Picnik.
  • You can use the blobstore as the image storage engine. Documentation is here.
About these ads

9 thoughts on “Manipulating images on the Google App Engine with Gaelyk

  1. Guillaume Laforge

    Good point about the useless image service itself.
    When I first added that service in the mix, I noticed that lots of useful utility methods were in the factory itself which sounded a bit silly.
    But actually, your suggestion of trying to have the shortcut map to both the factory and the service itself is something I could look into.
    Thanks for the suggestion.

    Reply
  2. Pingback: Sharedy.com: How To Create Facebook’s Status Messages Timeline with Embedly, jQuery and Google App Engine. › PHP App Engine

  3. Thomas

    I tested your app and wondered, why the picture was scaled down interpolated … I tried Image Service Factory myself for a GAE project and it wasn’t …

    Could you give me a hint on this?

    Thx in advance!
    Thomas

    Reply
  4. Guillaume Laforge

    I’m using the images service these days for a small family app (sharing pictures), and I came across that weirdness of factory vs service sharing different set of methods.
    I’m going to fix this in the next version of Gaelyk by combining both classes in a same wrapper.

    Reply
  5. Pingback: Tools used at a Grails Startup – technologies / infrastructure used at Secret Escapes | Tomás Lin’s Programming Brain Dump

  6. Muhammad Hakim

    hi,
    your template project no longer available;
    may I get the source code of this app ? I want to create simple image service (only resize image uploaded by user via mobile application) in AppEngine.

    thank you

    Reply
  7. Pingback: Geb and Sikuli – Adding image recognition to help your functional tests see | Tomás Lin’s Programming Brain Dump

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s