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
Discussion
Kudos for implementing JPEG encoder in JS.
Btw, Opera supports Canvas.toDataURL( 'image/jpeg', quality ); where quality is a float in the range [ 0 ; 1 ]
This looks great! Its funny you just released this, because I just released Downloadify (downloadify.info) yesterday. Right now it just handles strings, but I was looking to add support for jpg creation etc. I am going to try to use your script with a newer build of mine and see how it works out. If it works, a in-browser graphics editor could theoretically be built without the need for a server.
Great work!
postMessage has been updated to support serialisation and transfer of a number of DOM objects – notably for your case the ImageData object – however this change was made sometime after WebKit's postMessage implementation was written, and as yet it has not been updated to this new behaviour. This should make life easier/faster in future.
Probably falling off trace in TraceMonkey. I'll put a bug on file and we'll look into it.
https://bugzilla.mozilla.org/show_bug.cgi?id=531185
The script doesn't actually work in Firefox, unless you have Firebug installed. You want to be using |if (!window.console)|, not just the |console| bareword; the bareword test is a syntax error in all browsers that don't already have a property of that name on the global object.
well, why there are no opera test ?
@ Mathieu: Opera may support this, but just using this is not so much fun
For a more practical approach one could create a wrapper around toDataUrl and use native jpeg generation where possible and the js based encoder as fallback.
@ Doug: I think you will have to change some things than, because you can't just let flash save the Base64 encoded file. You would have to pass the binary data or must decode the Base64 data in flash. Right now the easiest way to save an encoded image is to choose “save to downloads folder” from the context menu (but there is no way to choose a name for the file…).
@ Oliver: I know, Firefox serializes and deserializes Objects automatically and one can emulate this behavior in Webkit by using JSON.stringify() and parse(). The problem is, that this is rather slow and memory consuming: a singe RGB pixel would be serialized to ”[65,65,65]”. Using encoding as a String using charCode results in just “AAA”. I tried JSON and I remember the JSON serialization took more time than the actual encoding…
@ Boris: Thank you for reporting this error. I have corrected the examples and hope the work now in a non-Firebug enabled Firefox.
@ lol: Sadly the encoder does not work in Opera because I use window.btoa() to do the Base64 encoding and currently this method is not implanted in Opera. One could use a js Base64 encoder as fallback. I don't know how severe the loss of performance would be.
Very well done! I'm excited to see how this will be put to use as time goes on.
As a side note, the text on your blog is extremely difficult to read, I had to turn off styles just to be able to finish it.
I'm interested. I've just been debugging a problem with using GD2 in PHP to resize and compress a image, however for a server with memory restrictions and attempts with large images it fails. Where it appears that this solution succeeds. It makes me wonder how to save the JS compressed image on the server side - has any work been done down this avenue? If not I would definitely be interested in giving it a shot. How is the work licenced?
One more thing. The answer to your “Probably the Mozilla guys should consider adopting Nitro or V8?” question is that SpiderMonkey does some things that neither one of those does, and is a lot faster than them on some other testcases. The effort required to remedy these two differences is likely at least comparable to the effort needed to make SpiderMonkey faster on various other testcases…
no tests on Linux? shame. Neat project though.
this may be a dumb question but is it possible to use your jpeg encoder to create a jpeg from an image map? need to do this to make it possible to print an image map from ie7 which apparently isn't supported. Thanks!
Hey, just an FYI, I think you typo'd sad:
“It's das to see that web workers…”
Awesome project, btw.
Oh, and this captcha is horrid.
Speaking of getting data into/out of the workers – have you taken a look at the drag/drop functionality available in HTML5? You can basically take data directly off the filesystem via drag/drop and then possibly use this to compress and encode the file data before sending it over to the server via AJAX.
@Benjamin: You can solve the issue with the size of a JPEG by using imagemagick on the server. Side note: The size is actually related to the area of the photo (megapixels) and not the file size. This JPEG encoding library would actually do a fantastic job getting around the other limit – file size (since PHP has to allocate memory for file uploads as well as image resizes).
When I require the essay paper, I will ask <a href=“http://www.essayscentre.com”>custom essay writing</a> service to assist me. But you can write the bright data related to this good post by yourself. You have got master’ writing technique, I can tell you, I tell you.
Well, it's great, I'm going to take a look to the code … now, the next step is the decoder JPG …
Do not enough cash to buy a car? Worry not, just because that is real to take the credit loans to resolve all the problems. Thus get a consolidation loan to buy all you need.
What did you mean, Loan ?
Really, I didn't understand the last part, but I think that was sarcastic …
Looks like this comment thread is getting a nice liberal sprinkle of SPAM. A better CAPTCHA is in order. May I suggest reCAPTCHA or possibly text-based question/answer sort of thing?
Ohh, OK … that was SPAM, lol. Patrick the CAPTCHA would be great (text-based question/answer sort of thing) … I'm gonna check in google
However this change was made sometime after WebKit's postMessage implementation was written, and as yet it has not been updated to this new behaviour.
postMessage implementation was written, and as yet it has not been updated to this new behaviour.
It's nice that you are still growing and build the network so more people know what you are doing to help us with helpful information!Everyone can make use of this site! Thank you!