shrinkImage

This article is part of a series which is continued here.


This article presents a proof of concept to greatly reduce/compress the filesize of alpha transparent PNG images via PHP without loosing the alpha information and reassembling the separated channels with the help of jQuery and HTML5 canvas.

Current evolution of HTML5, CSS3, widespread availability of broadband internet connections and (finally!) Internet Explorer 6 becoming more and more obsolete we see ourselves, at times, faced by new kinds of challenges.

As we frequently develop promotional microsites/raffles for our clients (and these kind of sites do actually have very special requirements in terms of product/brand identity and representation) we, as developers, are often forced to use huge amounts of PNG-images with full alpha-channel. Their sheer amount plus the fact that PNG is an almost uncrompessed format can become such an aforementioned challenge as you will see in the following article.

The Problem: huge filesize of PNG-images

In one of my latest projects I ended up having a page with 4.5MB of data in total which was mainly caused by one image-slider making extensive use of PNG files with alpha-channel information. The images of this slider, in fact, summed up to a total of 3.8MB alone which would lead to a loading time of approx. 20-30 seconds for a 2MBit connection! Otherwise the code was quite clean and making extensive use of CSS3 and sprites to reduce loading time and the number of requests. To put it in a nutshell: an unacceptable condition in terms of a developer’s honor.

I discussed my concerns with Christoph when he finally joked about trying to use canvas to somehow circumvent the problem of huge filesizes when being forced to use PNGs. We both had an extended laugh, at first.

The idea: separate color- and alpha-channel and reassemble them in a canvas element

I then thought about it for a moment and had an idea: Canvas could indeed be part of a possible solution if I would somehow manage to separate the image’s color-information from its alpha-channel and then save them as separate files. Due to the fact that the most problematic PNGs are those that would normally best be saved as JPG (if they did not need an alpha-channel) I was almost sure that it should be possible to save the separated color-information as JPG thus greatly reducing total filesize.

At first I did some research (always remember: Google is your friend) and found some interesting articles. None of them covered all aspects of what I wanted to achieve but they were a great source of inspiration and knowledge:

The abstract: four challenges on the way to PNG size reduction

So I ended up facing four questions needed for a simple proof of concept:

  1. Would it be easily possible to separate color- and alpha-information via PHP?
  2. Is it possible to re-combine these two channels via Javascript and the canvas-element?
  3. Is there any chance to write the combined channels back into an image?
  4. Will there be a way to circumvent the doubled amount of requests due to the separation?

1. Separating color- and alpha-information via PHP

I actually started with some far more simple code for the proof of concept but this is what I came up with in the end. In this example, I am leaving out some parts, including mainly the saving of the final images to keep it short and easy to understand. (See the end of this articles for the helper functions used in the PHP-part)

			<?php
			$sourceFile = 'Path to your source-image';

			// temporary storage for alpha-colors
			$colorCache = array(127 => rgb2color(0, 0, 0, 0));

			// read width and height from source-image
			$size   = @getimagesize($sourceFile);
			$width  = $size[0];
			$height = $size[1];

			// create image from source-image
			$imageSource = @imagecreatefrompng($sourceFile);

			// create image for color-information
			$imageJpg    = @imagecreatetruecolor($width, $height);

			// create image for alpha-information
			$imagePng    = @imagecreatetruecolor($width, $height);

			// enable alpha-handling for source- and alpha image
			@imagealphablending($imageSource, false);
			@imagesavealpha($imageSource, true);
			@imagealphablending($imagePng, false);
			@imagesavealpha($imagePng, true);

			// fill color- and alpha-image
			@imagefill($imageJpg, 0, 0, rgb2color(127, 127, 127));
			@imagefill($imagePng, 0, 0, $colorCache[127]);

			// iterate over source-image
			for($x = 0; $x < $width; $x++) {
				for($y = 0; $y < $height; $y++) {
					// convert GDLib-color to RGBa-values
					$color = color2rgb(imagecolorat($imageSource, $x, $y));

					// check if color alpha is less than 127 (which means transparent)
					if($color['a'] < 127) {
						// write color-information to color-image
						imagesetpixel($imageJpg, $x, $y, rgb2color($color['r'], $color['g'], $color['b']));

						// check if alpha-color is in temporary storage
						if(!isset($colorCache[$color['a']])) {
							// put alpha-color in temporary storage
							$colorCache[$color['a']] = rgb2color(0, 0, 0, 127 - $color['a']);
						}

						// write alpha-information to alpha-image
						imagesetpixel($imagePng, $x, $y, $colorCache[$color['a']]);
					}
				}
			}

			// convert GDLib-resources to color- (as JPEG) and alpha-image (as PNG)
			$imageJpg = getImage($imageJpg, 'jpeg', true, 80);
			$imagePng = getImage($imagePng, 'png', false, 9, PNG_ALL_FILTERS);
			?>

Just to give you a quick & dirty example the following source-image (leftmost: approx. 129 KB) is split into separate color- and alpha-images (approx. 32 KB in total using 80% compression for the JPEG)zwei

Note: The background pattern assigned is not part of the images but only used to visualize transparency. All images are resized via CSS to fit the layout.

2. Re-combine color- and alpha-image via Javascript and canvas

This is the main part of the javascript I ended up with. It combines the two separate channels from their img-elements via a canvas element. Remember to process this on window load. Otherwise the result will be an empty canvas.

			<script type="text/javascript">
			var context,
				width   = 256, // width of your source-image
				height  = 256, // height of your source-image
				jpg     = '', // img-element holding color-image
				png     = '', // img-element holding alpha-image
				canvas  = document.createElement('canvas');

			canvas.style.display = 'none';
			canvas.width         = width;
			canvas.height        = height;

			context = canvas.getContext('2d');
			context.clearRect(0, 0, width, height);
			context.drawImage(jpg, 0, 0, width, height);

			context.globalCompositeOperation = 'xor';
			context.drawImage(png, 0, 0, width, height);
			</script>

3. Write combined channels back into image

This is by far the simplest part as the HTML5 canvas element provides a native method to directly fetch a valid dataURL. As an example I use jQuery to create an image, assign the dataURL and append it to the document body.

			<script type="text/javascript">
			var dataURL = canvas.toDataURL('image/png');

			jQuery('<img />', { src: dataURL, alt: '' }).appendTo('body');
			</script>

Look at the final outcome on the right and carefully compare it to the source PNG on the left (remember, this is 32 KB instead of 129 KB, so only 25% of the original filesize):fünf

Note: The background pattern assigned is not part of the images but only used to visualize transparency.

Savings in filesize will vary roughly between 60% to 80% reduction of the original filesize (using 80% compression for the JPEG).

//

4. Circumvent doubled amount of HTTP-requests due to image separation

Well, up to now everything was quite simple due to the scripts being more or less a proof of concept. As stated before I left out some parts which, to be honest, mainly dealt with exactly this problem. The solution is in fact not that complicated but would simply be too much for this part of the article. Just to give you a hint: .htaccess and modRewrite come to the rescue in the second part of this article which can now be found here.

PHP helper functions
			<?php
			function rgb2color($r, $g, $b, $a = 0) {
				return ($r << 16) + ($g << 8) + $b + ($a << 24); 			} 			function color2rgb($color) { 				$return = null; 				if(preg_match('/^\d+$/', $color)) { 					$return = array( 						'r' => ($color >> 16) & 0xFF,
						'g' => ($color >> 8) & 0xFF,
						'b' => $color & 0xFF,
						'a' => ($color & 0x7F000000) >> 24,
					);
				}

				return $return;
			}

			function getImage($resource, $type = 'png', $interlace = false, $quality = NULL, $filter = 248) {
				if($interlace === true) {
					@imageinterlace($resource, 1);
				}

				ob_start();

				switch($type) {
					case 'png':
						$quality = ($quality === NULL) ? 9 : max(0, min(9, (int) $quality));

						@imagepng($resource, NULL, $quality, $filter);
						break;
					case 'jpeg':
						$quality = ($quality === NULL) ? 100 : max(0, min(100, (int) $quality));

						@imagejpeg($resource, NULL, $quality);
						break;
				}

				return trim(ob_get_clean());
			}
			?>
This article is part of a series which is continued here.

shrinkImage continued

This is part 2 of an article I wrote about a theoretical technique to reduce the filesize of PNG images with alpha-channel information by about 70% to 80%.

As this project has left the „proof of concept“ phase in the meantime this article will be far more practical than its predecessor and give you a fully functional jQuery plugin and additional sources to implement the automatic generation of the reduced images.

The best and only way I found to avoid multiple requests but keep the result easily usable in javascript was to use JSON as a container format for the compressed files.

The shrinkImage jQuery plugin

The plugin for jQuery 1.7.2 should work on all major browsers across all operating systems and gracefully fallback to the original PNG image in case canvas is not supported or the AJAX request fails for any reason. As the plugin normally requires other jQuery extensions I developed I combined them all in this package. The plugin uses strict mode and passes jsHint linting with checks for bitwise operators and defined but unused variables disabled.

How to use the plugin

<script type="text/javascript">
;(function($, window, document, undefined) {
    'use strict';

    $(document).ready(function() {
		$('img[data-shrinkimage],.shrinkimage').shrinkimage({
			attribute: 'data-shrinkimage', // default
			debug:     false,              // default
			quality:   80                  // default
        });
    });
})(jQuery, window, document);
</script>

<!-- Foreground image -->
<img alt="" data-shrinkimage="img/example.png" />

<!-- Background image -->
<div class="shrinkimage" style="background-image: url(data:image/shrink,img/example.png);"></div>

<!-- Foreground image with non default quality -->
<img alt="" data-shrinkimage="img/example.png?quality=75" />

<!-- Background image with non default quality -->
<div class="shrinkimage" style="background-image: url(data:image/shrink,img/example.png?quality=75);"></div>

<!-- Foreground image with non default storage location -->
<img alt="" data-shrinkimage="img/example.png?target=img/custom.shrunk" />

The passed options reflect the plugins built-in default options so could be left out in this case. Remember to not use the images src attribute for this plugin because if an image has this attribute it will be requested no matter what you do afterwards. If „debug“ is set to true the plugin will fallback to PNG. I implemented this because during development any kind of DOM inspector (Firebug or Chrome built-in) can become annoyingly slow due to the massive attribute values.

One thing to note is that shrinkImage always checks for the presence of a background image set via CSS and also tries to shrink it. For the same reason the images src-attribute cannot be used for foreground images shrinkImage requires a special form of data-URI for CSS background images (see example above).

You can also listen to the custom (namespaced) events shrinkimage provides to e.g. show a loading animation or whatever comes to your mind. See the following example to learn how to use shrinkImages‘ events:

<script type="text/javascript">
;(function($, window, document, undefined) {
	'use strict';
	$(document).ready(function() {
		$('img[data-shrinkimage],.shrinkimage')
			.on('requested.shrinkimage', function(event, file) {
				// do something when shrinkimage requests the
				// shrunk image
			})
			.on('queued.shrinkimage', function(event, file) {
				// do something when shrinkimage queued a request
				// because the same resource is already requested
			})
			.on('cached.shrinkimage', function(event, file, compressedSize, originalSize) {
				// do something whenever shrinkimage adds a
				// shrunk version to its temporary cache
			})
			.on('loaded.shrinkimage', function(event, file, usedCache, usedFallback) {
				// do something when shrinkimage loaded/processed
				// the shrunk image (includes cache hits)
			})
			.shrinkimage({
				attribute: 'data-shrinkimage', // default
				debug:     false,              // default
				quality:   80                  // default
			});
	});
})(jQuery, window, document);
</script>

 

ShrinkImage stores all processed shrunk images in a temporary cache at the latest processing stage possible to not have to request and/or process them again and therefore further reduces time, requests and resources needed for subsequent requests of the same file.

A practical example

Recycling the example from the previous article this is what the plugin comes up with (original image is left, compressed one is right):

Unbenannt3

Note: Again, the background pattern assigned is not part of the images but only used to visualize transparency.

Automatic serverside generation

.htaccess redirection

As mentioned in the prior article a little .htaccess magic is needed to make automatic generation possible. Make sure you have mod-rewrite enabled in Apache and put the following lines into the corresponding section:

AddType application/json .shrunk
AddType application/javascript .shrunk.jsonp

<IfModule mod_rewrite.c>
	RewriteCond %{REQUEST_FILENAME} !-f
	RewriteRule ^(.+.q(?:[0-9]+).shrunk)$ shrinkimage.php?file=$1 [QSA,L]
	RewriteRule ^(.+.q(?:[0-9]+).shrunk.jsonp)$ shrinkimage.php?file=$1&jsonp=1 [QSA,L]
</IfModule>

<Ifmodule mod_deflate.c>
	AddOutputFilterByType DEFLATE application/json application/javascript
</IfModule>
</pre>


What these lines do is very simple: Whenever a file with the prefix ".shrunk" is requested and the file does not already exist on the servers filesystem that request is redirected to a PHP script (in the webservers root directory in this case). Requests to a file with the prefix ".shrunk.jsonp" will, in contrast, always get redirected because of the custom callback name required for JSONP. The other lines define proper content-types and enable gzip compression if supported by the requesting client.

PHP image processing

The PHP file doing all the hard work is also part of the download package and is, in the end, only an advanced example of how the processing can or has to be done. I will not go too much into details in this article but will only very briefly summarize (as promised) what the file does:

At first the file checks if the compressed version already exists. If it does not exist it will be created and stored in the directory where the original image is located (so make sure PHP has write permission to these directories). Afterwards the script will output the compressed version as JSON or JSONP, depending on what the jQuery plugin requested.

The PHP file from the download package assumes being stored in the webservers document root which might not be suitable in your case. If that is the case simply alter line 12 of the file according to your needs and also adjust your .htaccess!

Download shrinkImage package

If you would like to stay up to date I also set up a GitHub repository for all my experiments.