Saturday, March 6, 2010

Photo-bot



My latest project for school involved creating a photo editing and sharing website using Google App Engine. I was excited about the opportunity because (a) I love creating web apps and (b) I was interested in learning app engine. I was pretty happy with the results of my assignment, so I thought I'd share my site with everyone: http://photo-bot.appspot.com/.

There was one pretty tricky use case I had to handle for the assignment. App Engine limits the size of Blobs saved in their datastore to 1MB. However, I had to allow users to upload photos greater than 1MB. Before saving to the datastore, the images needed to be resized. Simply keeping the image data in memory and calling images.resize() is not good enough, since the call to images.resize() actually saves the image data in the datastore, which throws an exception when the image is too large.

To solve the problem, I used blobstore. Blobstore allows files up to 10MB in size. The images could then retrieved from the blobstore, resized and saved to the datastore. The blobstore entry would then be deleted, since saving to the blobstore costs $$. :)

Of course, this makes the code more complex. And, of course, documentation on the App Engine google page is quite limited! Luckily, I found some excellent code posted by Benjamin Pearson. Thanks to Benjamin for posting this code, it helped immensely in trying to solve the problem! I thought I'd post my code, too, in order to get more information out there on the topic. Here's what my basic class looked like:
1:  class AddPhotos(blobstore_handlers.BlobstoreUploadHandler, GenericHandler):
2: def get(self):
3: uploadUrl = blobstore.create_upload_url('/addPhotos')
4: self.template_values['uploadUrl']=uploadUrl
5: self.setTemplate('templates/addPhotos.html')
6:
7: def post(self):
8: upload_files = self.get_uploads()
9: blob_info = upload_files[0]
10: blob_key = blob_info.key()
11: photo = main.Photo(image=db.Blob(self.getImage(maxImageDimension, maxImageDimension, blob_key)),
12: dateAdded=datetime.today(),
13: dateModified=datetime.today(),
14: thumb=db.Blob(self.getImage(maxThumbWidthDimension, maxThumbHeightDimension, blob_key))
15: )
16: photo.put()
17: blobstore.get(blob_key).delete() #delete image after use
18: self.redirect('/')
19:
20: def getImage(self, maxWidthDimension, maxHeightDimension, blob_key):
21: img = images.Image(blob_key=str(blob_key))
22: img.resize(width=maxWidthDimension, height=maxHeightDimension)
23: image = img.execute_transforms(output_encoding=images.PNG)
24: return image

First, a disclaimer about the code: this is the most basic form of my code. I didn't include error checking, among other things. I only wanted to cover the basics, since even the basics can be tricky.

That being said, I want to point out the important pieces of the code (in bold). At line 1, I made my class a subclass of BlobstoreUploadHandler, and not the typical request handler. This is required when uploading to the blobstore.

Next, I created an upload URL, passing it a path (line 3). App engine redirects to a post call on the path that you pass. This path should be in your call to the WSGIApplication constructor:
1:  application = webapp.WSGIApplication([
2: #Main Page
3: ('/', handlers.MainHandler),
4:
5: #Add photos
6: ('/addPhotos', handlers.AddPhotos),
7: ],
8: debug=True)
9: util.run_wsgi_app(application)
In my case, app engine redirects a post call on /addPhotos, which is handled by the AddPhotos class.

Now we have to handle the post and retrieve the blobstore upload. Behind the scenes, Google App engine uploads the file to the blobstore, and allows us to get access to the blob via the key. This is done in lines 8-10. First, the uploaded files are retrieved by calling self.get_uploads(). Next the blob_info is obtained for the first file uploaded. Finally, the key is retrieved. (Note: this could probably be done in one line!)

The key gives access to the image data. In line 21, you can see that a new Image object is created by passing the blob key to the constructor. The image object can be used to resize or transform the uploaded photo using any of the Image functions. After performing the transformations, make sure to execute the transformations by calling execute_transformations() on the Image.

Once the image has been altered, you can save the image to the database as a blob (line 11). Pass the result of the execute_transformations() call to the db.Blob constructor and, as long as your image is now less than 1MB in size, you can save this blob to the datastore!

Finally, delete the blobstore object, since there is no need for it anymore (line 17).

One thing I should mention before I end: you need a billing account with Google before you can use the Blobstore in production (locally it will work fine). However, as long as your blobstore data usage is small, you won't get charged.

Good luck!

No comments:

Post a Comment