How To Make A Transparent Image With Dynamic Color Layers

Posted By on Feb 2, 2013 | 5 comments


This is a new one hot off the press. I’m excited because this is the first actual product I’ve come up with that I can immediately put on my blog.

Right now, I’m working on a site that will eventually use brand-based templates. The client hopes to eventually sell to other people, who can then color up the site to match their particular branding and colors. For the most part, this is easy. CSS covers most of the items that would be colored but images cannot…. The images on this site need to not only have a changing color, but there are shadows and other elements to the images that need to be branded as well.

This is the original image:

Here’s an example of what I’m talking about:

The process for this is pretty easy. To start, you need to break your source image into separate, one-color (black) layers; one for each different color grade you plan to use. From there, the code will work to overwrite your one-color channel with the desired color and grade. Take our source image; when you break it apart, you get the following layer images:

Another thing to note: over time experimenting with this code, I’ve found it best to start with a image larger than you intend to use. For the most part, this seems to help with pixelation in merging the layers.

Our source image cleanly breaks into three layers: the border, the background, and the quotes. Now, we want the original color to be assigned to the border layer, a solid white to the quotes, and somewhere in between for the background.

Use whatever method you would like to measure out the differences in your original source image. For ours, I found the color to be about 35% lighter than the border. So, for any new image, we know that the background will be that same grade lighter, whatever the color. After a little Google searching I found the below function (originally here) to help translate colors for me. Simply provide it a six-character hex code (eg. #FF0000) and a factor, and the code will return you a new six-character hex that represents a color that is that much closer to white.

function hexLighter($hex, $factor = 30) {
	$new_hex = '';
	
	$base['R'] = hexdec($hex{0}.$hex{1});
	$base['G'] = hexdec($hex{2}.$hex{3});
	$base['B'] = hexdec($hex{4}.$hex{5});
	
	foreach ($base as $k => $v) {
		$amount = 255 - $v;
		$amount = $amount / 100;
		$amount = round($amount * $factor);
		$new_decimal = $v + $amount;
		
		$new_hex_component = dechex($new_decimal);
		
		$new_hex .= sprintf('%02.2s', $new_hex_component);
	}
	
	return $new_hex;         
}

Next, we set up our images to work with. Using the PHP GD image library, we need to create two images: one to be our final composite, and one to help with the merging (explained more later).

// Desired final size of image
$n_width = 50;
$n_height = 50;

// Actual size of source images
$width = 125;
$height = 125;

$image = 	imagecreatetruecolor($width, $height);
			imagesavealpha($image, true);
			imagealphablending($image, false);

$n_image = 	imagecreatetruecolor($n_width, $n_height);
			imagesavealpha($n_image, true);
			imagealphablending($n_image, false);

$black = imagecolorallocate($image, 0, 0, 0);
$transparent = imagecolorallocatealpha($image, 255, 255, 255, 127);
	
imagefilledrectangle($image, 0, 0, $width, $height, $transparent);

There’s a lot that goes into setting up a transparent image, as you can see. First you have to declare the new object (lines 9, 12). After declaring those images, you have to tell the GD engine to use the alpha channel, as it does not by defauly (lines 10, 13). Also, since we will be merging images, you have to temporarily disable GD’s alpha blending (lines 11, 14). Then, you allocate/define the colors you want to work with (lines 17, 18). Finally, you set the canvas by laying a layer of “transparent” color first before anything else (line 20). This set the stage for all our other work.

Next, we move to our created layers. Remember, they already need to have a transparent element to them, so stick to .gif or .png images for these. In the code, all the coloring is automated and fed by an array of parameters:

$layers[] = array( 'src' => 'layer01.gif', 'level' => 0 );	// Border
$layers[] = array( 'src' => 'layer02.gif', 'level' => 35 ); 	// Background
$layers[] = array( 'src' => 'layer03.gif', 'level' => 100 );	// White Quotes

Each array represents a separate image layer to be stiched. The src field corresponds to the filename of the image (relative to this script), and the level field represents the grade of color change you want. 0 represents the exact color provided and 100 is pure white.

Now for the really technical part. For each different layer provided, the script will scan it pixel-by-pixel on the x/y axes. For every transparent pixel it finds, the script does nothing. But, for every black pixel it finds, the script will replace it with the color code representing the provided color and the grade level for that layer. Simple enough, right? Here’s the code:

foreach ($layers as $idx => $layer) { 
	$img = imagecreatefromgif( $layer['src'] );
	$processed = imagecreatetruecolor($width, $height);
	
	imagesavealpha($processed, true);
	imagealphablending($processed, false);
	
	imagefilledrectangle($processed, 0, 0, $width, $height, $transparent);
	
	$color = hexLighter( $_GET['color'], $layer['level'] );
	$color = imagecolorallocate($image,
		hexdec( $color{0} . $color{1} ),
		hexdec( $color{2} . $color{3} ),
		hexdec( $color{4} . $color{5} )
	);
	
	for ($x = 0; $x < $width; $x++)
		for ($y = 0; $y < $height; $y++)			
			if ($black === imagecolorat($img, $x, $y))
				imagesetpixel($processed, $x, $y, $color);
	
	imagecolortransparent($processed, $transparent);
	imagealphablending($processed, true);
	
	array_push($layers_processed, $processed);
	
	imagedestroy( $img );
}

The code will loop through all provided source images, process them, and then add them to another, processed, array of images. First, we have to read the source image into memory (line 2). Then, we create a new image to hold all the processed information (lines 3, 5, 6, and 8). After that, we finally get to use our hexLighter function to calculate out the color we will be using, then we translate it to GD (lines 10, 11).

Now we have our stage set for the transfer/processing of color. Think of this next part like using a pantograph on your source layer. You scan the x and y axes of your source image, and for every black color you find, you insert a "color"-ed point at the same x/y coordinated on your new image (lines 17-20). Finally, push the image into the "processed" array, save a little memory by using imagedestroy on the source image you don't need anymore (line 27), and move on to the next layer.

With the technical complexities out of the way, we move to the procedural complexities. GD is a very particular library to work with; if you don't get your code exactly right, call all the right functions, and all in the right order, the image will not work. That said, this is the reason why we create two images to begin with.

foreach ($layers_processed as $processed) {
	imagecopymerge($image, $processed, 0, 0, 0, 0, $width, $height, 100);
	
	imagedestroy( $processed );
}

imagealphablending($image, true);

imagecopyresampled($n_image, $image, 0, 0, 0, 0, $n_width, $n_height, $width, $height);

imagealphablending($n_image, true);

The first, larger image you declared all the way back in the beginning is now used to build a composite of your newly created layers. The second image is then used for re-sizing the composite down to your desired size. It may seem like an extra step, but in order to keep the transparency channels in every layer and produce a true transparent image. Which brings me to the last step. Way back when we created the images, I mentioned that the alpha blending needed to be turned off. Now that we're done, it's time to turn it back on (line 11).

Finally, we serve up the image. Since this is intended to be the source of an img tag, we could just serve it back using either imagepng() or imagegif(), depending on taste. Remember to adjust your header depending on your choice.

// Towards the top of your script
if (file_exists( "cache/{$_GET['color']}.png" )) {
	header( 'Content-Type: image/png' );
	readfile( "cache/{$_GET['color']}.png" );
	
	exit(0);
}

// The final save/return of a newly processed image
header( 'Content-Type: image/png' );
imagepng( $n_image, "cache/{$_GET['color']}.png" );
imagepng( $n_image );

I decided to take it a step further and build in a simple file caching system to first check if the file exists before proceeding (lines 2-7). If no image already exists, the script runs through, and saves a copy in the cache (line 11) before returning the new image (line 12). Ideally, the client-user will call any newly-generated image long before any end-user does, leaving the end-user with a fraction of the processing time.

Here's the code all together:

function hexLighter($hex, $factor = 30) {
	$new_hex = '';
	
	$base['R'] = hexdec($hex{0}.$hex{1});
	$base['G'] = hexdec($hex{2}.$hex{3});
	$base['B'] = hexdec($hex{4}.$hex{5});
	
	foreach ($base as $k => $v) {
		$amount = 255 - $v;
		$amount = $amount / 100;
		$amount = round($amount * $factor);
		$new_decimal = $v + $amount;
		
		$new_hex_component = dechex($new_decimal);
		
		$new_hex .= sprintf('%02.2s', $new_hex_component);
	}
	
	return $new_hex;         
}

// Sanitize/Validate provided color variable
if (!isset($_GET['color']) || strlen($_GET['color']) != 6) {
	header($_SERVER['SERVER_PROTOCOL'] . ' 400 Bad Request', true, 400);
	
	exit(0);
}

if (file_exists( "cache/{$_GET['color']}.png" )) {
	header( 'Content-Type: image/png' );
	readfile( "cache/{$_GET['color']}.png" );
	
	exit(0);
}

// Desired final size of image
$n_width = 50;
$n_height = 50;

// Actual size of source images
$width = 125;
$height = 125;

$image = 	imagecreatetruecolor($width, $height);
			imagesavealpha($image, true);
			imagealphablending($image, false);

$n_image = 	imagecreatetruecolor($n_width, $n_height);
			imagesavealpha($n_image, true);
			imagealphablending($n_image, false);

$black = imagecolorallocate($image, 0, 0, 0);
$transparent = imagecolorallocatealpha($image, 255, 255, 255, 127);
	
imagefilledrectangle($image, 0, 0, $width, $height, $transparent);

$layers = array();
$layers_processed = array();

$layers[] = array( 'src' => 'layer01.gif', 'level' => 0 );	// Border
$layers[] = array( 'src' => 'layer02.gif', 'level' => 35 ); 	// Background
$layers[] = array( 'src' => 'layer03.gif', 'level' => 100 );	// White Quotes

foreach ($layers as $idx => $layer) { 
	$img = imagecreatefromgif( $layer['src'] );
	$processed = imagecreatetruecolor($width, $height);
	
	imagesavealpha($processed, true);
	imagealphablending($processed, false);
	
	imagefilledrectangle($processed, 0, 0, $width, $height, $transparent);
	
	$color = hexLighter( $_GET['color'], $layer['level'] );
	$color = imagecolorallocate($image,
		hexdec( $color{0} . $color{1} ),
		hexdec( $color{2} . $color{3} ),
		hexdec( $color{4} . $color{5} )
	);
	
	for ($x = 0; $x < $width; $x++)
		for ($y = 0; $y < $height; $y++)			
			if ($black === imagecolorat($img, $x, $y))
				imagesetpixel($processed, $x, $y, $color);
	
	imagecolortransparent($processed, $transparent);
	imagealphablending($processed, true);
	
	array_push($layers_processed, $processed);
	
	imagedestroy( $img );
}

foreach ($layers_processed as $processed) {
	imagecopymerge($image, $processed, 0, 0, 0, 0, $width, $height, 100);
	
	imagedestroy( $processed );
}

imagealphablending($image, true);

imagecopyresampled($n_image, $image, 0, 0, 0, 0, $n_width, $n_height, $width, $height);

imagealphablending($n_image, true);

header( 'Content-Type: image/png' );
imagepng( $n_image, "cache/{$_GET['color']}.png" );
imagepng( $n_image );

// Free up memory
imagedestroy( $n_image );
imagedestroy( $image );

With this script, you can provide any hex code and get a themed image returned. So a url of /image.php?color=223455 gets you:

And to use this in your markup:


Hope you enjoy and, if you have any ideas for improvements, please let me know!

5 Comments

  1. Thats awesome! I used this code on my website! Thanks Colonel 😉

    Post a Reply
    • Consider adding a layer to be ignored, maybe by setting level to -1 or something.

      Many times, at least in our case, we have buttons that may contain a real graphic followed by colored background and white text.

      Do you think this would be something easy to add? I’m no PHP expert, but I imagine it would be feasible!

      Drew

      Post a Reply
      • It’s absolutely feasible, Drew. Consider your real graphic a separate layer. All you would need to do is, when you are defining the layer array, add a new boolean value to the layer with your graphic in it. Then, in the first line of your for loop, add a quick test for that variable and step out of the loop if you find it.

        Something like this:

        $layers[] = array( 'src' => 'layer01.gif', 'level' => 0 );	// Border
        $layers[] = array( 'src' => 'layer02.gif', 'level' => 35 ); 	// Background
        $layers[] = array( 'src' => 'layer03.gif', 'level' => 100 );	// White Quotes
        $layers[] = array( 'src' => 'graphic.gif', 'level' => 100, 'skip' => true );	// New Layer (on top of everything)
        

        And your loop:

        foreach ($layers as $idx => $layer) {
                if ( isset($layer['skip']) && $layer['skip'] == true ) {
                    // Make an image object out of the layer and add it to the stack, then step out of the loop
        	    $img = imagecreatefromgif( $layer['src'] );
        
        	    array_push($layers_processed, $processed);
        
        	    continue; // Step out of the loop and move on
                }
        
        	$img = imagecreatefromgif( $layer['src'] );
        	$processed = imagecreatetruecolor($width, $height);
        
        	imagesavealpha($processed, true);
        	imagealphablending($processed, false);
        
        	imagefilledrectangle($processed, 0, 0, $width, $height, $transparent);
        
        	$color = hexLighter( $_GET['color'], $layer['level'] );
        	$color = imagecolorallocate($image,
        		hexdec( $color{0} . $color{1} ),
        		hexdec( $color{2} . $color{3} ),
        		hexdec( $color{4} . $color{5} )
        	);
        
        	for ($x = 0; $x < $width; $x++)
        		for ($y = 0; $y < $height; $y++)			
        			if ($black === imagecolorat($img, $x, $y))
        				imagesetpixel($processed, $x, $y, $color);
        
        	imagecolortransparent($processed, $transparent);
        	imagealphablending($processed, true);
        
        	array_push($layers_processed, $processed);
        
        	imagedestroy( $img );
        }
        

        Hope this helps! 🙂

        Post a Reply

Submit a Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.