TextImage Helper
Quick Start
The code is quite well documented, so if you're in a hurry, you'll find the code at the bottom of this article.
What does it do?
If you want to use a non-standard typeface for dynamic content on your site, you have a number of options.
You could use Flash - maybe in the form of sIFR http://www.mikeindustries.com/sifr/ - but maybe you don't want to rely on a plugin and/or javascript.
The alternative is to use the GD functions in PHP. There are plenty of tutorials and code snippets lying around which explain how to do this, but in my experience you have to a lot of tweaking to get the results you want in a real-world project.
This Helper encapsulates the GD text-rendering functionality for Cake and provides a number of convenient features including:
- Automatic baseline-alignment
- Automatic word-wrapping
- Rendering against a transparent background
- Control over aliasing quality
- Anti-aliasing toggle
- Control over padding
- Simple caching
Basic Usage
'Install' as follows:- Save the helper as 'text_image.php' in your helpers folder. The code is at the bottom of this article.
- Include the helper in your controller.
- Put the font file which you want to use in the folder where the helper looks for fonts, by default that's a folder called '/app/fonts'. Make sure the relevant font license allows you to do this. I would also recommend password-protecting this folder. You can use truetype, opentype or postscript fonts.
- Create a subfolder called 'dynimg' in your '/app/webroot/img' folder.
Now, in your views, you can do something like this:
Download code
$textImage->fontFile = "bodoni.ttf";
$textImage->setPointSize(32);
echo $textImage->image("Hello World: 32Pt");
And if you're lucky you'll get something like this:

Where are the images saved?
By default the image rendered will be named something like 'hello_world_32pt.png' and stored in the dynimg directory. If you want to save the images in different directories you can specify the directory to save in (before rendering anything), using the setImageDirectory method. For example:
Download code
$textImage->setImageDirectory('dynimg/headlines');
$textImage->setPointSize(32);
echo $textImage->image("Homepage");
$textImage->setImageDirectory('dynimg/navigation');
$textImage->setPointSize(14);
echo $textImage->image("Homepage");
The directory is always relative to the /webroot/img directory. It's often a good idea to put different kinds of dynamically-rendered images in different subdirectories. For example, if you have a dynamically-rendered headline called 'Contact' rendered at 24 points and a navigation point 'Contact' rendered at 12 points - you will want them in separate directories so they don't mutually overwrite each other.
Advanced Usage
Baseline-alignment
This is one of the most important features for me. Basically it means that when you render a series of text-images, and then place them side-by-side, the 'baseline' of all the images will line up. Without this feature a menu might look like this:

- With the TextImageHelper, you would get something more like this:

The Helper supports baseline-alignment by first measuring the maximum possible height for text in the given font, at the given point size, and then sizing the image and positioning the text accordingly. Baseline-alignment is turned on by default. To turn it off, use:
Download code
$textHelper->baseAlign = false
Word wrapping
You can create images with multiple lines of text which automatically wrap to a specified width. To do this, use the optional second '$wrapWidth' parameter of the $textImage->image() function. The default value of 0 means no wrapping. Set it to a positive integer and the helper will try and wrap the text to that pixel width:
Download code
echo $textImage->image('... Nulla vulputate, mi sed vulputate faucibus ...', 200)
- Will generate something like this:

Additional parameters for the image function
The $textImage->image() method also has optional third and fourth parameters. The third parameter is an explicit name for the image file to be created - if this isn't specified then the name will be based on the text to be rendered. The fourth parameter is an array of html attributes to be attached to the image tag. Note that if an alt attribute is not specified, the alt text will be the text to be rendered.
Control over padding
Often you will find that text is being clipped by the GD Module - this varies from font to font. Here's an example with the free font epilog from http://fonts.tom7.com/

- here the text is being clipped at the top
By calling the $textImage->setPadding($top, $right, $bottom, $left) method before rendering, you can prevent such clipping by manually adding some extra pixels to the edges of your images.: Download code
$textImage->setPadding(7, 0, 0, 0);

Simple caching
The Helper has a $cache attribute. By default it is set to -1 (no caching). Set it to 0 for permanent caching, or to a positive integer to cache for n seconds. So
Download code
$textImage->cache = 3600;
will cache images for an hour.
Aliasing control
Sometimes, particularly when rendering at smaller point sizes, the quality of the images can be inadequate. The helper uses a simple technique to provide some control over rendering quality. Basically you the helper can be configured to automatically render at a significantly larger point size and then scale down the image to the required size. - The idea is that you rely on the superior accuracy of the scaling algorithm to the font rendering one. Note there is also a performance penalty, so only do this if you need to. The attribute to set is $textImage->soften Factor. By default it is 0. Here is an example of some text that's not rendering so well at 16pt:

Now with some softening:
Download code
$textImage->softenFactor = 4;
You get:

The result is certainly more balanced but it is blurrier, and PHP has had some extra work to do in the background - so it's a trade-off.
Other features
To render against a transparent background:
Download code
$textImage->transparent = true;
To turn anti-aliasing off (For example, if you're rendering a pixel font and its blurry you might want to do this):
Download code
$textImage->aliasing = true;
To set the text color:
Download code
$textImage->setColor("FF0000");
To set the image background color:
Download code
$textImage->setBgColor("CCCCCC");
To adjust linespacing for multiline text:
Download code
$textImage->linespacing = 1.2;
- This value is a float. 1 is about 'normal'. As you get closer to 0, lines will start overlapping.
Theoretically the helper supports the rendering of .gifs as well as .pngs - but I haven't tested this on a version of PHP which supports GIF generation from the GD Module. If it does work, this is how you turn it on:
Download code
$textImage->type = TEXT_IMAGE_GIF;
Real World Usage
I've used versions of this code on several cake production sites this year. For example http://www.zahns.com. - The headlines, subheadlines and the navigation are all generated using this helper (although not on the front page which is flash).
When using the TextImage helper on real projects I've tended to add another layer of wrapping around it by creating functions in a site-specific helper. This is to avoid repetitive and scattered code. So the configuration of the TextImage helper to render a headline is written only once, in one place. Here's an example, pretty much straight from www.zahns.com. There are functions for rendering three types of text. These in turn access a private function which does some standard configuration of the TextImage helper before rendering the text appropriately.
Download code
<?php
/**
* Example Helper class with text rendering functions specific to MySite
*/
class MySiteHelper extends Helper
{
var $helpers = array('TextImage');
/**
* Render text as an image in the headline typeface
* And return the image as an HTML tag.
*/
function renderHeadline($text)
{
return $this->__renderText($text, 48, "headlines", 3, array(1,0,-4,0));
}
function renderSub($text, $maxWidth = 270)
{
return $this->__renderText($text, 24, "subheadlines", 5, array(0,0,-4,0), $maxWidth);
}
function renderSmall($text, $maxWidth = 270)
{
$this->TextImage->linespacing = 1;
return $this->__renderText($text, 14, "small_text", 5, array(0,0,0,0), $maxWidth);
}
function __renderText($text, $ps, $dir, $softenFactor, $margins, $maxWidth = FULL_WIDTH)
{
$this->TextImage->setImageDirectory("dynimg/$dir/");
$this->TextImage->fontFile = 'myFont.ttf';
$this->TextImage->softenFactor = $softenFactor;
$this->TextImage->setPointSize($ps);
$this->TextImage->setColor(333333);
$this->TextImage->setPadding($margins[0], $margins[1],$margins[2],$margins[3]);
return $this->TextImage->image($text, $maxWidth);
}
}
And finally, the helper itself:
Download code
<?php
uses('sanitize');
/**
* Helper class for generating text as images dynamically.
* Font file should exist in /app/fonts/
* Respect font licenses.
*
* for more information, see the article in the Bakery:
* http://bakery.cakephp.org/articles/view/131
*
* @version 1.0
* @copyright Copyright (c) 2007, Rob Meek
* @author Rob Meek
* License: MIT
*
* example usage:
*
* $textImage->fontFile = "bodoni.ttf";
* $textImage->setPointSize(32);
* $textImage->image("Hello World");
*
*/
define('TEXT_IMAGE_PNG', 0);
define('TEXT_IMAGE_GIF', 1);
class TextImageHelper extends Helper
{
/**
* Name of the font file to use
* default is Verdana
* @var string
* @access public
*/
var $fontFile = "Verdana.TTF";
/**
* Point size to render at
* default is 24
* best set using setPointSize()
* @var int
* @access public
*/
var $pointSize = 24;
/**
* Color to render text in
* default is black
* best set using setColor()
* @var array
* @access public
*/
var $fgColor = array('r'=>0, 'g'=>0, 'b'=>0);
/**
* Background color
* default is white
* best set using setBgColor()
* @var array
* @access public
*/
var $bgColor = array('r'=>255, 'g'=>255, 'b'=>255);
/**
* Type of bitmap to render, PNG or GIF. Default PNG (TEXT_IMAGE_PNG)
* @var int
* @access public
*/
var $type = TEXT_IMAGE_PNG;
/**
* Caching control.
* -1 = no cacheing.
* 0 = cache permanently
* any positive number = cache for n seconds
* @var int
* @access public
*/
var $cache = -1;
/**
* Turn aliasing on / off - useful for rendering pixel fonts with aliasing
* Note: the default setting of false means soft contours
* @var boolean
* @access public
*/
var $aliasing = false;
/**
* Soften contours by rendering big and scaling down - can improve
* perceived rendering quality, particularily at smaller point sizes
* set to 0 for no softening. Avoid high values (more than 6 would be crazy)
* @var int
* @access public
*/
var $softenFactor = 0;
/**
* render a transparent background
* @var boolean
* @access public
*/
var $transparent = false;
/**
* padding around the text
* best set with the setPadding function
* @var int
* @access public
*/
var $padTop = 0;
var $padRight = 0;
var $padBottom = 0;
var $padLeft = 0;
/**
* Linespacing setting for images with multiple lines of text
* @var float
* @access public
*/
var $lineSpacing = 0.8;
/**
* Render an image sized for the maximum ascent and descent of the font
* and position the text baseline accordingly.
* @var boolean
* @access public
*/
var $baseAlign = true;
/**
* Path to where dynamic text images will be stored.
* @var string
*/
var $__imagePath = 'dynimg';
/**
* Path to the font files to use relative to APP
* @var string
*/
var $__fontPath = 'fonts';
var $helpers = array('Html');
/**
* Return a full image tag for the given text.
* @param string $text the text to be rendered
* @param int $wrapWidth a pixel width to wrap at. 0 means no wrapping
* @param string $imgName specify an optional filename for the image -
* otherwise the filename will be formed automatically from the text
* @param string $htmlAttr html attributes for the image tag.
*/
function image($text, $wrapWidth = 0, $imgName = null, $htmlAttr = null)
{
$imgAttr = $this->getImageAttributes($text, $wrapWidth, $imgName);
$src = $imgAttr['src'];
unset($imgAttr['src']);
$sani = new Sanitize();
$imgAttr['alt'] = $sani->html($text);
if ($htmlAttr)
{
$imgAttr = array_merge($imgAttr, $htmlAttr);
}
return $this->Html->image($src, $imgAttr);
}
/**
* Return an array of valid attributes for the given text with the keys
* 'src', 'height' and 'width'.
* @param string $text the text to be rendered
* @param int $wrapWidth a pixel width to wrap at. 0 means no wrapping
* @param string $imgName specify an optional filename for the image -
* otherwise the filename will be formed automatically from the text
*/
function getImageAttributes($text, $wrapWidth = 0, $imgName = null)
{
if (!$imgName)
{
$imgName = $this->cleanFilename($text)
}
$suffix = ($this->type == TEXT_IMAGE_PNG)?".png":".gif";
$path = IMAGES_URL . $this->__imagePath . DS . $imgName . $suffix;
$generate = false;
$created = @filemtime($path); // false if file does not exist
if ($this->cache == -1 || $created === false)
{
$generate = true;
}
else if ($this->cache > 0)
{
$generate = ((time() - $created) > $this->cache);
}
if ($generate)
{
$this->generate($text, $path, $wrapWidth);
}
$src = $this->__imagePath . "/" . $imgName . $suffix;
$imgSize = getimagesize($path);
$result = array("src" => $src, "width" => $imgSize[0], "height" => $imgSize[1]);
return $result;
}
/**
* Generate a rendered image and save it to disk
* @param string $text the text to be rendered
* @param string $filename specify the filename for the image
* @param int $wrapWidth a pixel width to wrap at. 0 means no wrapping
*/
function generate($text, $filename, $wrapWidth = 0)
{
$ps = $this->pointSize;
$top = $this->padTop;
$bottom = $this->padBottom;
$left = $this->padLeft;
$right = $this->padRight;
if ($this->softenFactor > 0)
{
$ps *= $this->softenFactor;
$top *= $this->softenFactor;
$bottom *= $this->softenFactor;
$left *= $this->softenFactor;
$right *= $this->softenFactor;
}
$font = APP . $this->__fontPath . DS . $this->fontFile;
if (!file_exists($font))
{
echo "font file not found: " . $font;
return;
}
// extended parameters for gd/freetype
$xtraParamArr = array('linespacing' => $this->lineSpacing);
// do hard wrap if required
$numLines = 1;
if ($wrapWidth != 0)
{
$text = $this->__hardwrap($text, $wrapWidth, $this->pointSize, $font, $xtraParamArr);
$numLines = count(explode("\n", $text));
}
// try to calculate an optimal image height
// for this point size and font
// by rendering a text with big ascenders and descenders
$testText = "gyT§?_";
if ($wrapWidth != 0)
{
$testText = implode("\n", array_fill(0, $numLines, $testText));
}
$maxSizeArr = imageftbbox($ps, 0, $font, $testText, $xtraParamArr);
$minY = min($maxSizeArr[5], $maxSizeArr[7]);
$maxY = max($maxSizeArr[1], $maxSizeArr[3]);
$fontHeight = $maxY - $minY;
$imageH = $fontHeight + $top + $bottom;
// calculate baseline using a string with big ascender
$baselineArr = imageftbbox($ps, 0, $font, "Ül", $xtraParamArr);
$baselineY = (max($baselineArr[5], $baselineArr[7])) - $top;
// calculate image dimensions
$textSizeArr = imageftbbox($ps, 0, $font, $text, $xtraParamArr);
// process image dimenstions
$minX = min($textSizeArr[0], $textSizeArr[6]);
$maxX = max($textSizeArr[2], $textSizeArr[4]);
$textW = ($maxX - $minX) + 2;
if (!$this->baseAlign)
{
$minY = min($textSizeArr[5], $textSizeArr[7]);
$maxY = max($textSizeArr[1], $textSizeArr[3]);
$imageH = $maxY - $minY + $top + $bottom;
$baselineY = (max($textSizeArr[5], $textSizeArr[7])) - $top;
}
// make image
$width = $textW; //max($wrapWidth, $textW);
$width += ($left + $right);
$im = imagecreatetruecolor($width, $imageH);
// define colors
$backCol = imagecolorallocate($im, $this->bgColor['r'], $this->bgColor['g'], $this->bgColor['b']);
if ($this->transparent)
{
imagecolortransparent($im, $backCol);
}
else
{
imagefill($im, 0, 0, $backCol);
}
$textCol = imagecolorallocate ($im, $this->fgColor['r'], $this->fgColor['g'], $this->fgColor['b']);
// render text
$col = $textCol;
if ($this->aliasing)
{
$col = 0 - $col;
if ($col == 0)
{
$col = -1;
}
}
imagefttext($im, $ps, 0, (1-$minX) + $left, -1 - $baselineY,
$col, $font, $text,
$xtraParamArr);
// save image to disk
if ($this->softenFactor > 0)
{
$im2=imagecreatetruecolor($width / $this->softenFactor, $imageH / $this->softenFactor);
// make real size image
imagecopyresampled($im2,$im,0,0,0,0,imagesx($im2),imagesy($im2),imagesx($im),imagesy($im));
if ($this->type == TEXT_IMAGE_PNG)
{
imagepng($im2, $filename);
}
else
{
imagegif($im2, $filename);
}
}
else
{
if ($this->type == TEXT_IMAGE_PNG)
{
imagepng($im, $filename);
}
else
{
imagegif($im, $filename);
}
}
// destroy images
@imagedestroy($im);
if ($this->softenFactor)
{
@imagedestroy($im2);
}
}
/**
* Set the directory where dynamic text images
* are stored - relative to the /webroot/img folder
* directory will be constructed if it doesn't exist
* but without recursion in PHP4
*/
function setImageDirectory($directory)
{
$this->__imagePath = $directory;
if (!file_exists(IMAGES_URL . $this->__imagePath))
{
if (phpversion() < 5)
{
mkdir(IMAGES_URL . $this->__imagePath);
}
else
{
mkdir(IMAGES_URL . $this->__imagePath, 0777, true);
}
}
}
/**
*
*/
function setFontPath($fontPath)
{
$this->__fontPath = $fontPath;
}
/**
* Set the point size to render at
*/
function setPointSize($pointSize)
{
$this->pointSize = $pointSize / 96 * 72;
}
/**
* Set the foreground color for the images in hex e.g. 0xFF0000
*/
function setColor($color)
{
$rgb = hexdec($color);
$this->fgColor['r'] = ($rgb & 0xFF0000) >> 16;
$this->fgColor['g'] = ($rgb & 0xFF00) >> 8;
$this->fgColor['b'] = ($rgb & 0xFF);
}
/**
* Set the background color for the images in hex e.g. 0xCCCCCC
*/
function setBgColor($color)
{
$rgb = hexdec($color);
$this->bgColor['r'] = ($rgb & 0xFF0000) >> 16;
$this->bgColor['g'] = ($rgb & 0xFF00) >> 8;
$this->bgColor['b'] = ($rgb & 0xFF);
}
/**
* Set pixel padding around the rendered text
* useful if clipping is occurring
*/
function setPadding($top, $right, $bottom, $left)
{
$this->padTop = $top;
$this->padRight = $right;
$this->padBottom = $bottom;
$this->padLeft = $left;
}
/**
* wrap a text for a specific width
* inserts hard returns into the returned string
*/
function __hardwrap($text, $wrapWidth, $ptSize, $font, $xtraParamArr)
{
$text .= " ";
$spaces = array();
$widths = array();
$i = 0;
// measure the widths of all the words
while(true)
{
$nextSpace = strpos(substr($text,$i)," ");
if(!($nextSpace === false))
{
$spaces[] = $nextSpace + $i;
$bbox = imageftbbox($ptSize, 0, $font, substr($text, $i, $nextSpace + 1), $xtraParamArr);
$left = ($bbox[0] > $bbox[6])?$bbox[6]:$bbox[0];
$right = ($bbox[2] > $bbox[4])?$bbox[2]:$bbox[4];
$widths[]= $right - $left;
$i = $nextSpace + $i + 1;
}
else
break;
}
$lastspace =- 1;
$lineWidth = 0;
$result = "";
for ($i = 0; $i < count($spaces); $i++)
{
if((($lineWidth > 0) && ($lineWidth + $widths[$i]) > $wrapWidth)) // time for a line break
{
// wrap
$result .= "\r\n";
$lineWidth = 0;
$i--;
}
else
{
// add a word to line
// we'll always get at least one word (even if too wide) thanks to
// ($lineWidth > 0) test above
$result .= substr($text, $lastspace + 1, $spaces[$i] - $lastspace);
$lineWidth += $widths[$i];
$lastspace = $spaces[$i];
}
}
return $result;
}
function cleanFilename($str)
{
uses('sanitize');
$cleaner = new Sanitize();
$str = str_replace(' ', '_', $str);
$str = str_replace('&', '_and_', $str);
$str = str_replace('/', '_', $str);
$str = str_replace('\\', '_', $str);
$str = str_replace('ä', 'ae', $str);
$str = str_replace('Ä', 'Ae', $str);
$str = str_replace('ü', 'ue', $str);
$str = str_replace('Ü', 'Ue', $str);
$str = str_replace('ö', 'oe', $str);
$str = str_replace('Ö', 'oe', $str);
$str = str_replace('ß', 'ss', $str);
$str = $cleaner->paranoid($str, array('_','-'));
$str = str_replace('__', '_', $str);
return strtolower($str);
}
}
?>
Comments
Bug
1 Some little bugs
1) in the helper file on line 181 there sould be $imgName = $this->cleanFilename($text), not $imgName = cleanFilename($text) as it is now.
2)in mysitehelper, you use $this->TextImage->setFontFile and $this->TextImage->setMargins; these functions aren't defined in the TextImageHelper.
Thank you, and keep the good work!
Bug
2 and one more
This plus a ; at the end of the line :D
Comment
3 Nice work