Generate title and caption images on the fly

Monday, January 14, 2008   —   Vienna 0 Comments

This article demonstrates how to use PHP to generated title and caption images on the fly...

When a web designer wanted to display text as an image, the procedure would commonly begin with something in the vain of "open Photoshop..." But in the new web of dynamic content and massive scale, this approach won't work.

In most cases, an image text is used to display titles and headings. The example below is of the Apple's iPhone Developer Center.

Screenshot of iPhone Dev Center

All the headings here are images like this one:

Introducing the iPhone Developer Program. The fastest path from code to customer.

Each of these "static" images is created specifically for that title, and is stored on a server somewhere. The Apple website uses these titles all over the place. It makes me think that organizing and storing title graphics like this has got to be expensive – both in server space and effort. Plus it just doesn't scale very well. Imagine you wanted to use images to render the title of each article in your 3 year archive. You couldn't do it this way and meet any sort of budget, let alone the sadism of subjecting anyone to having to creating them. Imagine the headache of storing and backing up each image file. And the nightmarish logistics of indexing them to their corresponding article just give my vertigo!

This article will show you how to create title images like Apple's without having to hand-craft each one and without having to store a single image on your server. This approach is much easier, versatile, lightweight, and cheaper.

We will use the PHP GD library to create our title graphics dynamically. We simply pass the text you want to print through the URL, the script then does the rest of the work by applying the custom font, color, size and auto-wrapping the text for us. Neat huh?!

Choose a font

Before we begin, though, you're going need a TrueType font for this tutorial. A search for "free fonts" will give you plenty of websites that offer uh... free fonts. Otherwise you can rummage around on your computer for one.

Please fight the temptation is find some really cool, eye-catching, tricked-out, totally illegible font and splatter it all over your site. Creatively nudging the envelope is great. Cruel and unusual design is not. Did I mention that your font needs to be a TrueType font?

Step 1. Upload font

The first thing to do is to upload your font onto your server. The way I did it was to create a directory called "fonts" and drop suede.ttf into it. Incidentally, If your font file does not end with .tff then please reread the preceding paragraph.

Step 2. Create an image with text

Let's create an image using our font.

// Create image
$img = imagecreatetruecolor(353, 64);

// Set colors
$fgcolor = imagecolorallocate($img, 255, 128, 128);
$bgcolor = imagecolorallocate($img, 128, 64, 64);

// URL pointing to your font file
$fontfile = "../fonts/suede.ttf";

// Size of font
$size = 12;
$angle = 0;
$x = 0;
$y = $size;

// Some words to print
$text = "Nostalgia ain't what it used to be.";

// Fill entire image with bgcolor
imagefill($img, 0, 0, $bgcolor);

// Draw text with our custom TrueType font
imagettftext(
	$img, $size, $angle, $x, $y,
	$fgcolor, $fontfile, $text
);

// Output image header
header("Content-type: image/png");

// Flush image
imagepng($img);
imagedestroy($img);

If you get this ugly message:
Warning: imagettftext(): Could not find/open font in dyn_text_img_1.php on line 10

It most probably means your URL to the font file is wrong or you need to read this paragraph again.

If all goes well you should see this image when you run the script

Step 3. Snap image dimension to fit text

The next things we're going to do is make our image dimensions snap to the size of the text.

To do this, we get the bounding box of our text area using the PHP function imagettfbbox like this:


// URL pointing to your font file
$fontfile = "../fonts/suede.ttf";

// Size of font
$size = 12;
$angle = 0;
$x = 0;
$y = $size;

// Some words to print
$text = "Nostalgia ain't what it used to be.";

//
$bbox = imagettfbbox($size, $angle, $fontfile, $text);
$width = $bbox[2] - $bbox[0];
$height = $bbox[1] - $bbox[7];

// Create image
$img = imagecreatetruecolor($width, $height);

// Set colors
$fgcolor = imagecolorallocate($img, 255, 128, 128);
$bgcolor = imagecolorallocate($img, 128, 64, 64);

// Fill entire image with bgcolor
imagefill($img, 0, 0, $bgcolor);

// Draw text with our custom TrueType font
imagettftext(
	$img, $size, $angle, $x, $y,
	$fgcolor, $fontfile, $text
);

// Output image header
header("Content-type: image/png");

// Flush image
imagepng($img);
imagedestroy($img);

There we have it. Nice and trimmed .

Great. So we have our text printed and nicely trimmed off. So lets address a potential problem right now.

Let's say we want to print out a lot of text -- the first two sentences of "Lorem ipsum" for instance

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

What all that means remains a mystery to me, but it sure looks pretty the way the plain text wraps nicely within the paragraph tags. But when we try this with our script we get this design disaster...

The image width just continues all the way to Neverland in order to accommodate the length of our $text string. What we need to do is implement some sort of text-wrapping.

Step 4. Text wrapping

I have not found any function in PHP which is appropriate for this problem so I've written a wrapping logarithm of my own.

The wrapMyText function takes two parameters: a string $str of the text we wish to wrap and an integer $wrap_width, which is the number of pixels within which to confine the text.

This is how the function looks like (as much as I've tried, I couldn't, for the life of me, will myself to explain this line for line)


function wrapMyText($str, $wrap_width)
{
	global $size;
	global $fontfile;
	global $padding;
	$wrap = $wrap_width;
	$text = array($str);
	$xspan = 0;
	$yspan = 0;

	$i = 0;
	while($i < count($text))
	{
		$line = $text[$i];
		$bbox = imagettfbbox($size, 0, $fontfile, $line);
		$width = $bbox[2] - $bbox[0] + $padding;
		$yspan = $bbox[1] - $bbox[7] + $padding;

		if($width > $wrap)
		{
			$sub = $line;
			$length = strlen($line);

			while($width > $wrap)
			{
				$break = strrpos($sub, " ");
				if($break) { $length = $break; } else { break; }
				$sub = substr($line, 0, $length);
				$bbox = imagettfbbox($size, 0, $fontfile, $sub);
				$width = $bbox[2] - $bbox[0] + $padding;
			}

			if($length < 1) { break; }

			$trunked = trim(substr($line, $length, strlen($line)));
			$text[$i] = trim(substr($line, 0, $length));

			if($i < count($text))
			{
				$text[$i + 1] = $trunked . $text[$i + 1];
			}
			else
			{
				array_push($text, $trunked);
			}
		}

		$xspan = ($width > $xspan) ? $width : $xspan;
		$i++;
	}

	array_splice($text, 0, 0, array($xspan, $yspan));

	return $text;
}

The function returns an array whose first two elements are the width of the longest line of our wrapped text and the height of a single line of text. These are followed by n numbers of elements containing our original text spliced up into shorter lines.

Using the height that our function give us, we multiply it by the number of lines contained in our array (which is simply the entire array length minus 2) to determine how much height our image needs in order to contain all the lines of text. The image width is dictated by the longest line of text.

Let's wrap it up

Putting everything together would look something like this:


// URL to font file
$fontfile = "../fonts/suede.ttf";

$wrap = 400;
$padding = 4;

// Size of font
$size = 12;
$angle = 0;
$x = 0;
$y = $size;

// Some words to print
if(!$text) { $text = "Super-dooper long text..."; }

// $wrapped will be our array containing a chopped up version of our text $text
$wrapped = wrapMyText(stripslashes($text), $wrap);

$line_height = $wrapped[1];
$line_count = count($wrapped) - 2; // we minus two to omit the initial 2 elements (width and height)
$width = $wrapped[0];
$height = $line_height * $line_count;

// Create image
$img = imagecreatetruecolor($width, $height);

// Set colors
$fgcolor = imagecolorallocate($img, 255, 128, 128);
$bgcolor = imagecolorallocate($img, 128, 64, 64);

// Fill entire image with bgcolor
imagefill($img, 0, 0, $bgcolor);

// Draw text by iterating through the wrapped array starting from element #2
for($i=0; $i<$line_count; $i++)
{
	imagettftext(
		$img, $size, 0, $x, $y + ($line_height * $i),
		$fgcolor, $fontfile, $wrapped[$i + 2]
	);
}

// Output image header
header("Content-type: image/png");

// Flush image
imagepng($img);
imagedestroy($img);

// Text wrapping logarithm
function wrapMyText($str, $wrap_width)
{
	global $size;
	global $fontfile;
	global $padding;
	$wrap = $wrap_width;
	$text = array($str);
	$xspan = 0;
	$yspan = 0;

	$i = 0;
	while($i < count($text))
	{
		$line = $text[$i];
		$bbox = imagettfbbox($size, 0, $fontfile, $line);
		$width = $bbox[2] - $bbox[0] + $padding;
		$yspan = $bbox[1] - $bbox[7] + $padding;

		if($width > $wrap)
		{
			$sub = $line;
			$length = strlen($line);

			while($width > $wrap)
			{
				$break = strrpos($sub, " ");
				if($break) { $length = $break; } else { break; }
				$sub = substr($line, 0, $length);
				$bbox = imagettfbbox($size, 0, $fontfile, $sub);
				$width = $bbox[2] - $bbox[0] + $padding;
			}

			if($length < 1) { break; }

			$trunked = trim(substr($line, $length, strlen($line)));
			$text[$i] = trim(substr($line, 0, $length));

			if($i < count($text))
			{
				$text[$i + 1] = $trunked . $text[$i + 1];
			}
			else
			{
				array_push($text, $trunked);
			}
		}

		$xspan = ($width > $xspan) ? $width : $xspan;
		$i++;
	}

	array_splice($text, 0, 0, array($xspan, $yspan));

	return $text;
}

Wrapping our text to a width of 400 will give us this:

And I think that's pretty much enough to get your started on implementing the technique with your own specifications.

Snags!

This technique solves several problems. It enables you to use non-standard fonts for your titles, and gives you a means to automatically apply design to dynamic text. There is obvious benefit in being able generate as many image texts as you want without having to manage files on a server. Here is also the solution for that mercurial witty slogan you've been working on for a year for your website. Don't deny it ;-)

As is the case with almost any hack, however, there are drawbacks.

Images, for one thing do not support text-selection. There is a problem here that becomes obvious when you encounter something like this

Yes, that long, space deprived, string of letters is in fact a town in Wales. And because you don't believe me here's proof. And no, I wasn't really born there.

The average reader, coming across this would be thoroughly annoyed because the first thing their going to try and do is highlight the ridiculously long word and paste it into the Google search bar. But of course they can't because it's an image.

Here's another potential snag:
The name of my new book is: Great title don't you think?
...Uh, what title?

The image here has failed to load, rendering something clever into another reason for my reader to hate me.

Failsafe

The latter problem has a very simple solution provided by the alt attribute we can include within an image tag. The alt attribute was in fact specifically designed for this sort of problem; to allow image tags to degrade gracefully.

The name of my new book is: Invisible text. Great title don't you think?

Right-click on the words "Invisible text" to see that it is in fact a broken image tag.

A solution for the problem of impossibly long Welsh names is less technically solved. In such cases I would recommend figuring out some discrete way to include the problematic word in plain text as near to the image as possible. So in the case of the little known Welsh town of  Llanfairpwllgwyngyllgogerychwyrndrobwyllllantysiliogogogoch

I ought to point out that the word "Llanfairpwllgwyngyllgogerychwyrndrobwyllllantysiliogogogoch" means "The church of St Mary in the hollow of white hazel trees near the rapid whirlpool by St Tysilio's of the red cave".

Add a comment

The fields that are highlighted contain errors
Name:
Email:     (Not shown)
Website:
Comment:
  Notify me when my comment has been cleared.
  Notify me of followup comments.