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 4Chrome 4Firefox 3.5Firefox 3.6b2Flash (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 4Chrome 4Firefox 3.5Firefox 3.6b2
8455 ms crash 65426 ms 33333 ms

Basic Blocking Encoder, PC Core 2 Duo 3Ghz, Windows 7

Safari 4Chrome 4Firefox 3.5Firefox 3.6b2Flash (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 4Chrome 4Firefox 3.5Firefox 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

A better include function for JavaScript

The common way of including a JavaScript file in a web application is to use the <script>-tag, but what if you want to load a script from another script? There are frameworks and some smaller simple include functions, which essentially create a <script>-tag dynamically. But there is a problem with all the functions I know: If you include multiple scripts, you can't be sure if they are loaded in the order you included them. In fact you can be almost sure, that this won't happen. So if the second included script relies on functions in the first included script you app will most likely break. This was the reason I created a new and better include function which solves several problems of existing js include solutions.

Features

  • includes will be loaded in the order they are mentioned in the script
  • there is a timeout: if a script does not load, it will be skipped
  • a callback function, which is called when the script has loaded, can be defined
  • if you try to include a script a second time and specify a callback function, only the callback function will be executed
  • especially for Titanium apps it is possible to include not only .js but also .py (Python) and .rb (Ruby) scripts
  • works with standard compliant browsers (Webkit-based, Gecko-based, Opera)

The Code

JavaScript Include Function

var include = (function(){
    var scripts = [];
    var curr = 0;
    var currScriptElem = null;
    var loadTimeout = null;
    var busy = false;
    var inited = false;
    var head = null;
 
    window.addEventListener('load',init,false);
 
    function init(){
        head = document.getElementsByTagName('head')[0];
        inited = true;
        next();
    };
 
    function next(e){
        busy = true;
        if(e){
            window.clearTimeout(loadTimeout);
            e.target.removeEventListener('load',next);
            var callbacks = scripts[curr-1].callbacks;
            scripts[curr-1].loaded = true;
 
            if(callbacks.length > 0){
                var i = callbacks.length;
                while(i--){
                    callbacks[i]();
                }
            }
        }
 
        if(scripts.length > curr){
            var currentScript = scripts[curr].path;
            curr++;
 
            var script = document.createElement('script');
            var suffix = currentScript.substring(currentScript.lastIndexOf('.')+1);
            var type;
            switch(suffix){
                case 'js': type ='text/javascript';break;
                case 'rb': type ='text/ruby';break;
                case 'py': type ='text/python';break;
                case 'php': type ='text/php';break;
                default: type = 'text/javascript';
            }
 
             script.setAttribute('type', type);
             script.setAttribute('src', currentScript);
            script.addEventListener('load',next,false);
 
            currScriptElem = script;
            loadTimeout = window.setTimeout(skip,10000);
            head.appendChild(script);
 
        } else {
            busy = false;
        }
    };
 
    function skip(){
        currScriptElem.removeEventListener('load',next);
        head.removeChild(currScriptElem);
        next();
    };
 
    function include(scriptPath,optCallback){
        var dup = false;
        var i = scripts.length;
        while (i--) {
            if(scripts[i].path == scriptPath){
                dup = true;
                if (optCallback) {
                    if(scripts[i].loaded){
                        optCallback();
                    } else {
                        scripts[i].callbacks.unshift(optCallback);
                    }
                }
                break;
            }
        }
 
        if(!dup){
            var cbs = [];
            if(optCallback) cbs.unshift(optCallback);
            scripts.push({path:scriptPath,callbacks:cbs,loaded:false});
        }
 
        if(!busy && inited) next();
    };
    return include
})();

Usage

The include function is very easy to use. The callback function is optional.

Usage Example

// simple
include('includetest.js');
 
// with callback
include('includetest2.js',function(){console.log('include 2 callback');});

Please comment, if you have ideas of how to improve the function.

NuShell wins Titanium Application Contest

After two weeks of coding (even and especially during the Easter holidays) NuShell has won the first Titanium Application Contest with the highest rating of all submitted apps. I'm really happy that so many people liked my application and voted for it. So, a big thank you to all voters!

But NuShell won't stop there. I plan to continue development on NuShell, but at a little slower pace. Right now I am coding on a small utility app called BorderImageHelper (download link), which helps developers to create the css needed for using the border-image css property.

There is also a lot of work to do to bring webkitty.org to life. So, I will be very busy these days. Back to NuShell: I expect to release the next version on Thursday the 23th of April. You can expect an enhanced calc command and some general usability enhancements.

webkitty.org is online but is still missing content

The webkit reference project webkitty.org is online now, but there is still not much content on it. I spend all my time in the last weeks developing NuShell for the Titanium App Contest. I hope I can post some content within next week and the site can finally go public.

NuShell submitted to Titanium App Contest

On April the 13th I submitted my first Titanium app called NuShell to the Appcelerator Titanium App Contest. You can find out more about the App on this page: NuShell