Showing posts with label html5. Show all posts
Showing posts with label html5. Show all posts

Wednesday, March 13, 2013

Take (and Manipulate!) a Photo with a Web Page

So not that long ago, if you wanted an app to take a photo, it had to be a native app -- such as a Windows/Mac app or a native mobile application.  But HTML5 has brought a number of new APIs that allow not only taking photos, but analyzing and manipulating them all within a browser.

Sadly, there is still inconsistent support for these features across browsers (both desktop and mobile).  So in practice, the best approach may be to target the APIs supported by iOS 6, which is more or less the minimal feature set across browsers that support any of this at all.

The three pieces we'll need are:
  • The Canvas
  • File I/O
  • Changes to the File Upload input and Media Capture
    • Note that Media Capture has been superceded by getUserMedia (more info) but we don't use that here because iOS doesn't support getUserMedia yet

The Canvas is a space on the page to draw into, using either 2-D or 3-D APIs.  Conveniently, it also lets you draw images into it, inspect and alter the pixel data, and export its content as an image. Canvas is supported by IE 9 and modern versions of Firefox, WebKit/Safari/iOS 6, Opera, and Chrome.  However, not all browsers support 3-D (via WebGL), and browsers don't generally support ALL of the Canvas spec (which has continued to evolve even after initial browser support).  Still, the parts we need are supported across browsers.

The additional "accept" and "capture" attributes on the file upload input element let us specify that the user should be able to select an image file, getting it from an attached camera if possible.  Crucially, on iOS you are given the option to select a photo from the photo library or take a photo on the spot (though some desktop browsers only let you browse for an image file, even if a Webcam is attached). Then the "files" property on the input element lets us access the selected files (unfortunately, not supported in IE < 10).

So the formula for taking and manipulating a photo is:
  1. Configure a file input using the new attributes
  2. When an image file is selected (or photo taken), access the image file
  3. Check the image EXIF data for rotation using File I/O
  4. Decide what size to scale the image to (if needed)
  5. Configure a Canvas for the selected image dimensions
  6. Draw the image to the Canvas, rotating and scaling as appropriate
  7. Access and manipulate the pixel data for the Canvas
  8. Write the pixel data back to the Canvas
  9. Export the image on the Canvas as an image file

Taking these one step at a time (and note that I use jQuery for convenience, but it is not needed for this to work):

1. Configure a file input using the new attributes

There are two ways to configure the input (and it makes no difference to desktop browsers). An input configured like this will prompt the user to take a photo or select a saved impage on either iOS or Android:
<input type="file" id="PhotoPicker"
       accept="image/*" />
With the additional capture="camera" attribute, Android goes directly to the camera without giving the option to use a saved photo (though the iOS behavior is the same):
<input type="file" id="PhotoPicker"
       accept="image/*" capture="camera" />
Note that if you don't like the default appearance, you can conceal the input widget and connect a button or link (styled any way you like) to activate it.  The input doesn't work if set to display: 'none', but we can put it inside a div of size 0 to effectively hide it:
<div style="width: 0; height: 0; overflow: hidden;">
    <input type="file" id="PhotoPicker" 
           accept="image/*" capture="camera" />
</div>
<button class="lovely" id="PhotoButton">1. Select Photo</button>
$('#PhotoButton').click(function() {
    $('#PhotoPicker').trigger('click');
    return false;
});

2. When an image file is selected (or photo taken), access the image file

We just use the new files property on the input:
$('#PhotoPicker').on('change', function(e) {
    e.preventDefault();
    if(this.files.length === 0) return;
    var imageFile = this.files[0];
    ...
});

3. Check the image EXIF data for rotation using File I/O:

All images may have a rotation recorded by the camera, such that no matter how the camera was oriented when the photo was taken, the image can be displayed appropriately on screen.  In particular, an iPad considers the landscape orientation to be normal (though it can still be upside-down), and the portrait orientation takes photos with a 90 degree rotation.  The EXIF header in an image records this rotation, so we can read it in order to display the image appropriately.

This part uses two open source (MPL) EXIF-reading scripts:
<script src="http://www.nihilogic.dk/labs/exif/exif.js"
       type="text/javascript"></script>
<script src="http://www.nihilogic.dk/labs/binaryajax/binaryajax.js"
       type="text/javascript"></script>
Then we use the new File API to read the image data (and there's a nice image here describing the EXIF Orientation codes):
var width;
var height;
var binaryReader = new FileReader();
binaryReader.onloadend=function(d) {
    var exif, transform = "none";
    exif=EXIF.readFromBinaryFile(createBinaryFile(d.target.result));

    if(exif.Orientation === 8) {
        width = img.height;
        height = img.width;
        transform = "left";
    } else if(exif.Orientation === 6) {
        width = img.height;
        height = img.width;
        transform = "right";
    }
    ...
};

binaryReader.readAsArrayBuffer(imageFile);

4. Decide what size to scale the image to (if needed)

If you want to limit the size of the image, you can scale the height and width accordingly. With mobile devices with high-resolution cameras, this may be necessary to limit memory usage:
var MAX_WIDTH = 1024;
var MAX_HEIGHT = 768;
if (width/MAX_WIDTH > height/MAX_HEIGHT) {
    if (width > MAX_WIDTH) {
        height *= MAX_WIDTH / width;
        width = MAX_WIDTH;
    }
} else {
    if (height > MAX_HEIGHT) {
        width *= MAX_HEIGHT / height;
        height = MAX_HEIGHT;
    }
}

5. Configure a Canvas for the selected image dimensions

If the canvas is on the page, you can just grab it:
var canvas = $('#PhotoEdit')[0];
Otherwise, you can use an offscreen canvas:
var canvas = document.createElement('canvas');
And then size it accordingly:
canvas.width = width;
canvas.height = height;

6. Draw the image to the Canvas, rotating and scaling as appropriate

In order to draw the image to a Canvas, first we have to load it in an <img />, and a convenient way is to create a URL to assign as the img.src from the selected file (using part of the File API):
var img = new Image();
var url = window.URL ? window.URL : window.webkitURL;
img.src = url.createObjectURL(imageFile);
img.onload = function(e) {
    url.revokeObjectURL(this.src);
    ...
};
Then inside the onload handler, we can draw the image to the canvas. (In practice, I put most of the logic we're discussing inside the onload handler.) A transformation matrix on the context handles rotating the image (if needed) and shifting it back into the viewable area.
var ctx = canvas.getContext("2d");
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, 700, 600);
if(transform === 'left') {
    ctx.setTransform(0, -1, 1, 0, 0, height);
    ctx.drawImage(img, 0, 0, height, width);
} else if(transform === 'right') {
    ctx.setTransform(0, 1, -1, 0, width, 0);
    ctx.drawImage(img, 0, 0, height, width);
} else if(transform === 'flip') {
    ctx.setTransform(1, 0, 0, -1, 0, height);
    ctx.drawImage(img, 0, 0, width, height);
} else {
    ctx.setTransform(1, 0, 0, 1, 0, 0);
    ctx.drawImage(img, 0, 0, width, height);
}
ctx.setTransform(1, 0, 0, 1, 0, 0);

7. Access and manipulate the pixel data for the Canvas

The getImageData method returns an array of pixel data, with one byte each for red, green, blue, and alpha. This example applies a "green screen" effect, setting pixels to transparent (alpha = 0) if they are mainly green.
var pixels = ctx.getImageData(0, 0, canvas.width, canvas.height);
var r, g, b, i;
for (var py = 0; py < pixels.height; py += 1) {
    for (var px = 0; px < pixels.width; px += 1) {
        i = (py*pixels.width + px)*4;
        r = pixels.data[i];
        g = pixels.data[i+1];
        b = pixels.data[i+2];
        if(g > 100 && g > r*1.35 && g > b*1.6) pixels.data[i+3] = 0;
    }
}
There are of course other possibilities for filtering the image.

8. Write the pixel data back to the Canvas

Then we use putImageData to write the array of pixel data back to the canvas.
ctx.putImageData(pixels, 0, 0);

9. Export the image on the Canvas as an image file

Finally, the whole content of the canvas can be exported as an image. There may be a way to do this through the browser (in Firefox, for instance, right-clicking on any Canvas lets you view it as an image), but there is a canvas API call as well. Every browser supports saving the image as a PNG, and some may support other formats:
var data = canvas.toDataURL('image/png');
The resulting data URL looks like this:
data:image/png;base64,...
Where the "..." is the base 64 encoded version of the binary image file. So you can, for instance, upload the data URL to a server as a form field, and the server can strip off the data:image/png;base64, prefix and base 64 decode the rest and save it to a file like mycanvas.png.

Summary

So there you have it -- a Web page that takes a photograph, scales and orients it as appropriate, processes the image as desired, and then displays it to the user and/or uploads it to a server.
Unfortunately, there is not yet a way to present a "Save As" dialog to the user to save the file directly to their local disk -- the FileSystem and FileWriter APIs are not widely supported across browsers. But you can of course upload the image to the server and then redirect the user to a server URL to save it...

Consolidated Sample Code

Here's a single page with a working example using the code we've gone through here.

You can also use this sample image to see the green screen replacement effect.

Platform Support

Bold entries work fully:

Desktop Browsers:
  • Firefox 18
  • Safari 6
  • Chrome 25
  • IE 10
  • IE 9 (does not support getting selected files from file input)

Mobile Browsers:
  • iOS 6
  • iOS 5 (file input doesn't work)
  • BlackBerry 10
  • Android 4.1 (built-in browser)
  • Android 4.1 with Chrome from Play store
  • Android 2 (native browser; does not take photo)
  • Windows Phone 8 (file input doesn't work)
  • Surface RT (doesn't work)

Friday, May 4, 2012

Camera Access from Android Browser

The browser in Android 4 is able to take pictures from a web page and display them without any plugins or server interaction. This demo uses the Device API and File API to take and display a picture on a phone using the browser.

Try it on your Android phone http://don.github.com/html-cam/.

I wrote this demo after reading David Calhoun’s post about Android implementing device APIs.

This works on my Google Nexus S running Ice Cream Sandwich (4.0.4). The Device API works on Samsung Galaxy Tab 10.1 with Honeycomb (3.2) but the File APIs do not. YMMV.

Loading images from the gallery is finicky. Images load from the device but fail to load from Picasa albums. I had to alter the image blob and change data:base64 to data:image/jpeg;base64 to get photos from galleries to display.

I kludge the photo orientation based on the device orientation, clearly there is a better solution.

It's possible to run the browser out of memory. I'm guessing this is because a huge photo is encoded into a base64 string. Maybe window.URL.createObjectURL(file) would work better than reading the image with FileReader?

Adjusting the input tag can control the input types:

<input type="file" />
Browser prompts for camera, camcorder, sound recorder, 
  music track, or gallery

<input type="file" accept="image/*" />          
Browser prompts for camera or gallery   

<input type="file" accept="image/*;capture=camera" />           
Browser goes directly to the camera

For more info see the github project.

UPDATE 2012-05-06
It looks like Facebook's mobile website allows camera access from a browser on Android.