The problem is that every single character may have a slightly different width, for example W
and i
. Becuase of that you cannot split a string by letter count per line you need a more accurate method.
The main trick is to use
imagettfbbox
which gives the bounding box of a text using TrueType fonts and from this, you can get the real width that text will use.
Here is a function for pixel perfect split found at http://php.net/manual/en/function.wordwrap.php
use it instead of wordwrap and pass extra values like image width, font-size and font path
<?php
/**
* Wraps a string to a given number of pixels.
*
* This function operates in a similar fashion as PHP's native wordwrap function; however,
* it calculates wrapping based on font and point-size, rather than character count. This
* can generate more even wrapping for sentences with a consider number of thin characters.
*
* @static $mult;
* @param string $text - Input string.
* @param float $width - Width, in pixels, of the text's wrapping area.
* @param float $size - Size of the font, expressed in pixels.
* @param string $font - Path to the typeface to measure the text with.
* @return string The original string with line-breaks manually inserted at detected wrapping points.
*/
function pixel_word_wrap($text, $width, $size, $font)
{
# Passed a blank value? Bail early.
if (!$text)
return $text;
# Check if imagettfbbox is expecting font-size to be declared in points or pixels.
static $mult;
$mult = $mult ?: version_compare(GD_VERSION, '2.0', '>=') ? .75 : 1;
# Text already fits the designated space without wrapping.
$box = imagettfbbox($size * $mult, 0, $font, $text);
if ($box[2] - $box[0] / $mult < $width)
return $text;
# Start measuring each line of our input and inject line-breaks when overflow's detected.
$output = '';
$length = 0;
$words = preg_split('/\b(?=\S)|(?=\s)/', $text);
$word_count = count($words);
for ($i = 0; $i < $word_count; ++$i) {
# Newline
if (PHP_EOL === $words[$i])
$length = 0;
# Strip any leading tabs.
if (!$length)
$words[$i] = preg_replace('/^\t+/', '', $words[$i]);
$box = imagettfbbox($size * $mult, 0, $font, $words[$i]);
$m = $box[2] - $box[0] / $mult;
# This is one honkin' long word, so try to hyphenate it.
if (($diff = $width - $m) <= 0) {
$diff = abs($diff);
# Figure out which end of the word to start measuring from. Saves a few extra cycles in an already heavy-duty function.
if ($diff - $width <= 0)
for ($s = strlen($words[$i]); $s; --$s) {
$box = imagettfbbox($size * $mult, 0, $font, substr($words[$i], 0, $s) . '-');
if ($width > ($box[2] - $box[0] / $mult) + $size) {
$breakpoint = $s;
break;
}
}
else {
$word_length = strlen($words[$i]);
for ($s = 0; $s < $word_length; ++$s) {
$box = imagettfbbox($size * $mult, 0, $font, substr($words[$i], 0, $s + 1) . '-');
if ($width < ($box[2] - $box[0] / $mult) + $size) {
$breakpoint = $s;
break;
}
}
}
if ($breakpoint) {
$w_l = substr($words[$i], 0, $s + 1) . '-';
$w_r = substr($words[$i], $s + 1);
$words[$i] = $w_l;
array_splice($words, $i + 1, 0, $w_r);
++$word_count;
$box = imagettfbbox($size * $mult, 0, $font, $w_l);
$m = $box[2] - $box[0] / $mult;
}
}
# If there's no more room on the current line to fit the next word, start a new line.
if ($length > 0 && $length + $m >= $width) {
$output .= PHP_EOL;
$length = 0;
# If the current word is just a space, don't bother. Skip (saves a weird-looking gap in the text).
if (' ' === $words[$i])
continue;
}
# Write another word and increase the total length of the current line.
$output .= $words[$i];
$length += $m;
}
return $output;
}
;
?>
Below is working code example:
I modified this function pixel_word_wrap
a little bit. Also, modified some calculation in your code. Right now is giving me the perfect image with correctly calculated margins. I am not super happy with the code noticed that there is a $adjustment variable, that should be bigger when you use bigger font-size. I think It's down to imperfection in imagettfbbox
function. But It's a practical approach that works pretty well with most font-sizes.
<?php
$angle = 0;
$left_margin = 20;
$top_margin = 20;
$image_width = 1210;
$image_line_height = 42;
$font_size = 32;
$top = $font_size + $top_margin;
$font_ttf = './OpenSans-Regular.ttf';
$text = 'After reading Mr. Gatti`s interview I finally know what bothers me so much about his #elenaFerrante`s unamsking. The whole thing is about him, not the author, not the books, just himself and his delusion of dealing with some sort of unnamed corruption';$adjustment= $font_size *2; //
$adjustment= $font_size *2; // I think because imagettfbbox is buggy adding extra adjustment value for text width calculations,
function pixel_word_wrap($text, $width, $size, $font) {
# Passed a blank value? Bail early.
if (!$text) {
return $text;
}
$mult = 1;
# Text already fits the designated space without wrapping.
$box = imagettfbbox($size * $mult, 0, $font, $text);
$g = $box[2] - $box[0] / $mult < $width;
if ($g) {
return $text;
}
# Start measuring each line of our input and inject line-breaks when overflow's detected.
$output = '';
$length = 0;
$words = preg_split('/\b(?=\S)|(?=\s)/', $text);
$word_count = count($words);
for ($i = 0; $i < $word_count; ++$i) {
# Newline
if (PHP_EOL === $words[$i]) {
$length = 0;
}
# Strip any leading tabs.
if (!$length) {
$words[$i] = preg_replace('/^\t+/', '', $words[$i]);
}
$box = imagettfbbox($size * $mult, 0, $font, $words[$i]);
$m = $box[2] - $box[0] / $mult;
# This is one honkin' long word, so try to hyphenate it.
if (($diff = $width - $m) <= 0) {
$diff = abs($diff);
# Figure out which end of the word to start measuring from. Saves a few extra cycles in an already heavy-duty function.
if ($diff - $width <= 0) {
for ($s = strlen($words[$i]); $s; --$s) {
$box = imagettfbbox($size * $mult, 0, $font,
substr($words[$i], 0, $s) . '-');
if ($width > ($box[2] - $box[0] / $mult) + $size) {
$breakpoint = $s;
break;
}
}
}
else {
$word_length = strlen($words[$i]);
for ($s = 0; $s < $word_length; ++$s) {
$box = imagettfbbox($size * $mult, 0, $font,
substr($words[$i], 0, $s + 1) . '-');
if ($width < ($box[2] - $box[0] / $mult) + $size) {
$breakpoint = $s;
break;
}
}
}
if ($breakpoint) {
$w_l = substr($words[$i], 0, $s + 1) . '-';
$w_r = substr($words[$i], $s + 1);
$words[$i] = $w_l;
array_splice($words, $i + 1, 0, $w_r);
++$word_count;
$box = imagettfbbox($size * $mult, 0, $font, $w_l);
$m = $box[2] - $box[0] / $mult;
}
}
# If there's no more room on the current line to fit the next word, start a new line.
if ($length > 0 && $length + $m >= $width) {
$output .= PHP_EOL;
$length = 0;
# If the current word is just a space, don't bother. Skip (saves a weird-looking gap in the text).
if (' ' === $words[$i]) {
continue;
}
}
# Write another word and increase the total length of the current line.
$output .= $words[$i];
$length += $m;
}
return $output;
}
$out = pixel_word_wrap($text, $image_width -$left_margin-$adjustment,
$font_size, $font_ttf);
$lineas = preg_split('/\\n/', $out);
$lines_breaks = count($lineas);
$image_height = $image_line_height * $lines_breaks;
$im = imagecreatetruecolor($image_width, $image_height + $top);
// Create some colors
$white = imagecolorallocate($im, 255, 255, 255);
$black = imagecolorallocate($im, 0, 0, 0);
imagefilledrectangle($im, 0, 0, $image_width, $image_height + $top, $white);
foreach ($lineas as $linea) {
imagettftext($im, $font_size, $angle, $left_margin, $top, $black, $font_ttf,
$linea);
$top = $top + $image_line_height;
}
header('Content-Type: image/png');
imagepng($im);
Here is an example
You can also use a monospace font. A monospace is a font whose letters and characters each occupy the same amount of horizontal space.