Word Wrapping in ImageMagick
Creating an image out of text in ImageMagick is easy, if you have a static string. If, on the other hand, you have a string of varying lengths, and you want to keep the same font size, it’s an entirely different matter.
The requirements:
- Keep box height and width the same
- Keep font size the same
- Incoming string lengths vary
- Add “ellipses” if strings are too long, cut off at readable point (between words)
This simple idea is actually quite trickier than we thought.
newPseudoImage()
This method is very handy. With the “caption” prefixing the string, ImageMagick renders the text as an image, problem is that the font size is determined by the best fit.
setBackgroundColor("transparent");
$words->setOption("fill","white");
$words->newPseudoImage($x,$y, "caption: ".$text);
$im->compositeImage($words, Imagick::COMPOSITE_OVER,0,0);
$im->setImageFormat( "png") ;
header( "Content-Type: image/png" );
echo $im;
?>
AnnotateImage()
The old school way is to use ImageMagick’s AnnotateImage() method. This allows you to create a drawable object with static font size. Yay! But you have to provide it with a string that is already short enough, otherwise the words will scroll right off. Note use of “wordwrap()” method to create new lines at a certain character point. I hard-coded this as 15, based on trial and error. More on this later.
newImage($x,$y,new ImagickPixel("blue"));
$draw = new ImagickDraw();
$draw->setFillColor("white");
$draw->setFontSize("12");
$im->annotateImage($draw, 10,14,0,$text);
$im->setImageFormat( "png") ;
header( "Content-Type: image/png" );
echo $im;
?>
So, we’re almost there. To further leverage wordwrap(), we need to know the character length of the box. To cut the string properly, we need to know how many rows are in the box. Using those, we can snip it appropriately and add the ellipses. There’s still one more handy method in our arsenal. Introducing…
queryFontMetrics()
This method gives you pixel lengths based on a string and a font. With this information we can create either an average character length in pixels, or a ratio that we can apply to the long string, i.e. our string is 3x as big as required, cut it in thirds.
newImage($x,$y,new ImagickPixel("blue"));
$draw = new ImagickDraw();
$draw->setFillColor("white");
$draw->setFontSize(10);
$metrics = $im->queryFontMetrics($draw, $text);
/* how many rows in box? */
$rows = round($y/$metrics['textHeight'])-1; /* less 1 for vertical padding */
/* extend to one long row to compare with metrics */
$single_status_px = $rows * $x;
/* create shorter string by applying ratio */
$px_ratio = round($single_status_px / $metrics["textWidth"],2);
$status_length_char = strlen($text);
$cutoff_char_point = round($status_length_char * $px_ratio);
$short_string = substr($text,0,$cutoff_char_point);
/* cutoff last element, which could be a partial word, and add ellipses */
$words = explode(" ",$short_string);
array_pop($words);
$short_string_final = implode($words, " ");
$short_string_final .= "...";
/* make new lines and add to image */
$short_string_final = wordwrap($short_string_final, round(strlen($short_string_final)/$rows));
$y2= $metrics['textHeight'] + 3;
$im->annotateImage($draw,3,$y2,0,$short_string_final);
$im->setImageFormat( "png") ;
header( "Content-Type: image/png" );
echo $im;
?>
Review
So the solution is a series of steps. First, queryFontMetrics() to find pixel length with applied font, create ratio to dynamic string, use PHP’s wordwrap() function to gracefully find a cutting point, and then use annotateImage() to output the text to the image.
As you can see, it needs some improvement. Getting queryFontMetrics() on each line is CPU intensive (IM calls are) especially if it’s in a loop, adding characters to fill a slotted width. I also played with getting an average character width, but that resulted in uglier results than this.
More Reading
ImageMagick Test Functions
Mikko’s blog post on Typesetting
GD’s imagettfbbox() method is very similar to queryFontMetrics()
cateogories: App Development, Blog, Image processing