Put your message here! Contact me for more information
 
 








 

I submitted my application entry, GithubFinder, to the 10K Apart Contest last night. GithubFinder is basically a Mac-like Finder app to explore a Github repository. A lot of the time when I’m visiting Github, I wanted to quickly browse a repository, but there isn’t a quick way to do so. The 10K Apart contest gave me a reason to build something cool that I also could use. The rules to the contest are: the app needs to be less than 10K in total and it has to be 100% client-side and 100% compatible with IE9, Firefox, and Chrome/Safari. That means everything, markup, images, stylesheets, and CSS, need to be less than 10 KB (10,240 bytes) and there won’t be any server-side processing for the app to work. Packing a high-impact application into 10,240 is surprisingly not simple, I’ve learned quite a few things working on GithubFinder.

I wanted to share a technique that was invented by NIHILOGIC. The idea is to store the javascript code inside a PNG file to leverage the image compression and significantly reduce the size. And this technique can also be applied to CSS as well. I hope that my implementation would benefit someone before the 10K contest end.

From the early stage of the app, I know that I’d want to go over the 10K limit (well, limits are there to be pushed, and rules are set to be broken!) I really wanted GithubFinder to be useful, even in a minimal form. Moreover, I’m not the briefest coder in the world, so I decided to cheat the 10K rule by using Nihilogic’s inventon. I had read his article a while back, and I was amazed by his 8KB Mario implementation. Since IE9 supports Canvas, now is the time to whip out this trick. However, coming back to the article, the script to encode the JavaScript was no longer available. I ended up spending a few hours to re-created the javascript-to-png script again in Ruby, and a long the way, wrote an automate build script for the application.

If there’s anything in this project I’m really proud of, probably it’s my build script. The build process for the application works like this: all JS and CSS files are merged and minified using YUICompressor, then the minified JS and CSS are concatenated into one string, separated by a unique delimiter. This long string is then encoded into a PNG image. If there is any error in the javascript, YUICompressor would prompt and I’d jump to the error line, fix, then re-run the build to verify. Here’s the first part of the build-script containing the utility methods to handle all the merging, compressing, and generating image.

The merging and minification are both done inline to not litter the application with temporary files. Using Open3, I was able to just use yuicompressor directly (line 19). The interesting bit is the string2png() and its sibling, and the string2pngs() method. Here’s the string2png() method again:

string2png would read the input string character by character and convert this character to its ASCII equivalent, then create a new pixel with the red color of the ASCII value (line4 and 5). Afterward, we compress the image using the ZipCompression and write it out as an 8-bit PNG file. If you open up this file in Photoshop, you’ll see a long vertical stripe of different shade of red. One gotcha for this technique is that you won’t be able to use UTF-8 characters since we don’t have ASCII equivalents for them.

On the front-end, I created a “bootstrap.js” script similar to Nihilogic’s one to read back in the image file. Essentially the bootstrap.js script would load up the image, paint it on the canvas, then read back the pixel one-by-one and grab the red color out. Since the characters are stored in binary format and compressed, the code can greatly be minified.

The image produced by string2png works well in Chrome and Firefox, however, with IE9, there is a bug preventing the image to be read back properly. IE9’s getImageData() could only read the first 8192 bytes of the canvas pixel data, everything after the 8192th pixel are zero’ed out (you can see my test cases here — IE9 would consistently fail). (SEE UPDATE 1 below for the fix) The work around is to split the image into smaller ones, each has a maximum of 8192 pixels. The bootstrapper’s job now is to read these image files sequentially back in before reconstructing the js/css string. Hence we have string2pngs method:

The down side of this IE9 work-around is that the compression efficiency is reduced, since intead of having just 1 image, we’d have 2 or 3 images, and they all have to store their own color palette separately as apposed to sharing a single one. I estimate that I lost about 20% of storage space(1 single image: ~9.5KB, 3 equivalent images: ~12KB) due to the the splits. I ended removing features so that I could fit everything into the 10K limit.

At any rate, here’s the bootstrap.js script to handle the decoding of the image:

The function of interests here is x(). The whole decoding businesss are done from line 26 to 29, then the decoded string are passed back to the callback. Please bear with me the short variables, I was trying to minify this script. The run() call back would check for the chunks, and only proceed when it receives all the chunks. My earlier naive implementation of run() had a bug causing by a race condition which the images were re-assembled in different orders, thus sometimes the app initialized properly, and some other times it would fail.

Together with the build script, I had a quite flexible mechanism to tweak and play with the output files to get to the targeted size. Click here to see my full build script. I hope this help people participating in the 10K contest to create more wonderful applications.

Finally, my verdict on IE9 Platform Preview release? Unimpressive and buggy. I was able to crash it reliably (!). Microsoft should have waited a few more weeks to put in more features before rushing to release a piece of inferior software. Even worse, they didn’t make developer’s job to try IE9 out any easier by not including an address bar. What kind of idiotic thinking is this? And when all I wanted to report a bug, they force me to sign up for a Live account before doing that. Well, forget it, too much hassle.


UPDATE 1:

Per Dennis’s comment:

“Have you tried using one image, and then copying the image data beyond the first 8192 bytes mark back to 0,0 for IE9?”

I went back and tweaked the x() function, which handles the extraction of the image data, and got it to work in IE9. This means that IE9 will be able to extract data no matter how big the image is. The trick is to shift the image as it gets rendered on to the canvas by 8192 pixels each time. Essentially we render the image in “chunks” of 8192 pixels. So instead of splitting into multiple images using the build script, we can just use 1 image and let Canvas do the chunking for us. This is made possible because the encoded image is only 1px wide, hence we only need to shift the top position of the image and run getImageData() from 0 to the smaller value between chunkSize and the remaining height of the image. Here’s the updated function:

I wish I had thought of this work-around before hand, I could pack even more features into the 10K app.


Tags: , , , , , ,

 

27 Responses to “10K Apart Contest: Cheating by Compressing Your JavaScript and CSS to PNG Images



1:00 pm
August 21, 2010
#302375

Righteous hack, sir.
Did you report the IE9 bug on their tracker?

Also you should really drop the linkwords script. It really fucking blows. :)




1:15 pm
August 21, 2010
#302380

I haven’t reported the getImageData() bug yet, and I ran into another bug with the css stuff as well (appending CSS inline would not render background images, unless page re-render is forced).

And per your request, LinksWorth’s text-ads is now gone :)




Frak
1:40 pm
August 21, 2010
#302381

IE9: Does this surprise us? Nay. Microsoft consistently offers inferior browsers. They scream, “More features!” We cry, “Web standards and stability!” They don’t listen to the folks. Screw ‘em.




5:30 pm
August 21, 2010
#302410

Great, great work. I love it when contests force us to think in “smarter” ways like this. The coolest techniques always turn out to be the ones that seem so simple in retrospect. Bad ass.




6:02 pm
August 21, 2010
#302413

Rad! What’s the compression ration you gained when you compare the YUICompressor’s output and bootstrap+png images?




7:12 pm
August 21, 2010
#302418

I re-ran the build and got this report:

ie9/index.html 214
ie9/img/dir.gif 101
ie9/img/la.gif 64
ie9/img/la_h.gif 53
ie9/img/txt.gif 94
ie9/js/bootstrap.js 827
ie9/js/c0.png 4221
ie9/js/c1.png 4272
ie9/js/c2.png 343
Total size: 10189
10K Limit: 51

Minified only(CSS+JS combined): 16641
string2png (1 png): 7571 (45.50%)
string2pngs (split pngs): 8836 (53.10%)

To sum up: the size for the combined JS (without the bootstrapper) and the CSS is 16,641 bytes. Compressing everything down to 1 image got a compression ratio (compressed data/uncompressed data) of 45.50%. Splitting the code into multiple images for IE9 and the ratio reduced to 53.10%.

Also, this time I used PNGOUT to process the generated PNGs and got the size down even more. My submitted code actually was not compressed with PNGOUT and weight at a total of 10,239 bytes with only a single byte left to spare, I just couldn’t find a place to fill up the last 1 byte — it was too time consuming so I let it go at 10,239. Anyhow, running the same build (with some modifications/bug fixes) and PNGOUT gave me an extra 51 bytes. PNGOUT is definitely awesome to further compress the code.




cal
8:12 pm
August 21, 2010
#302425

why not encode 3 bytes per pixel (r/g/b) or even 4 (r/g/b/a). it seems like you’d fix more code in that way, since those compressed zero bytes have a non-zero cost. does it come out better or worse?




cal
8:19 pm
August 21, 2010
#302427

I mis-read - you’re storing 8-bit PNGs. However, the ‘PLTE’ block takes up space itself, which wouldn’t be needed for a 24 or 32 bit image. Would be interesting to benchmark all 3 and see if one is significantly different to the others.




8:49 pm
August 21, 2010
#302430

@cal

Storing the characters in 3 color components of the pixel was my very first implementation :) Great minds think alike! However, this approach won’t provide the most optimized image for the purpose of encoding the string into an image.

It took me a while to figure this out. In one of my attempts, I used all 3 color components to store the javascript text and output it as a PNG8. As soon as I hit a certain length of the string, my decoded script would look all mumble jumble. However, as soon as I shortened that string by 1 character, everything looked fine. It was because the PNG8 output image lost some of the colors and the missing colors were substituted with something else (probably the closest matching colors). So when the text was extracted from the image, some of the characters were replaced by other characters wrongly while some would come out correctly.

8-bit PNG (or GIF) have a color palette of only 256 colors, this means that we can reliably store the characters in only one color component. If we put 3 characters into a pixel, say #616161 (that’s for AAA), we would potentially have 256*256*256 possible color combinations. We’d end up with a PNG24 instead. And size-wise a PNG24 is bigger than a similar PNG8, since in PNG8, the color palette is fixed (indexed) so it has much better compression than PNG24, where the pixels are not indexed.




12:09 am
August 22, 2010
#302445

i think it could be nice to make your apps bookmarkable.

aka project + path + lineno in the url anchor




11:48 am
August 22, 2010
#302526

I have proposed this for quite a while, you could even add it to an existing header image and crop it in CSS - thus you even save another HTTP request on loading.




12:07 pm
August 22, 2010
#302530

@Chris,

Can you explain a bit more about adding “it” (is this the encoded data?) to the header image?

@Jerome: it’s a feature I’ve been thinking about, so it’ll be coming soon :) The problem with opening up a path upon running GHFinder is that I’d have to traverse down the folders (or “tree”) one by one to grab the latest tree data and and try to locate the file, then eventually if the file is found, it’d be loaded. This can potentially be a lot of ajax calls if the path is deeply nested.




Dennis
3:16 pm
August 22, 2010
#302551

Have you tried using one image, and then copying the image data beyond the first 8192 bytes mark back to 0,0 for IE9?




4:50 pm
August 22, 2010
#302565

Dennis, thanks for the comment. I went back and tweaked the extraction method and got it to work with IE9!




11:59 pm
August 22, 2010
#302604

Wow, impressive!




cal
1:05 am
August 23, 2010
#302611

I had a play with different ways of encoding data into the image to see if it could be useful in production. I wrote up the results here: http://www.iamcal.com/png-store/




IIXII
5:38 am
August 23, 2010
#302648

You should use a greyscale PNG for best compression without the unneeded PLTE chunk.




10:24 am
August 23, 2010
#302678

I tried access http://www.nihilogic.dk/labs/canvascompress/ and IE9 Platform Preview 1.9.7916.6000 works with any file that it loads.




12:51 pm
August 23, 2010
#302698

@cal: excellent work! Thanks for putting together the comparison. I went back and change the build script to spit out really wide image, and the compression ratio is now down to 38.35%! Amazing. I may re-submit GithubFinder with more functionality since I now have an extra 2000 bytes to play with.

@Vitor, I changed the script to output the PNG as grayscale and the size is even smaller (compression ratio at 30.52%)! Great!




Stephen
2:09 pm
August 23, 2010
#302706

I can understand being annoyed that it didn’t work in IE, but saying they shouldn’t release a preview because it might be buggy is a really stupid comment, the team was constantly criticized for their lack of activity and listening to community, so with IE9 they specifically wanted to get latest versions of IE in the hands of developers to give them a chance to find bugs before release.




2:25 pm
August 23, 2010
#302709

@Stephen,

The criticism here is that the IE9 team should not rush to release a half-ass application. If they really want to win back developers, they should treat developers well by providing solid toolset and quick, easy way to find and submit bugs. Releasing a browser without an address bar? That is just crappy and in a way, I feel like I’m being mistreated for being an early adopter at using IE9 preview. The product manager should have delayed the release until it’s at least usable. Treat developers with dignity and respect. That’s the point which everyone is making.

PS: IE9 Developer Tool is still very much sucky. It’s a piece of crap compared to Firebug.




cal
3:44 pm
August 23, 2010
#302717

Nice! I can’t see anyway to create grayscale PNG-8s from PHP’s GD bindings. Makes sense though, since dropping the PLTE while retaining the same data is going to be a bit (780 bytes) smaller.




cal
4:30 pm
August 24, 2010
#302860

I switched to using Perl & ImageMagick for the generation and came up with a bunch of new numbers (along with some new bugs):

http://iamcal.github.com/PNGStore/




12:43 am
August 25, 2010
#302913

Have you tried applying any lossless PNG optimization tools, like optipng or pngcrush, to these images?




bob
11:12 pm
August 26, 2010
#303208

Of course, gotta include the IE bashing, mandatory in any article discussing HTML5. It’s a DEVELOPER PREVIEW. You understand what that means, right? That means its less than beta. That means it’s not a release. That means you’re NOT an early adopter. The lack of an address bar? WAAH, I forgot the URL of the page I’m visiting because it’s not immediately visible. WAAH, I have to click a few more buttons to navigate to different pages. If this was their release browser, you would have a point, but it’s a DEVELOPER PREVIEW. It’s not a release of a product. It’s a testing program designed so YOU can break it before a non-developer can when the ACTUAL browser is finished and released.

As for the bug reporting, I can see where you’re coming from. I already had a Live account so it wasn’t a hassle to me, but to each his own I guess.

Finally, enough rant and one observation: If such compression is possible, I wonder why it is not natively supported by browsers. It would be great if, for example, server software can detect and compress javascript in this way (also applying the usual gzipping) and the browser decompresses it upon receiving it.




Nicholas
4:01 pm
August 28, 2010
#303483

This is incredible! Great work.

I think IE’s limitation is by design: dataURIs in IE can only be 32kb, so I’m interpreting that to mean the 8192 byte limit has something to do with reading from an rgba representation limited to 32kb. Anyway, this seems unlikely to change between now and release—but the 0,0 workaround is truly inspired.

Jesus, I just defended IE.




6:43 am
November 16, 2010
#322134

Isn’t the problem with IE9 that the maximum image width is 8192? (possibly because of hw acceleration limits)
What if you put the data into a more square/rectangular image?




 

Leave a Reply