Skip to content

Instantly share code, notes, and snippets.

@jubstuff
Created April 11, 2015 18:22
Show Gist options
  • Save jubstuff/633b07aa5aa3cdc14a4b to your computer and use it in GitHub Desktop.
Save jubstuff/633b07aa5aa3cdc14a4b to your computer and use it in GitHub Desktop.
Wrap text in imagick
<?php
/**
* Auto Fit Text To Image
*
* Write text to image using a target width, height, and starting font size.
*
* @author Clif Griffin
* @url http://cgd.io/2014/auto-fit-text-to-an-image-with-php-and-wordpress
*
* @access public
* @param bool $canvas_image_filename (default: false) The base image.
* @param string $dest_filename (default: 'output.jpg') The output filename.
* @param string $text (default: '') The text being written.
* @param int $starting_font_size (default: 60) The starting (max) font size.
* @param int $max_height (default: 500) The maximum height in lines of text.
* @param int $x_pos (default: 0) X position in pixels for first line of text.
* @param int $y_pos (default: 0) Y position in pixels for first line of text.
* @param bool $font_file (default: false) Path to font file (.ttf)
* @param string $font_color (default: 'black') The color. Also accepts rgba values. Example: "rgba(0,0,0,0.5)"
* @param int $line_height_ratio (default: 1) Allows scaling the line height. Should be between 0.1 and 1.
* @param string $dest_format (default: 'jpg') Output format.
* @return bool True: success, False: failure
*/
function autofit_text_to_image( $canvas_image_filename = false, $dest_filename = 'output.jpg', $text = '', $starting_font_size = 60, $max_width = 500, $max_height = 500, $x_pos = 0, $y_pos = 0, $font_file = false, $font_color = 'black', $line_height_ratio = 1, $dest_format = 'jpg' ) {
// Bail if any essential parameters are missing
if ( ! $canvas_image_filename || ! $dest_filename || empty($text) || ! $font_file || empty($font_color) || empty($max_width) || empty($max_height) ) return false;
// Do we have a valid canvas image?
if ( ! file_exists($canvas_image_filename) ) return;
$canvas_handle = fopen( $canvas_image_filename, 'rb' );
// Load image into Imagick
$NewImage = new Imagick();
$NewImage->readImageFile($canvas_handle);
// Instantiate Imagick utility objects
$draw = new ImagickDraw();
$pixel = new ImagickPixel( $font_color );
// Load Font
$font_size = $starting_font_size;
$draw->setFont($font_file);
$draw->setFontSize($font_size);
// Holds calculated height of lines with given font, font size
$total_height = 0;
// Run until we find a font size that doesn't exceed $max_height in pixels
while ( 0 == $total_height || $total_height > $max_height ) {
if ( $total_height > 0 ) $font_size--; // we're still over height, decrement font size and try again
$draw->setFontSize($font_size);
// Calculate number of lines / line height
// Props users Sarke / BMiner: http://stackoverflow.com/questions/5746537/how-can-i-wrap-text-using-imagick-in-php-so-that-it-is-drawn-as-multiline-text
$words = preg_split('%\s%', $text, -1, PREG_SPLIT_NO_EMPTY);
$lines = array();
$i = 0;
$line_height = 0;
while ( count($words) > 0 ) {
$metrics = $NewImage->queryFontMetrics( $draw, implode(' ', array_slice($words, 0, ++$i) ) );
$line_height = max( $metrics['textHeight'], $line_height );
if ( $metrics['textWidth'] > $max_width || count($words) < $i ) {
$lines[] = implode( ' ', array_slice($words, 0, --$i) );
$words = array_slice( $words, $i );
$i = 0;
}
}
$total_height = count($lines) * $line_height * $line_height_ratio;
if ( $total_height === 0 ) return false; // don't run endlessly if something goes wrong
}
// Writes text to image
for( $i = 0; $i < count($lines); $i++ ) {
$NewImage->annotateImage( $draw, $x_pos, $y_pos + ($i * $line_height * $line_height_ratio), 0, $lines[$i] );
}
$NewImage->setImageFormat($dest_format);
$result = $NewImage->writeImage($dest_filename);
return $result;
}
<?php
// http://codereview.stackexchange.com/questions/33424/performance-of-autofit-text-method-using-imagick-is-horrible-unfortunately
function fitImageAnnotation( Imagick $image, ImagickDraw $draw, $text, $maxHeight, $leading = 1, $strokeWidth = 0.04, $margins = array( 10, 10, 10, 10 ) )
{
if( strlen( $text ) < 1 )
{
return;
}
$imageWidth = $image->getImageWidth();
$imageHeight = $image->getImageHeight();
// margins are css-type margins: T, R, B, L
$boundingBoxWidth = $imageWidth - $margins[ 1 ] - $margins[ 3 ];
$boundingBoxHeight = $imageHeight - $margins[ 0 ] - $margins[ 2 ];
// We begin by setting the initial font size
// to the maximum allowed height, and work our way down
$fontSize = $maxHeight;
$textLength = strlen( $text );
// Start the main routine where the culprits are
do
{
$probeText = $text;
$probeTextLength = $textLength;
$lines = explode( "\n", $probeText );
$lineCount = count( $lines );
$draw->setFontSize( $fontSize );
$draw->setStrokeWidth( $fontSize * $strokeWidth );
$fontMetrics = $image->queryFontMetrics( $draw, $probeText, true );
// This routine will try to wordwrap() text until it
// finds the ideal distibution of words over lines,
// given the current font size, to fit the bounding box width
// If it can't, it will fall through and the parent
// enclosing routine will try a smaller font size
while( $fontMetrics[ 'textWidth' ] >= $boundingBoxWidth )
{
// While there's no change in line lengths
// decrease wordwrap length (no point in
// querying font metrics if the dimensions
// haven't changed)
$lineLengths = array_map( 'strlen', $lines );
do
{
$probeText = wordwrap( $text, $probeTextLength );
$lines = explode( "\n", $probeText );
// This is one of the performance culprits
// I was hoping to find some kind of binary
// search type algorithm that eliminates
// the need to decrease the length only
// one character at a time
$probeTextLength--;
}
while( $lineLengths === array_map( 'strlen', $lines ) && $probeTextLength > 0 );
// Get the font metrics for the current line distribution
$fontMetrics = $image->queryFontMetrics( $draw, $probeText, true );
if( $probeTextLength <= 0 )
{
break;
}
}
// Ignore font metrics textHeight, we'll calculate our own
// based on our $leading argument
$lineHeight = $leading * $fontSize;
$lineSpacing = ( $leading - 1 ) * $fontSize;
$lineCount = count( $lines );
$textHeight = ( $lineCount * $fontSize ) + ( ( $lineCount - 1 ) * $lineSpacing );
// This is the other performance culprit
// Here I was also hoping to find some kind of
// binary search type algorithm that eliminates
// the need to decrease the font size only
// one pixel at a time
$fontSize -= 1;
}
while( $textHeight >= $maxHeight || $fontMetrics[ 'textWidth' ] >= $boundingBoxWidth );
// The remaining part is no culprit, it just draws the final text
// based on our calculated parameters
$fontSize = $draw->getFontSize();
$gravity = $draw->getGravity();
if( $gravity < Imagick::GRAVITY_WEST )
{
$y = $margins[ 0 ] + $fontSize + $fontMetrics[ 'descender' ];
}
else if( $gravity < Imagick::GRAVITY_SOUTHWEST )
{
$y = $margins[ 0 ] + ( $boundingBoxHeight / 2 ) - ( $textHeight / 2 ) + $fontSize + $fontMetrics[ 'descender' ];
}
else
{
$y = ( $imageHeight - $textHeight - $margins[ 2 ] ) + $fontSize;
}
$alignment = $gravity - floor( ( $gravity - .5 ) / 3 ) * 3;
if( $alignment == Imagick::ALIGN_LEFT )
{
$x = $margins[ 3 ];
}
else if( $alignment == Imagick::ALIGN_CENTER )
{
$x = $margins[ 3 ] + ( $boundingBoxWidth / 2 );
}
else
{
$x = $imageWidth - $margins[ 1 ];
}
$draw->setTextAlignment( $alignment );
$draw->setGravity( 0 );
foreach( $lines as $line )
{
$image->annotateImage( $draw, $x, $y, 0, $line );
$y += $lineHeight;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment