A JPEG Encoder for JavaScript
Why
It might seem a crazy idea to implement a jpeg encoder in JavaScript and in fact, it is! I did it because of another little project of mine, which is about a way to support alpha-transparency in jpeg images (using basically two images: a normal jpeg and a greyscale mask). I wanted to code a tool to create these images with Appcelerator Titanium, but I had a problem: You can create a jpeg with the getImageData canvas method, but currently the quality option is not working. This was very annoying, because the tool was all about optimized the file size. So the tool was useless without proper jpeg encoding. I searched for jpeg encoders for Python, Ruby or PHP, but there weren't any! They all rely on external platform specific libraries. Then I remembered that there was an ActionScript 3 based jpeg encoder floating around and I decided to try to port it to JavaScript.
How
I downloaded the sources for the AS3 jpeg encoder ( AS3 corelib) and analyzed the code. I'm not an expert in writing encoders, actually I think it would have taken me month to start from scratch. The ActionScript code seemed tidy and my first step was to replace things not possible in javascript from the code. Then I wrote a little converter function to transform the 1-dimensional canvas image data array into a 2-dimensional array, which the original encoder expects. To cut it short: In about 2 hours I had the first working version!
It was surprising that it was that easy to get the first js-encoded jpeg displayed in the browser. Of course I didn't want to stop there. I wanted to optimize things as much as I could to make the encoder fast. This took me several days. I found optimized encoder versions for flash and haxe floating around the net (Faster JPEG Encoding with Flash Player 10) and tried the optimizations used there in my javascript version. As you can seen in the benchmarks below I was quite successful.
Another idea was to use the new web workers to do the heavy lifting in an separate thread, not blocking the gui. This is something flash can't do. So I created a version using a web worker for the encoding. The main problem (and in my opinion a major design flaw) with web workers it that you can't pass large amounts of data from and to the worker. You can only pass simple strings! It was not easy to pass the image data array of a 2880x2880px image into the worker. I tried:
- Using JSON.encode() → much to slow!
- Using Array.join and String.split() → faster, but still very slow
- Encoding the data as a String, using a String as byte array (transform the integers from 0-255 to their corresponding char-code) → still annoying, but fast enough
As you can see from the benchmarks below, the threaded version takes about twice the time the non-theaded version uses. This is mostly due to the overhead created by the Array→String→Array→String conversion necessary. But at least the GUI is only blocked for a short time.
It's sad to see that web workers are so crippled by design. How are they supposed to do heavy lifting in the background, when there is no effective way to pass data in and out?
Usage
Basic Encoder
Create a new encoder object
var myEncoder = new JPEGEncoder([quality])
The “quality” parameter is an optional integer between 1 (low quality) and 100 (high quality). If omitted a default value of 50 is used.
Encode an image
var JPEGImage = myEncoder.encode(CanvasPixelArray,[quality])
The encoder expects a “CanvasPixelArray” (as returned by the “getImageData” method of a canvas). The jpeg-encoded image is returned as a data-uri and can be used as src-attribute of an image-tag.
If no “quality” value is provided the last assigned quality value is used.
Threaded Encoder
Create a new encoder object
var myThreadedEncoder = new JPEGEncoderThreaded([quality])
The “quality” parameter is an optional integer between 1 (low quality) and 100 (high quality). If omitted a default value of 50 is used.
Encode an image
var myPrepardedImage = myThreadedEncoder.encode(CanvasPixelArray,[quality],callback-function,cache-boolean)
The encoder expects a “CanvasPixelArray” (as returned by the “getImageData” method of a canvas). When the encoder has finished is will can the provided callback-function with the data-uri of the image as the only parameter.
If you passed “true” for the cache parameter the encode method return a so called “preparedImageObject”.
Using a "preparedImageObject"
The idea is, that the image data is cached in the web worker. If you need to encode an image in different quality settings this speeds up the encoding process because the image won't have to be encoded as a String again and again.
myPrepardedImage.encode([quality])
The quality parameter is optional, if omitted the last used quality value will be used. When the image has finished encoding the provided callback-function will be called, just like when you encode an image directly.
Preparing an image without encoding it
In case you know that you will have to encode an image at some time, you can prepare and cache the image data without actually encoding the image. This decouples the time needed to transfer the image data into the worker thread from the actual encoding task.
var myPrepardedImage = myThreadedEncoder.prepareImage(CanvasPixelArray,[quality],callback-function)
Now you can call the encode-method just like in the example above to encode the image.
Examples
Here are two examples of the encoder. One is using the single-threaded version and the other uses a web-worker for the encoding.
Benchmarks
Encoding a 2880x2880px jpeg image. I chose this size, because this is the maximum size the Flash jpeg encoder handles.
Basic Blocking Encoder, MBP 2,33Ghz, OS X 10.6.2
| Safari 4 | Chrome 4 | Firefox 3.5 | Firefox 3.6b2 | Flash (corelib) | Flash (optimized) |
|---|---|---|---|---|---|
| 4141 ms | 5648 ms | 53184 ms | 40199 ms | 15141 ms | 3358 ms |
Threaded Encoder, MBP 2,33Ghz, OS X 10.6.2
| Safari 4 | Chrome 4 | Firefox 3.5 | Firefox 3.6b2 |
|---|---|---|---|
| 8455 ms | crash | 65426 ms | 33333 ms |
Basic Blocking Encoder, PC Core 2 Duo 3Ghz, Windows 7
| Safari 4 | Chrome 4 | Firefox 3.5 | Firefox 3.6b2 | Flash (corelib) | Flash (optimized) |
|---|---|---|---|---|---|
| 4619 ms | 4192 ms | 45503 ms | 28040 ms | 10230 ms | 2753 ms |
Threaded Encoder, PC Core 2 Duo 3Ghz, Windows 7
| Safari 4 | Chrome 4 | Firefox 3.5 | Firefox 3.6b2 |
|---|---|---|---|
| 7969 ms | crash | 49581 ms | 35952 ms |
Notes
- Safari 4 is slower on the 3Ghz Windows PC than on the 2,33 MBP. This is probably related to the 64-bit nature of the Mac version. The difference was even bigger when using Safari 4.0.3.
- Google Chrome crashed with the threaded version of the encoder, but this seems to be related to the large size of the image. Smaller images will work in Chrome with the threaded version. Also note: Not Chrome itself crashed, only the tab did
Analysis
I think the results show that JavaScript is quite fast (at least in Safari and Chrome). A little over 4 seconds for the non-threaded version is a very goof result, when compared to the 3,3 seconds the optimized flash jpeg encoder takes. Please note, that JavaScript has no static types, no byte array, no Vector-class and is not pre-compiled. Taking these facts into account Nitro and V8 are faster than the ActionScript 3 VM.
Comparing the different browsers Nitro and V8 are a magnitude faster than TraceMonkey. Firefox 3.6b2 shows some improvements, but it's still a long way. Probably the Mozilla guys should consider adopting Nitro or V8?
Conclusion
When the quality parameter of the toDataUrl method finally works my javascript implementation will be obsolete, but till that time it is a usable way to encode a jpeg image in the browser.
I think my encoder shows that javascript is capable of doing some heavy processing in modern JIT implementations. Web workers are a great idea, I just wished there was a better way to get data into and out of the worker.
Download
You can download the JavaScript sources here: javascript_jpeg_encoder_v09.zip