Close modal

Blog Post

Uploading an image from iOS using Python (and Bottle)

Development
Mon 23 May 2016
0 Comments


Uploading an image is usually quite straight forward, but iOS uses some magic to handle orientation, and we need to un-magic it to share it properly - learn how we do this.

Don't get your head in a twist

Ever had trouble using images taken on iPhones when uploading or saving to a webpage? This is a common problem and we have the cure for your ailment, more specifically an implementation for Python/Bottle stack using PIL. The principle is similar for other frameworks/services but as always your mileage may vary.

Preview the image from your iPhone

So preview on OSX seems to know what to do with the image.

Image

Now add it to a webpage

Some sample HTML to show this as an image: Test Image Test Link

<html>
  <head>
    <title>test</title>
  </head>
  <body>
    <img src="IMG_3871.JPG" height="100%"/>
  </body>
</html>

But look what happens when we show this in the web browser:

Image

If you're curious why this happens, it's because on iOS images capture are always stored as "Landscape Left" and then the rotation is stored as a metadata - meaning the view has to perform the operation on the pixel buffer to ensure the orientation to the user displays as it was captured. In many cases when the image is loaded through a HTML page <IMG> tag, it does not check this information, even though the native imaging libraries on OSX perfectly handle this image when it's being viewed.

Solution using PIL

Essentially what we need to do to avoid everyone suffering when they download the file is correct the file at the time of upload - to apply the rotation and save it as a normal image.

We will use PIL (Python Image Library), a very good library and well supported to do this.

There is a function to rotate an image: image.rotate(degrees, expand=True) We specify expand as True because we wish to show the whole image when rotated and not just the cropped area.

Actually getting the orientation out of the image is a little convoluted as we have to seek out specific exif keys, and use a non public member like exif = dict(image._getexif().items()). We then map those to known degrees of rotation that we care about but the gist of it is in the implementation as follows:

from bottle import route, run, response, default_app, request
from PIL import Image, ExifTags
from io import BytesIO
from time import time


@route('/upload', method='POST')
def upload_image():
    try:  # Since we only accept JPEG, allow fail rather than check MIME
        with Image.open(BytesIO(request.body.read())) as image:
            orientation = [k for (k, v) in ExifTags.TAGS.items() if v == 'Orientation'][0]
            exif = dict(image._getexif().items())
            # Here we map the rotation enum to a specific rotation we must apply
            rotations = {3: 180, 6: 270, 8: 90}
            if exif[orientation] in rotations:
                image = image.rotate(rotations[exif[orientation]], expand=True)
            # Save the image, it will be closed by context manager
            image.save("uploads/{0}.jpeg".format(int(time())))
    except Exception as e:
        print(e)

run(app=default_app(), host='0.0.0.0', port=8080)

Please also note that most image operations in PIL such as rotate() and ImageOps are immutable and return a new Image instance out the end of the call (i.e. new_image = image.operation() ).

Conclusion

It's not that painful to convert the image to a more compatible implementation of jpeg by hard-rotating it. This is fairly good practice to process images during upload anyway as you can run a Mime type check on it to make sure it's a JPEG ( or image) and not something like an executable or nasty file type.

As a bonus you can easily make thumbnails pre-processed during this step to save download time for clients using the following snippet after image.save(..)

# make thumb of size (150, 150):
thumb = ImageOps.fit(image, (150, 150), Image.ANTIALIAS)
with open('uploads/{0}_thumb.jpeg'.format(int(time())), 'wb') as thumb_file:
    thumb.save(thumb_file)