-->

Saturday, May 14, 2016

Developing a Google App Engine Web Service

We interrupt this travel blog to bring you some important nerdly information.  A couple of months ago, I completed a web service to handle photo rotations and resizing as part of my personal photo processing work flow. I thought I would document this here for two reasons:


  1. It may be useful to someone else.
  2. It will definitely be useful for me to remember how this stuff works if I ever need to work on it again.

To bring yourself up to speed on my photo processing process, you can refer to my previous blog entry.

I wanted to eliminate the requirement to process all my photos with my Android Photoroto application prior to uploading the photos to google drive. In order to do that, I decided that I would process the rotations as part of my Google Drive script which moves the photos to a new location, and then creates a resized version in a mirror directory structure. Specifically, I created a web service using google app engine which allows a photo to be rotated and resized at the same time. This is a bit different than my old process, where the rotation was done on the original images and then only resized when copied to the mirror. I think this is acceptable for my use at this point, since it is primarily the resized versions which I had difficulty with being rotated incorrectly when using to Google public domain resizing web service. Here is how I created the web service.

Install the Eclipse SDK using this documentation: https://developers.google.com/eclipse/docs/getting_started#running

The code to actually implement the web service is pretty simple. I have included it here:

package net.chartrand.doug.photomanip;
import static com.google.appengine.api.urlfetch.FetchOptions.Builder.disallowTruncate;
import java.io.IOException;
import java.net.URL;
import java.util.Enumeration;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.google.appengine.api.images.CompositeTransform;
import com.google.appengine.api.images.Image;
import com.google.appengine.api.images.ImagesService;
import com.google.appengine.api.images.ImagesService.OutputEncoding;
import com.google.appengine.api.images.ImagesServiceFactory;
import com.google.appengine.api.images.OutputSettings;
import com.google.appengine.api.images.Transform;
import com.google.appengine.api.urlfetch.HTTPMethod;
import com.google.appengine.api.urlfetch.HTTPRequest;
import com.google.appengine.api.urlfetch.URLFetchServiceFactory;
@SuppressWarnings("serial")
public class PhotoManipServlet extends HttpServlet {
 public void doGet(HttpServletRequest req,
 HttpServletResponse resp)
 throws IOException {
     Enumeration<String> parameterNames = req.getParameterNames();
     String url = null;
     int rotate = 0;
     int size=0;
     while (parameterNames.hasMoreElements()) {
         String paramName = parameterNames.nextElement();
         if (paramName.equalsIgnoreCase("url"))
         {
        String[] paramValues = req.getParameterValues(paramName);
        for (int i = 0; i < paramValues.length; i++) {
            String paramValue = paramValues[i];
            url=paramValue;
        }
         }
         if (paramName.equalsIgnoreCase("rotate"))
         {
        String[] paramValues = req.getParameterValues(paramName);
        for (int i = 0; i < paramValues.length; i++) {
            String paramValue = paramValues[i];
            rotate=Integer.valueOf(paramValue).intValue();
        }
         }
         if (paramName.equalsIgnoreCase("size"))
         {
        String[] paramValues = req.getParameterValues(paramName);
        for (int i = 0; i < paramValues.length; i++) {
            String paramValue = paramValues[i];
            size=Integer.valueOf(paramValue).intValue();
        }
         }
     }
     if (url != null && (rotate != 0 || size != 0))
     {
    URL urlo = new URL(url);
    byte[] b = URLFetchServiceFactory.getURLFetchService().fetch(
    new HTTPRequest(urlo, HTTPMethod.GET,
    disallowTruncate().setDeadline(30.0))).getContent();
    Image oldImage = ImagesServiceFactory.makeImage(b);
    Image i = processImage(oldImage, rotate, size);
    resp.setContentType("image/jpeg");
    resp.setContentLength(i.getImageData().length);
    resp.getOutputStream().write(i.getImageData());
    resp.getOutputStream().flush();
    resp.getOutputStream().close();
     }
 }

 static Image processImage(Image o, int degrees, int size)
 {
ImagesService imagesService = ImagesServiceFactory.getImagesService();
CompositeTransform composite = ImagesServiceFactory.makeCompositeTransform();
if (degrees != 0)
{
Transform rotate = ImagesServiceFactory.makeRotate(degrees);
composite.concatenate(rotate);
}
if (size != 0)
{
Transform resize = ImagesServiceFactory.makeResize(size, size);
composite.concatenate(resize);
}
OutputSettings enc = new OutputSettings(OutputEncoding.JPEG);
enc.setQuality(90);
     Image newImage = imagesService.applyTransform(composite, o, enc);
   
return newImage;
 }
}
Once this is deployed to google app engine under a certain appid, I can now use the service in my google app script as follows - with the following changes to the original script.
function getDestBlob(sourcePath, sourceFolderId, srcFile, size)
{
  var data = srcFile.getBlob().getBytes();
  var blob;
  var w = 0;
  var h = 0;
  var rotate = 0;
  var comps = 0;
  var len = data.length;
  Logger.log("Start: " + getShortAt(data, 0, true));
  var offset = 2;
  while (offset < len) {
    var marker = getShortAt(data, offset, true);
    Logger.log("Marker: " + marker);
offset += 2;
if (marker == 0xFFC0) {
        Logger.log("Found the marker!");
   h = getShortAt(data, offset + 3, true);
w = getShortAt(data, offset + 5, true);
comps = data[offset + 7];
break;
} else {
        var inc = getShortAt(data, offset, true);
        Logger.log("inc: " + inc);
offset += inc;
}
  }
  Logger.log("h: " + h + " w: " + w);
 
  var bf = new BinaryFile(data, 0, data.length);
  var tags = ImageInfo.readInfoFromData(bf);
  var orientation = 0;
  if (tags["exif"] != undefined)
  {
    var exif = tags["exif"];
    var datetime = null;
    if (exif["Orientation"]) { orientation = exif["Orientation"];}
  }
  if (orientation == 3) {rotate = 180;}
  if (orientation == 5 || orientation == 6) { rotate = 90; }
  if (orientation == 7 || orientation == 8) {rotate = 270; }
 
  Logger.log("h: " + h + " w: " + w + " o: " + orientation + " r: " + rotate);
 
  var uri = sourceFolderId;
  if (sourcePath.match("^/")) {uri = uri + sourcePath;}
  else {uri = uri + "/" + sourcePath;}
  uri = encodeURI(uri);
  /*throw new Error(uri);*/
  /*throw new Error("Done!");*/
  try {
    var resize = 0;
    if ((w > size) || (h > size)) { resize = size; }
    if (rotate != 0 || resize != 0)
    {
      /* https://images1-focus-opensocial.googleusercontent.com/gadgets/proxy?url=http://googledrive.com/host/0B7HZjn1EQgIxcUpEeE5CcGJCdnM/Family/2013/2013-11/CAM00367.jpg&container=focus&resize_w=650&refresh=31536000 */
      var url = "https://myappid.appspot.com/photomanip?url=http://googledrive.com/host/"+
        uri +
        "&size="+resize+"&rotate="+rotate;
      blob = UrlFetchApp.fetch(url).getBlob();
    }
  }
  catch(err)
  {
    Logger.log("Error on resize: " + err.message);
    // blob = null;
  }
  return blob;
}
The primary change above is to retrieve the photo orientation from the exif and then calculate the appropriate rotation. Then, instead of calling google's opensocial.googusercontent.com webapp to perform just a resize, you called my new appengine webapp to perform both a rotate and resize at the same time. The specific call to the webapp is shown in this example:
var url = "https://myappid.appspot.com/photomanip?url=http://googledrive.com/host/"+        uri +         "&size="+resize+"&rotate="+rotate;blob = UrlFetchApp.fetch(url).getBlob();

Note that you can deploy this webapp for free, and do a limited number of photos per day. Because of this, I changed the appid above from the one that I have deployed to myappid. You would use whatever app id that you decide to develop for your application. myappid will not actually work here, since this app is not deployed at this id (as far as I know). You can also pay to be able to process more photos per day than allowed for free.

After implementing this change, I no longer need to route all my photos to an android platform in order to get the rotations correct. This has simplified my photo processing to a great extent.

Good luck, and let me know if you have any questions. I know this description is kind of short and rough, but I wanted to quickly get it out there while I had a chance.

Print this post

No comments:

Post a Comment