Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Text out of the box for converting into image in php

I am trying to convert text into image. I already did it, but the are some cases when text is out of the image boxImage

The "e" of the word "The" is cut. I have tried decreasing the font size or increasing the width of the image, but in some cases this happen again with another text. This is the code:

    $new_line_position = 61;        
    $angle = 0;        
    $left = 20;
    $top = 45;
    $image_width = 1210;
    $image_line_height = 45;                

    $content_input = wordwrap($content_input,    $new_line_position, "\n", true);  

    $lineas = preg_split('/\\n/', $content_input);
    $lines_breaks = count($lineas); 
    $image_height = $image_line_height * $lines_breaks;
    $im = imagecreatetruecolor($image_width, $image_height);

    // Create some colors
    $white = imagecolorallocate($im, 255, 255, 255);        
    $black = imagecolorallocate($im, 0, 0, 0);
    imagefilledrectangle($im, 0, 0, $image_width, $image_height, $white);   

   $font_ttf =  public_path().'/fonts/'.$ttf_font;                     


    foreach($lineas as $linea){
        imagettftext($im, $font_size, $angle, $left, $top, $black, $font_ttf, $linea);        
        $top = $top + $image_line_height;
    }

    // Add the text                        
    imagepng($im);                
    imagedestroy($im);

Thank you.

like image 908
Luis Avatar asked Oct 06 '16 16:10

Luis


2 Answers

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

enter image description here enter image description here

You can also use a monospace font. A monospace is a font whose letters and characters each occupy the same amount of horizontal space.

like image 128
Pawel Wodzicki Avatar answered Sep 23 '22 19:09

Pawel Wodzicki


The problem is that your font is a variable width per letter, but you are truncating based on the number of letters and not the width of the font.

Take the following example, ten "I"s vs ten "W", the second will be over twice as long.

iiiiiiiiii

WWWWWWWWWW

The "simple" option is to use a monospaced font, such as Courier, which is used in the block below:

iiiiiiiiii
WWWWWWWWWW

But that's a boring font!. So what you need is to use the ìmagettfbbox (Image True Type Font Bounding Box" function http://php.net/manual/en/function.imagettfbbox.php) on each line to get the width. You need to run this function one line at a time, in decreasing sizes until you get the size you need.

A pseduo bit of code (please note: written off-hand and not tested, you will need to juggle it to make it perfect):

$targetPixelWidth = 300;
$maximumChactersPerLine = 200;  // Make this larger then you expect, but too large will slow it down!
$textToDisplay = "Your long bit of text goes here"
$aLinesToDisplay = array();
while (strlen(textToDisplay) > 0) {
  $hasTextToShow = false;
  $charactersToTest = $maximumChactersPerLine;
  while (!$hasTextToShow && $maximumChactersPerLine>0) {
    $wrappedText = wordwrap($textToDisplay, $maximumChactersPerLine);
    $aSplitWrappedText = explode("\n", $wrappedText);
    $firstLine = trim($aSplitWrappedText[0]);
    if (strlen($firstLine) == 0) {
      // Fallback to "default"
      $charactersToTest = 0;
    } else {
      $aBoundingBox = imagettfbbox($fontSize, 0, $firstLine, $yourTTFFontFile);
      $width = abs($aBoundingBox[2] - $aBoundingBox[0]);
      if ($width <= $targetPixelWidth) {
        $hasTextToShow = true;
        $aLinesToDisplay[] = $firstLine;
        $textToDisplay = trim(substr($textToDisplay, strlen($firstLine));
      } else {
        --$charactersToTest;
      }
    }
  }
  if (!$hasTextToShow) {
    // You need to handle this by getting SOME text (e.g. first word) and decreasing the length of $textToDisplay, otherwise you'll stay in the loop forever!
    $firstLine = ???; // Suggest split at first "space" character (Use preg_split on \s?) 
    $aLinesToDisplay[] = $firstLine;
    $textToDisplay = trim(substr($textToDisplay, strlen($firstLine));
  }      
}
// Now run the "For Each" to print the lines.

Caveat: The TTF Bounding box function is not perfect either - so allow a bit of "lee way", but you'll still end up with far, far better results that you are doing above (i.e. +-10 pixels). It also depends on the font-file kerning (gaps between letters) information. A bit of Goggling and reading the comments in the manual will help you get more accurate results if you need it.

You should also optimize the function above (start with 10 characters and increase, taking the last string that fits may get you an faster answer over decreasing until something fits, and reduce the number of strlen calls for example).


Addendum in response to comment "Can you expand on "the TTF Bounding box function is not perfect either"?" (reply is too long for a comment)

The function relies on the "kerning" information in the font. For example, you want V to sit closer to A (VA - see how they "overlap" slightly) than you would V and W (VW - see how the W starts after the V). There are lots of rules embedded in the fonts regarding that spacing. Some of those rules also say "I know the 'box' starts at 0, but for this letter you need to start drawing at -3 pixels".

PHP does it's best to read the rules, but gets some wrong sometimes and therefore gives you the wrong dimensions. It's the reason as why you might tell PHP to write from "0,0" but it actually starts at "-3,0" and appears to cut off the font. The easiest solution is to allow a few pixels grace.

Yes, it's a well noted "issue" (https://www.google.com/webhp?q=php%20bounding%20box%20incorrect)

like image 21
Robbie Avatar answered Sep 21 '22 19:09

Robbie