Check out my
new book!
HTML5 Games book
Compression using Canvas and PNG-embedded data I got this idea to stuff Javascript in a PNG image and then read it out using the getImageData() method on the canvas element. Unfortunately, for now, that means only Firefox, Opera Beta and the recent WebKit nightlies work. And before anyone else points out how gzip is superior, this is in no way meant as a realistic alternative. Earlier today I posted about a compressed 8 KB version of the Super Mario script using this technique. Here are some more details about what is going on.

Now, the image above may look like noise to you but it is actually the 124 kilobyte Prototype library embedded in a 30 kilobyte 8 bit PNG image file. In a lossless image format, you could in theory store any kind of data in the pixel values, for instance Javascript code as we're doing here. And since many image formats offer some form of compression we can take advantage of that to shrink our code to a smaller size, provided that we are able to extract that data again.

The first step was to find the best image format for the job, that means the one that gives the best compression while still being lossless. Here on the intertubes, we don't get a lot of image format choices and since JPEG is lossy, we're down to GIF and PNG.
For PNG we have two options, 24 bit and 8 bit. Using 24 bit RGB colors, we can store 3 bytes of data per pixel while 8 bit indexed colors only gives us 1 byte per pixel.
A quick test in Photoshop tells us that a 300x100 8 bit image with monochromatic noise compresses down to just 5 KB while a 100x100 24 bit image with similar noise applied to each of the R, G and A channels compresses down to about 20 KB. A regular 8 bit GIF comes in a bit heavier than the 8 bit PNG, so we go with the PNG option. (Photoshop noise doesn't alter every pixel, but code isn't random either, so I figure it equals out.)

Now we need to convert our Javascript file into color data and stuff it in a PNG file. For this purpose, I crafted this quick and dirty PHP script, which reads the Javascript file, creates a PNG image file and simply lets each pixel have a value 0-255 corresponding to the ascii value of the character in the script.

I ran into a problem here, since the image is created as a truecolor image and we need it to be 8 bit indexed and PHP won't make an exact conversion. I guess there are ways to create a palletted image from scratch in PHP/GD, but I haven't looked into that yet. The solution for now is to simply run the generated image through something like Photoshop and convert it to 8 bit there.

So now we have the Javascript all nice and packed up in a compressed PNG file and now we need to get it out again in the client. Using the canvas element, we simply paint the picture using drawImage() and then read all the pixel data using getImageData(). This data is given to us as a large array of values, where each pixels takes up 4 elements (RGBA), so we just take every 4 value and tack them all together into an eval()-ready string. And we're done.

The reading function can look something like this: pngdata.js

A few test results:

prototype-1.6.0.2.js
123 KB Javascript compressed to 30 KB PNG (24%)

jquery-1.2.3.min.js
53 KB Javascript compressed to 17 KB PNG (32%)

excanvas.js
24 KB Javascript compressed to 8 KB PNG (33%)

excanvas-compressed.js
10 KB Javascript compressed to 5 KB PNG (50%)

dijit.js
46 KB Javascript compressed to 16 KB PNG (35%)

Pretty decent results and even for packed (or otherwise minified) scripts we can shave off another 50% using this method. The PNG's can even be further compressed using various optimizing tools. Check the comments for more details and test results (thanks FreakCERS!).

Click here to play with the test images yourself.

There is of course a bit of overhead, since we need some code to read the data and execute it, but it can be cooked down to 300 bytes or so. Some files also don't compress well, ie. files that are already very compressed. I tested it on Mathieu 'p01' Henri's excellent (and heavily compressed) 3D Tomb 2, and since the file is already so small and compressed, whatever was saved by the PNG compression was lost in the overhead.

And of course, for larger scripts you will also feel a significant load time as first the image is being painted to the canvas and then the pixels are read. Reading and parsing the 69 KB PNG compressed from the 255 KB dijit-all.js Javascript takes about 5-6 seconds (in FF2, Safari and others are faster) which generally isn't acceptable. The 16 KB PNG from the 46 KB dijit.js Javascript takes only 500-1000 ms, so for smaller files this problem isn't so big.

Anyway, since the support for the getImageData method on the canvas element isn't widely supported yet, I guess this remains a curiosity for now and just another way to use/misuse the canvas. So, this is meant only as thing of interest and is not something you should use in most any real life applications, where something like gzip will outperform this.

Again, click here to see the "decompression" in action. And here to see the Mario game using this technique.
⇓ 51 comments Anonymous

At the end of the embedded data the text shows many "null" (or something like that)
http://img257.imageshack.us/img257/254/00bm0.jpg

May 4, 2008 at 1:06 PM
Anonymous

Forgot to say: using Firefox 3 beta 5, on XP SP2.

May 4, 2008 at 1:07 PM
Jacob Seidelin

Ah, yea. Fixed. Thanks.

May 4, 2008 at 2:11 PM
Martin Hassman

Nice work. Looks other packers can still beat this, e.g. for jQuery

May 4, 2008 at 3:41 PM
Jacob Seidelin

Well it's hard to compare since that post seems to be using an older version of jquery, but I figure using gzip will give about the same compression as this.

May 4, 2008 at 10:33 PM
Anonymous

Is this a job worth doing?.. Ofcource it still may be useable by browsers that doesn't support GZip compression yet (which one doesn't?)...
Meanwhile, GZip compressing of given JS files gave me these results:

prototype-1.6.0.2.js
126,127 bytes -> 27,897 bytes GZip (22% vs. 24% PNG)

jquery-1.2.3.min.js
54,075 bytes -> 15,356 bytes GZip (28% vs. 32% PNG)

excanvas.js
23,822 bytes -> 6,770 bytes GZip (28% vs. 33% PNG)

excanvas-compressed.js
9,464 bytes -> 3,591 bytes GZip (38% vs. 50% PNG)

dijit.js
46,691 bytes -> 14,222 bytes GZip (30% vs. 35% PNG)

And, it needs no additional JS for "uncompressing", just a slight change in link to JS file. And even that can be avoided by writing rewrite rule (mod_rewrite).

So, I think, PNG compressing is just a fun theoretical stuff that has no practical use... But still this is a bit... extraordinari thing that was interesting to read and know of, thanks :)

Regards,

May 5, 2008 at 1:24 AM
Jacob Seidelin

No, you are entirely correct. I can't think of many reasons to use this over gzip. As I said in the post, it is merely another canvas curiosity.

May 5, 2008 at 1:47 AM
Anonymous

Did you try on the packed or unpacked code of 3D TOMB II ?

Anyhow I doubt the PNG compression would gain much as I tweaked my code to be very packer friendly to the type of packer ( LZSS ) I used.

Have you tried GIF vs PNG8 vs PNG24 ? and using PNGCrush and GIFOpt of course.

A crazy idea for huuge script would be to use a JPEG for coarse packing + a PNG for the error correction ;)

May 5, 2008 at 2:33 AM
Jacob Seidelin

@p01: Using it on the packed code did nothing at all. I think it even added a few bytes. Unpacking and then PNG compressing gave a few hundred bytes, but with the reading overhead, any advantage was lost. So yea, your compression is very efficient :)

I actually wasn't aware of pngcrush (or gifopt), I just went with what Photoshop produces. Maybe I'll see if that makes any difference. I did try both GIF, PNG8 and PNG24, though, and PNG8 seemed to give the better result.

May 5, 2008 at 2:52 AM
The Professor

Using optipng, I was actually able to compress the images further (though not much)

Here are a list of my results:
15515...dijit.js_optimized.png
15664...dijit.js.png
3920....excanvas-compressed.js_optimized.png
4180....excanvas-compressed.js.png
7443....excanvas.js_optimized.png
7623....excanvas.js.png
16577...jquery-1.2.3.min.js_optimized.png
16721...jquery-1.2.3.min.js.png
30000...prototype-1.6.0.2.js_optimized.png
30115...prototype-1.6.0.2.js.png

so there is actually a further 6.22% of the compressed png's to be shaved off in one case...

May 5, 2008 at 4:34 AM
Jacob Seidelin

@FreakCERS: Nice. Thanks for that. I guess every byte counts :)

May 5, 2008 at 5:18 AM
The Professor

Forgot to add that optipng will also automatically reduce the pallette, so you could probably bypass the use of photoshop entirely, and still end up with 8bit grayscale images

and for good measure, a link to optipng: http://optipng.sourceforge.net/

May 5, 2008 at 5:28 AM
Virtual Goods

this is smart thinking! I really like it, its ingenious!

May 5, 2008 at 7:08 AM
Anonymous

try using

imagetruecolortopalette ($im ,false , 256);

just before you output the image.
Saves opening photoshop :)

May 5, 2008 at 8:08 AM
Anonymous

hmm, actually that messes up the data - nevermind.

May 5, 2008 at 8:22 AM
Anonymous

OK, I think I got it sorted.

use this php & you'll get a 8-bit image with correct data

...

$iFileSize = filesize($filename);

$iWidth = ceil(sqrt($iFileSize / 1));
$iHeight = $iWidth;
//$im = imagecreatetruecolor($iWidth, $iHeight);
$im = imagecreate($iWidth, $iHeight);

$fs = fopen($filename, "r");
$data = fread($fs, $iFileSize);

fclose($fs);

$i = 0;
$colors = array();

for ($y=0;$y<$iHeight;$y++) {
for ($x=0;$x<$iWidth;$x++) {
$ord = ord($data[$i]);
if(!$colors[$ord]){
$colors[$ord] = imagecolorallocate($im,$ord,$ord,$ord);
}
$color = $colors[$ord];

imagesetpixel($im, $x, $y,$color);
$i++;
}
}
...

May 5, 2008 at 9:10 AM
Jacob Seidelin

@david wilhelm: Yea, the imagetruecolortopalette function doesn't do an exact conversion. Maybe I'll try your method. Thanks!

May 5, 2008 at 9:43 AM
Ellis

PNGout seems to give me the best results on optimizing the PNG file size.

May 5, 2008 at 11:36 AM
Ellis

Here's the PHP refactored

$filename = 'prototype-1.6.0.2.packed.js';

if(file_exists($filename))
{
$iFileSize = filesize($filename);
$iWidth = ceil(sqrt($iFileSize / 1));
$iHeight = $iWidth;
$im = imagecreate($iWidth, $iHeight);
$fs = fopen($filename, 'r');
$data = fread($fs, $iFileSize);

fclose($fs);

$i = 0;
$colors = array();

for($y=0;$y<$iHeight;++$y)
{
for($x=0;$x<$iWidth;++$x)
{
$ord = ord($data[$i]);

if(!$colors[$ord])
{
$colors[$ord] = imagecolorallocate($im,$ord,$ord,$ord);
}

$color = $colors[$ord];

imagesetpixel($im, $x, $y, imagecolorallocate($im, $color));

++$i;
}
}

header('Content-Type: image/png');
imagepng($im);
imagedestroy($im);
}

May 5, 2008 at 11:42 AM
Alek Traunic

just curious, since your ascii range is 33-126 why not get 4 characters per pixel using the full RGBA? i may be missing something obvious but my first reaction was "why no color?"

May 5, 2008 at 12:52 PM
Anonymous

Too bad ImageData.data is not assimilited to an Array. It would greatly ease the generation of the code. For a 32bits PNG image, it could become as simple as: eval( String.fromCharCode.apply( 0, context.getImageData( 0,0,width,height ).data ) );

Jacob: Btw you can gain 30-40 bytes on m.js

traunic: Bare in mind that some scripts use wider a range of characters.

May 5, 2008 at 3:49 PM
Spellcoder

OptiPNG, PNGOut(/PNGOutWin) or AdvanceCOMP might also help improve the compression and can throw away chunks of the PNG that aren't needed (like meta info on what program created it, color profiles etc).

May 6, 2008 at 2:02 AM
The Professor

Just for kicks, I've tried out AdvanceCOMP and pngout too (wrote a simple script to try different settings - so I think this should be as effecient as pngout can make it (but I am new to it, so I can't promise)

15664...dijit.js.png
15008...dijit.js_advpng.png
15515...dijit.js_optipng.png
14987...dijit.js_pngout.png
4180....excanvas-compressed.js.png
4073....excanvas-compressed.js_advpng.png
3920....excanvas-compressed.js_optipng.png
4050....excanvas-compressed.js_pngout.png
7623....excanvas.js.png
7381....excanvas.js_advpng.png
7443....excanvas.js_optipng.png
7334....excanvas.js_pngout.png
16721...jquery-1.2.3.min.js.png
16191...jquery-1.2.3.min.js_advpng.png
16577...jquery-1.2.3.min.js_optipng.png
16133...jquery-1.2.3.min.js_pngout.png
30115...prototype-1.6.0.2.js.png
28944...prototype-1.6.0.2.js_advpng.png
30000...prototype-1.6.0.2.js_optipng.png
28839...prototype-1.6.0.2.js_pngout.png

it seems pngout wins most of the time, but not all...

May 6, 2008 at 7:00 AM
Alek Traunic

@Mathieu 'p01' Henri: looking at www.asciitable.com I can only think that strings of text could possibly fall outside the 33-126. The vast majority of javascript will be covered and the exception strings could easily exists outside the image. Do you have any specific examples?

May 6, 2008 at 9:22 AM
Anonymous

traunic: Any compressed ( not just minified ) JavaScript. See 3D TOMB II or Super Mario in 14k of JS for instance.

Such technique is becoming more and more common ( with things like Dean Edward's packer, or YUI compressor, ... ) especially among the people crazy enough to encode their JS into a PNG ;)

May 6, 2008 at 10:56 AM
Alek Traunic

@m_p01_h: actually www.nihilogic.dk/labs/mario/mario.js gives an even better example in the aSpriteData values. so there is your example of a legit (although currently edge case) argument against the 33-126. Unfortunately, if this means supporting the entire possibilities of UTF-8 (or 16 if you like) then the practicality of the image solution having more than one char per pixel gets a bit blown. i would still be interested to see how much a 33-126 script gets compressed when using 4 chars per pixel. (it might look nice too ;)

May 6, 2008 at 1:32 PM
Anonymous

One question ... Why?

June 9, 2008 at 5:11 AM
Anonymous

As a trackback from translation of this article:
this 'compressing' method can be used for hiding exploits and malicious JS from user.

June 20, 2008 at 7:57 AM
Anonymous

amazing work, that's really awesome stuff.

Want to have a go at this myself, at the moment I'm relying on Google AJAX Libraries API to speed things up; which is also returning a good speed boost.

Keep it going, good work!

July 21, 2008 at 2:29 AM
Anonymous

Ingenious. I'm thinking crazy cross-domain Ajax via PNGs. Nice.

November 10, 2008 at 8:29 AM
Jacob Seidelin

@Jamie: Pixel data access with Canvas is limited by a same-origin policy much like XHR, so that wouldn't work. Nice idea, though. =)

November 10, 2008 at 10:44 AM
Anonymous

With modern broadband speeds surely the speed to decode versus the speed to load makes this pointless? In the days of 56k modems probably worth it but not now.

If your JS scripts are that large... maybe you're doing too much in JS? ;-)

I have managed most things with CSS, including tabbed menus and uber tooltips. I rarely use JS as I think a site can usually be designed without it, or at least with hardly any JS.

Even so, this is an amazing idea which proves you are a programming wizard :-)

January 27, 2009 at 10:49 AM
Anonymous

Thanks for this fresh information; I was looking for a convenient place for compressing heavy video file. Besides these, I have found another place to compress and decompress all sorts of files. That is www.krunchit.net where you can zip or unzip ten files online altogether.

February 11, 2009 at 3:39 AM
Anonymous

Assuming no characters > 127 are used, you can squeeze out another kilobyte or so by doing something like this:

Outside the loop...
$i = 1;
$last = ord($data[0]);

Inside the loop...
$ord = ord($data[$i]) + 127;
$offset = $last - $ord;
$last = $ord;
imagesetpixel($im, $x, $y, imagecolorallocate($im, $offset, $offset, $offset));

August 3, 2009 at 3:53 AM
Anonymous

Appending my last comment:

It appears a minified PNG of Mootools using my method + PNGOUT is actually smaller than a minified GZIP. I wasn't expecting that! :)

August 3, 2009 at 4:05 AM
Anonymous

Aaaand you can get rid of my last two comments. My math was wrong when trying to get the offset of each pixel - whoops.

August 3, 2009 at 4:52 AM
Alex Le

I wrote up a Ruby version for the string2png (using RMagick) here:

http://alexle.net/archives/306

Cheers!

Alex

August 21, 2010 at 1:57 PM
Jacob Seidelin

@Alex: Nice! I just noticed now that my PHP script can't be accessed anymore. Guess I should fix that.
It was awfully simple and crude anyway.

Also interesting that this technique found it's way into the 10K apart contest. Not using it for my own entry, though.

August 21, 2010 at 3:21 PM
cal

Played with some different encoding variants (bit depth, filters, bit packing) and it seems like PNG-8 using straight-up ASCII is probably the best for most files: http://www.iamcal.com/png-store/

August 23, 2010 at 12:23 AM
Anonymous

PNG is based on ZIP-compression. but all mayor browsers (the ones that are mentioned above plus many more) directly support GZIP-compression, which of course is also true for external JS-files, you can e.g. directly serve them from e.g. prototype.js.gz file, or alternatively activate runtime GZIP-compression on the server.

and since ZIP and GZIP-compression (for single files) are more or less identical, apart from a few different header-bytes, I don't see the purpose of using ZIP via PNG. can anybody think of a real-word advantage? or is this just meant as a gimmick?

August 23, 2010 at 4:51 AM
Brad

This may be a really dumb question... but what's the best way to call the loadPNGData() function on the html page to get the decoded javascript onto the page? :|

August 24, 2010 at 7:39 AM
Adam Ness

For folks who are wondering why compress at all, think of your monthly hosting bill. If you can cut the number of bits flowing to your upstream provider by 75%, that can make your hosting bill a heck of a lot cheaper.

It is an interesting hack, though as a lot of other folks pointed out, runtime or static gzip will save you the same amount (Both are compressing the bytes using the DEFLATE algorithm, and gzip has a smaller header than PNG)

August 25, 2010 at 6:29 PM
Web Resources.eu

Jast perfect ! :) xaxaxaxa :D

This technique is realy very helpfull but not for now !

We have to wait for full support of the canvas element !

Thanks !

September 4, 2010 at 7:00 AM
Anonymous

you might archieve much better results by using all 8 bits per pixel. Currently you use only one bit (black or white) pixel, however, PNG will store the other 7 pixels anyway (and however, this format is not very well suited for black/white compression anyways). You could either try to use all pixels (creating coloury pictures) or create a GIF file (which would affect in even better compression, as I believe.

September 6, 2010 at 12:04 AM
micksam7

I stumbled upon this method earlier and played with my own implimentation of it.

You can encode it into all 3 RGB channels in a 24-bit png and use a program like pngout to compress it. I actually saw about a 5% improvement in my experiments; not a lot, but an improvement never the less. Attempting to encode info into the alpha channel inflated the size, though.

September 9, 2010 at 6:15 AM
Victor Doss

Anyway to hack this to get cross domain xhr working ? All we need is one server that takes an url and dumps the content pointed by URL in PNG. Any content in the world is now available in ajaxy way. Any takers ? Client side mashup just got interesting...

September 23, 2010 at 11:28 AM
Anonymous

Can the encoding done in C# ? need help!!

February 17, 2011 at 1:21 AM
sagartu

.net solution of it... Enjoy!!!

Author : Sagar T U

usage : Give the byte array and destination file name...

public void createPNG(byte[] data, string fname)
{
// 8bpp
double w = Math.Floor(Math.Sqrt(data.LongLength));
double h = Math.Ceiling(data.LongLength/w);
int i;

Bitmap bmp = new Bitmap((int)w,(int)h);

Color[] clr = new Color[256];

for (i = 0; i < 256; i++)
{
clr[i] = Color.FromArgb(i, 0, 0);
}

i = 0;
for (int y = 0; y < h; y++)
{
for (int x = 0; x < w; x++)
{
if(i<data.LongLength)
bmp.SetPixel(x, y, clr[data[i]]);

i++;

}
}

bmp.Save(fname, ImageFormat.Png);

}

February 22, 2011 at 1:15 AM
Anonymous

Compression using Canvas and PNG-embedded data article presents a method that allows packing the 124 kilobyte Prototype library embedded in a 30 kilobyte 8 bit PNG image file. The data inside PNG image is and read out with JavaScript using the getImageData() method on the canvas element.

March 9, 2012 at 10:51 PM
Anonymous

Have you considered embedding the script in an actual image you need for the document to reduce overhead even more?

July 24, 2012 at 6:08 PM
Dr. Clue

This is not the first time I've seen images used in this fashion, but it is the first time I've noticed it in a web scripting context.

While some may consider it but a curiosity, there could be a number of practical uses for it including access control,widget packaging,active QR codes,as well as some interesting possibilities with HTML5 level custom protocol handlers and the like.

As to the compression itself, a little tinkering along with packing in other resources to save on request counts should yield
some fairly positive advantages overall that some may not be accounting for.

Even before the Internet went public we used images for conveying more than graphics. They've even been used for confidential communications by spooks and thugs and as multipart keys.

Thanks for the writeup. It came to my attention as a quasi random result while surfing up some compression information, making for an interesting diversion from the day's coding tasks

September 14, 2013 at 9:12 PM
Post a Comment