Emulating Lossy RGBA Images with HTML5's Canvas Element

eye catcher
Lossy RGB is smaller

Lossy RGBA images are something that always has been missing on the web (If you ignore Mozilla's brief support of JNG, that is). Nowadays, browsers typically support JPG, PNG, GIF, ICO, and BMP. There is obviously some feature overlap, but there isn't any good choice for true color images which also happen to use alpha.

Generally your choices boil down to JPG and PNG. If you ignore animated GIFs (which are pretty useless for Canvas usage), PNG can easily replace GIF, ICO, and of course BMP.

Today, I'll demonstrate how you can create RGBA images which use JPG for the RGB channels. Since this can decrease the file size up to 75%, it's a very easy and intriguing way to speed up the loading time of your game or application. (If you are currently using heavy PNG32 images, that is.)

PNG and JPG in a Nutshell

PNG's interesting modes are:

  • 8bit (indexed), which gives you up to 256 different colors. Interestingly, each color entry can also have an alpha value attached to it. Effectively it's like having an RGBA palette. Unfortunately it's poorly supported by most tools. You can, however, easily turn any 32bit PNG into a quantized 8bit PNG with tools like pngquant or pngnq.
  • 32bit (RGBA), which gives you four lossless 8bit channels. It's great for storing your finished artwork, but it also results in prohibitively huge files. You really don't want to waste 300kb for one slightly bigger image, ideally your whole application with all its assets should be smaller than that.

JPG's interesting modes are rather straightforward in comparison. There is:

  • 24bit true color for photos and the like, and
  • Gray-scale, which happens to be an interesting choice for storing an alpha channel separately.

renderToCanvas

All 3 methods below utilize my handy renderToCanvas utility function. For convenience the code is replicated here:

var renderToCanvas = function (width, height, renderFunction) {
    var buffer = document.createElement('canvas');
    buffer.width = width;
    buffer.height = height;
    renderFunction(buffer.getContext('2d'));
    return buffer;
};

True Color JPG + PNG8: toRgbaFromInverseAlphaMask

This function takes one RGB image (e.g. a JPEG) and another one which contains an inverse alpha mask (PNG8). It combines both using the Porter-Duff xor rule. The result is an RGBA image.

rgb + inverse alpha mask = rgba
Figure 1: RGB + inverse alpha mask = RGBA
var toRgbaFromInverseAlphaMask = function (rgbImage, inverseAlphaMask) {
    var width = rgbImage.width, height = rgbImage.height;
    return renderToCanvas(width, height, function (ctx) {
        ctx.drawImage(rgbImage, 0, 0);
        ctx.globalCompositeOperation = 'xor';
        ctx.drawImage(inverseAlphaMask, 0, 0);
    });
};

Thanks to this function you can now use that one supported lossy image format and still get your alpha channel. This can help you to reduce the download size drastically. The downside is that there is another file to download, but this problem can be also solved by using a single blob file for all resources (I'll cover this topic in the future).

Demo: toRgbaFromInverseAlphaMask.html

True Color JPG + Gray-Scale JPG: toRgbaFromAlphaChannel

Another similar function I've written uses a gray-scale JPEG for the alpha channel. This can help reducing the file size further if the alpha channel is somewhat "gradienty" in nature. That is, it doesn't look like a vector clipping path. Rendered flames or explosions are a good example for cases where this makes a lot of sense.

rgb + alpha = rgba
Figure 2: RGB + alpha = RGBA
var toRgbaFromAlphaChannel = function (rgbImage, alphaChannelImage) {
    var width = rgbImage.width, height = rgbImage.height;
    return renderToCanvas(width, height, function (ctx) {
        var alpha = renderToCanvas(width, height, function (ctx) {
            var id, data, i;
            ctx.drawImage(alphaChannelImage, 0, 0);
            id = ctx.getImageData(0, 0, width, height);
            data = id.data;
            for (i = data.length - 1; i > 0; i -= 4) {
                data[i] = 255 - data[i - 3];
            }
            ctx.clearRect(0, 0, width, height);
            ctx.putImageData(id, 0, 0);
        });
        ctx.drawImage(rgbImage, 0, 0);
        ctx.globalCompositeOperation = 'xor';
        ctx.drawImage(alpha, 0, 0);
    });
};

This one even uses renderToCanvas twice. Once for creating an inverse alpha mask from the gray-scale JPEG and the other one for combining those two things.

I could have read the pixel data of both images to create an RGBA image right away, but since reading and iterating over all those pixels is a tad slow, I decided to do only half of the work in JavaScript and let the native code take care of the other half. toRgbaFromInverseAlphaMask could have been reused here, but since you'll probably only use one of those functions I decided against that. Well, that function is very short either way. There isn't much to gain there.

Pitfall

Note: This one won't work locally (i.e. from file://) since it uses getImageData, which requires read access. For security reasons reading pixel data is restricted. You can only read pixels from images which came from the same origin (domain) as your script.

If you run it locally, Firefox will show an error message like the following one:

Error: uncaught exception: [Exception... "Security error"  code: "1000" nsresult: "0x805303e8 (NS_ERROR_DOM_SECURITY_ERR)"  location: "file:///X:/example.js Line: XX"]

Chrome's error message is basically the same:

Uncaught Error: SECURITY_ERR: DOM Exception 18

Opera allows it for some reason. Well, strictly speaking the origin is the same; the script and the image came from the local file system. So, technically Opera behaves correctly, I guess.

Either way, I highly recommend to run a local server for testing. You'll need one at some point. Be it for read access to pixel data, auto generating blob files, or for passing data or messages around.

Demo: toRgbaFromAlphaChannel.html

True Color JPG + SVG Clipping Path Data: toRgbaFromSvgClippingPathData

This one takes one RGB image and a clipping path which uses SVG's path data attribute format. It doesn't cover the whole specs, just enough to make it work with the kind of path data Inkscape generates.

rgb + clip path = rgba
Figure 3: RGB + clip path = RGBA
var toRgbaFromSvgClippingPathData = function (rgbImage, svgClippingPathData) {
    var width = rgbImage.width, height = rgbImage.height;
    return renderToCanvas(width, height, function (ctx) {
        var p, i, len;
        p = svgClippingPathData.split(/,| /);
        ctx.beginPath();
        for (i = 0, len = p.length; i < len; i++) {
            switch (p[i]) {
            case 'M':
                ctx.moveTo(p[i + 1], p[i + 2]);
                i += 2;
                break;
            case 'L':
                ctx.lineTo(p[i + 1], p[i + 2]);
                i += 2;
                break;
            case 'C':
                ctx.bezierCurveTo(p[i + 1], p[i + 2], p[i + 3], p[i + 4], p[i + 5], p[i + 6]);
                i += 6;
                break;
            case 'z':
            case 'Z':
                ctx.closePath();
                break;
            default:
                if (window.console) {
                    window.console.log(p[i]);
                }
                break;
            }
        }
        ctx.clip();
        ctx.drawImage(rgbImage, 0, 0);
    });
};

Demo: toRgbaFromSvgClippingPathData.html

Example Code

Download: rgba-examples.zip (122kb – zero-clause BSD)

Comments

JPEG2000

Safari supports JPEG2k, which is lossy and has alpha.

re: JPEG2000

Yea, it does support alpha. Same goes for JPEG XR (aka Windows Media Photo or HD Photo). But it will take yet another decade until all patents (and submarine patents) have expired.

I'd rather see a comeback of JNG. (I'm actually writing a short article about that right now.)

APNG

For animation with full color and alphachannels, you might want to take a look at APNG. Supported in Firefox and Opera. I suspect webkit supports it as well. However, it'll probably take until IE 12.0 until it's implemented there.

re: APNG

Yes, I know about APNG.

It's not supported by Webkit.

It's also not lossy (i.e. it's really huge) and for Canvas it's also completely useless.

JNG would be great though.

Post new comment

  • Web page addresses and e-mail addresses turn into links automatically.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.

More information about formatting options