【问题标题】:Text out of the box for converting into image in php开箱即用的文本,用于在 php 中转换为图像
【发布时间】:2017-02-15 12:41:11
【问题描述】:

我正在尝试将文本转换为图像。我已经这样做了,但是有些情况下文本超出了图像框

单词“The”的“e”被删减。我曾尝试减小字体大小或增加图像的宽度,但在某些情况下,另一种文本会再次发生这种情况。这是代码:

    $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);

谢谢。

【问题讨论】:

    标签: php fonts gd truetype imagettftext


    【解决方案1】:

    问题在于每个字符的宽度可能略有不同,例如Wi。因为你不能按每行的字母数来分割字符串,所以你需要一个更准确的方法。

    主要技巧是使用

    imagettfbbox
    

    它给出了使用 TrueType 字体的文本的边界框,从中,您可以获得文本将使用的实际宽度。

    这是在http://php.net/manual/en/function.wordwrap.php 找到的像素完美分割函数 使用它代替自动换行并传递额外的值,如图像宽度、字体大小和字体路径

    <?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;
    }
    ;
    
    ?>
    

    以下是工作代码示例: 我稍微修改了这个函数pixel_word_wrap。此外,修改了代码中的一些计算。现在正在给我正确计算边距的完美图像。我对代码不太满意,注意到有一个 $adjustment 变量,当你使用更大的字体大小时,它应该更大。我认为这归因于imagettfbbox 功能的不完善。但这是一种实用的方法,适用于大多数字体大小。

    <?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);
    

    这是一个例子

    您也可以使用等宽字体。等宽字体是一种字体,其字母和字符各自占据相同数量的水平空间。

    【讨论】:

    • 我只是试了一下,结果出乎意料。 [链接]s12.postimg.org/i7iobio7x/image_7708.png
    • @Luis 你能告诉我你用的是什么字体吗?我看看这个。
    • @Luis 请检查第二个代码示例,它在我的机器上为我提供了完美的图像。 (用各种设置测试)。您需要在脚本顶部设置 $left_margin、$top_margin、$text、$font_size 和 $image_line_height
    • 谢谢你,现在它正在工作。还有一些地方需要改进,比如证明文本的合理性,但我会先自己尝试。
    • 你好,我刚刚发现了一个bug,有时候会断字,看:s15.postimg.org/6yrjadpi3/image_6414.png。我认为是由于这种词:ó,ñ,í。所以我替换了 substr 和 strlen 例如: mb_strlen($words[$i], 'UTF-8') 或 mb_substr($words[$i], 0, $s) 。 '-', NULL, 'UTF-8'); ,但它仍然无法正常工作
    【解决方案2】:

    问题是您的字体是每个字母的可变宽度,但是您根据字母的数量而不是字体的宽度截断。

    以下面的例子为例,十个“I”对十个“W”,第二个将超过两倍。

    iiiiiiiiiiii

    万维网万维网

    “简单”选项是使用等宽字体,例如 Courier,在下面的块中使用:

    iiiiiiiiii
    WWWWWWWWWW
    

    但那是一种无聊的字体!因此,您需要在每一行上使用ìmagettfbbox(图像True Type Font Bounding Box"函数http://php.net/manual/en/function.imagettfbbox.php)来获取宽度。您需要一次运行此函数,以减小大小直到获得您需要的尺寸。

    一段伪代码(请注意:临时编写且未经测试,您需要调整它以使其完美):

    $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.
    

    警告:TTF 边界框功能也不是完美的 - 所以允许一点“背风”,但你仍然会得到比上面所做的更好的结果(即 +-10 像素) .它还取决于字体文件的字距调整(字母之间的间隙)信息。如果你需要的话,一些 Goggling 和阅读手册中的 cmets 将帮助你获得更准确的结果。

    您还应该优化上面的函数(从 10 个字符开始并增加,取最后一个适合的字符串可能会让您更快地得到答案,而不是减少直到适合,并减少 strlen 调用的数量例如)。


    针对评论“您能否扩展一下“TTF 边界框功能也不完美”的补充说明? (回复太长,无法评论)

    该函数依赖于字体中的“kerning”信息。例如,您希望 V 比 V 和 W 更靠近 A(VA - 看看它们如何稍微“重叠”)(VW - 看看 W 在 V 之后如何开始)。字体中嵌入了许多关于该间距的规则。其中一些规则还说“我知道‘盒子’从 0 开始,但是对于这个字母,您需要从 -3 像素开始绘制”。

    PHP 最好阅读规则,但有时会出错,因此会给出错误的维度。这就是为什么您可能会告诉 PHP 从“0,0”开始写入但它实际上从“-3,0”开始并且似乎切断了字体的原因。最简单的解决方案是允许几个像素宽限。

    是的,这是一个众所周知的“问题”(https://www.google.com/webhp?q=php%20bounding%20box%20incorrect)

    【讨论】:

    • 你能扩展一下“TTF边界框功能也不完善”吗?它有什么问题?这是一个已知问题吗? (原因是这可能是造成 OP 问题的一个因素。)
    • @RadLexus - 在答案中回复(评论太长了)
    • 在不完美的盒子上,我做了一些进一步的调查。该问题的根源在于所有 (!) php 字体函数似乎都返回文本的 pixel area,而不是 string 边界框。这意味着通常左边的 x 在右边“偏离”几个像素(除非一个字符伸出左边距),所以这就是你必须开始绘制文本的地方。但是对于单个字符1,它们仍然返回only 确切的边界框,not 带有通常的“sidebearings”——字符定义的左右两侧额外的白色以防止它们粘在一起。
    • .. 我认为这可以追溯到 php 的gd.c 中的gdImageStringFTEx。返回的边界框使用来自FT_Glyph_Get_CBox 的值进行更新。但是(!),移动“光标”的 advance 位置从image-&gt;advance 使用(通过FT_Glyph_To_Bitmap 更新)。对于正确的文本边界框,您需要累积的advances,而不是像素边界。 (尽管这些还有其他用途。)
    • 如果您确实需要准确性,网络上有一些“修复”,包括 PHP 手册的 cmets。我从来没有测试过它们的准确性/有效性,我也从来不需要担心像素完美的位置。但自从我记得以来就是这样,所以它没有排序一定是有某种原因 - 尽管我从来没有像你刚刚做过的那样深入研究(再次,从来不需要)。
    猜你喜欢
    • 2013-10-26
    • 1970-01-01
    • 2017-01-03
    • 1970-01-01
    • 1970-01-01
    • 2011-09-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多