Sunday, August 22nd, 2010

Want to pack JS and CSS really well? Convert it to a PNG and unpack it via Canvas

Category: Canvas, Performance

<p>Jacob Seidelin of nihilogic fame (remember his Super Mario in JavaScript solution) is one of my unsung heroes of JavaScript. His solutions have that Dean Edwards “genius bordering on the bat-sh*t-crazy” touch that make you shake your head in disbelief when they come out but later on become very interesting.

One of his posts from 2008 entitled “Compression using Canvas and PNG-embedded data” had a good idea: if you want to compress JavaScript and CSS you could reverse engineer a packing algorithm in JavaScript or you could use a lossless packing system that is already in use and supported in browsers. In this case the packed format is PNG and the way to unpack it is by using the canvas API’s getImageData() method:

javascript
< view plain text >
  1. var x = function(z, m, ix ) { // image, callback, chunk index
  2.   var o = new Image();
  3.   o.onload = function() {
  4.     var s = "",
  5.         c = d.createElement("canvas"),
  6.         t = c.getContext("2d"),
  7.         w = o.width,
  8.         h = o.height;
  9.     c.width = c.style.width = w;
  10.     c.height = c.style.height = h;
  11.     t.drawImage(o, 0, 0);
  12.     var b = t.getImageData( 0, 0, w, h ).data; //b : bucket of data
  13.     for(var i= 0; i < b.length; i += 4) {
  14.       if( b[i] > 0 )
  15.         s += String.fromCharCode(b[i]);
  16.     }
  17.     m(s, ix);
  18.   }
  19.   o.src = z;
  20. }

As there are quite some interesting competitions going on that need really small JavaScript solutions Alex Le took up Jacob’s work and wrapped it in a build script that concatenates, packs and converts to a PNG and unpacks it for the 10K competition with a JavaScript. In the process Alex also found some bug in Internet Explorer 9′s canvas implementation as it only reads the first 8192 bytes of a PNG and returns 0 for the others :(.

It is pretty amazing how efficient this way of packing is. What we need to test now is when and if it is worth while to have the unpacking done on the client. Imagine adding your JS and CSS to the end of an image and cropping it in CSS to have all the info you need in an app in a single HTTP request. Let the games begin.

Posted by Chris Heilmann at 2:30 pm
19 Comments

+++--
3.8 rating from 4 votes

19 Comments »

Comments feed TrackBack URI

Been there, done that, unfortunately didn’t sell the movie rights. The surprising thing is that for binary data, this is actually a whole lot faster than getting it via XMLHttpRequest and x-user-charset (which requires you to do a &0xff on each char). For text data there is little to gain outside competitions like js1k since anywhere else you’d just enable the gzip compression and be done with it, but for binary data it pretty much saves the day.

Comment by hansschmucker — August 22, 2010

BTW, I tried to submit something very similar to js1k, however it didn’t make it through since it contained characters not valid in UTF-8 (and sadly js1k’s submission process is text-based so your submission can’t contain such characters). Still, for people wanting to employ such a trick it is useful: Normally if you want to include binary data in a HTML file you have to encode it as base64 (which pretty much eats up all gains of the PNG compression). However there’s one part that in all browsers I’ve tried would not cause any trouble: arbitrary binary data within JS comment blocks (except the star-slash sequence of course… if your binary code has that than you’re screwed). So you can get the current document (which should not actually generate a request since it’s already cached) with x-user-charset and xmlhttprequest, delete everything outside the slash-star star-slash sequence, use btoa to make it into a base64 url, create a new image with that and finally use the aforementioned technique to extract it. While it adds more overhead (and honestly, too much to be any use in js1k) it can generate a nice gain for bigger files… well, in theory. As I said before you’d usually use GZIP.

Comment by hansschmucker — August 22, 2010

One last note, then I’ll shut up: You don’t actually need any fancy server to encode this: Just use putImageData, then toDataURL and save that. If you want to can run PNGCrush on the result.

Comment by hansschmucker — August 22, 2010

Per one of the comments of post, I went back and found a work-around for IE9

http://alexle.net/archives/306#update1

Essentially the image is rendered multiple times, and each time its top value is shifted by 8192 pixels each so that the getImageData() method can read the next 8192 bytes of the remaining data.

The updated code is

http://gist.github.com/544352

Cheers!

Comment by AlexLe — August 22, 2010

@hans: actually the only scenario which would be worthwhile for this trick is when pairing it with PNGCrush/PNGout/OptiPNG. Else the gain over standard gzipping would be dismal compared to the extra work you have to do to get this running.
.
Also considering the overhead for the CPU, this is probably a good scenario to use workers (albeit with the current API passing image data to a worker thread is very expensive and might undermine any benefit of using a worker)

Comment by gonchuki — August 22, 2010

I’m really surprised that no one researched this before publishing/promoting this as an awesome idea. This a stupid idea that gains nothing and actually hurts you.

Why? PNG images are compressed using DEFLATE. HTTP compression uses DEFLATE. It is nearly impossible to encode text into a PNG image and have it be smaller than regular HTTP compression.

The only way a PNG using DEFLATE will be smaller than HTTP compression using DEFLATE is if the PNG was produced using a tool with a custom implementation of DEFLATE instead of zlib. Theses from-scratch re-writes of zlib use larger sliding windows during the compression phase to find more redundancy and produce smaller PNG. The only tools that do this are PNGOut (Ken Silverman rewrote DEFLATE) and AdvanceCOMP, which is simply using the DEFLATE compressor from the 7Zip project, which also re-wrote their own implementation of DEFLATE. (Read more here: http://zoompf.com/blog/2010/01/top-png-optimizers-dont-use-zlib)

Even on the extreme off chance that the DEFLATE datastream inside the PNG might be made smaller than HTTP compression, you still have all the other PNG chunks that are required to make a valid PNG image (and thus an image that can be blitted into a CANVAS and retrieved). Not to mention the dependence on JavaScript, and how this would mangle the download pipeline for the browser.

In short, no one should be doing this.

Comment by BillyHoffman — August 22, 2010

@gonchuki , @BillyHoffman
GZIP==Deflate==Http Compression. Like I said there’s no point if that’s available, other than transporting binary data without x-user-charset. Even with PNGCrush there’s no reason to use this over GZIP since PNGCrush is just a very slow and thorough Deflate compressor.

Comment by hansschmucker — August 22, 2010

It’s an interesting hack of course, kudos for that, but shouldn’t be taken too seriously, not just because, as others pointed out the benefits are dubious at best, but more seriously because it hurts a certain idea of the open web as it makes view source a lot more complicated. It’s a form of obfuscation.

Comment by BertrandLeRoy — August 22, 2010

@hans

Agreed. What we should be asking is, why doesn’t mod_deflate use a an implement of DEFLATE other than what comes with zlib? PNGOut is not open source, put 7Zip’s implementation of DEFLATE is.

Comment by BillyHoffman — August 22, 2010

I used a technique like this for my 10k apart submission, except I moved all my html, css and js into one image.

src = http://github.com/jimbojw/10k

Comment by jimbojw — August 22, 2010

A correctly configured server will not compress jpg, png images etc. And I would far rather use native gzip/compress in the browser.

I believe however that the real gains are in combining multiple requests into a single connection since http performance improvements start with reducing the number of connections. If there was a way of delimiting multiple files, creating a hash, delivering that as a png, and then writing the document to the browser (perhaps using localstorage as a caching mechanism) then I believe we would have a winner.

Comment by JamesWestgate — August 23, 2010

interesting topic. did it ages ago in python… but it’s not very useful in application.

Comment by DoubleAW — August 23, 2010

IIRC Tobias Schneider mentioned at JSConf he’s using this in Gordon to expand SWF files.

Comment by Michael Mahemoff — August 23, 2010

@hans, @billy:
you are obviously forgetting about PNG scanline filtering http://www.w3.org/TR/PNG-Filters.html
That’s why I mentioned those PNG recompressors, as they apply those filters via brute force to find the combination that produces the smallest file size possible. A clever pre-processor could also use a variable width image to verify which width benefits more from those filters and export just that one.
Heck, maybe this topic really needs more research just for the sake of testing at which point the cost/benefit ratio becomes something usable in the real world.

Comment by gonchuki — August 23, 2010

This is what I needed when I wrote my own 10k submission. Instead, I used another technique: When I looked at the minified source code (~13KB) produced by Google Closure Compiler I noticed that some terms (e.g. moveTo, addEventListener, …) are used multiple times in the file. Thus I wrote a script that takes a file as input and identifies terms that are used often. The occurences of a term number n are then replaced with the unicode character number 256 + n and the full term is added to the top of the file with the unicode character number 127 as a delimiter. Using this method, I was able to reduce the code size to ~9KB. I had to place the code inside an html comment because IE9 complained about special chars in javascript code. The decompressing algorithm takes about 200 Bytes.

Comment by timbaumann — August 23, 2010

@gonchuki I honestly have to admit that I did. I still don’t think it will usually make a difference big enough to outweigh the size of the PNG header but who knows. Thanks for the info!

Comment by hansschmucker — August 23, 2010

Cal Henderson did some research into “embedding compressed CSS & JavaScript in PNGs” and posted detailed results:

http://www.iamcal.com/png-store/

He concluded “GZipping your source files will get you a larger improvement and greatly simplify your production build process” but it could be useful in situations where gzip compression is not available, such as free hosting providers.

Comment by davidlantner — August 25, 2010

Wait! There is a different use for this hack. Cross domain XHR!!. img src can point to different domain and this article proves we can successfully use PNG to transport data. All you need is one little server that takes an url and translates the content to PNG. Did anyone try that ?

Comment by rollingtweets — September 23, 2010

rollingtweets, I don’t see any advantages with this technique over JSONP for that purpose. You still can’t POST data with either method.

To me, the only use for this could be to take obfuscation to a whole new level. Sure it’s possible to decode the JS & beautify it, but it’s 1 extra hurdle for snoopers to jump though, which may be useful to some people.

Neat hack though.

Comment by DGathright — October 6, 2010

Leave a comment

You must be logged in to post a comment.