Source

linebreaks4imagettftext.php

<?php

/**
 * Linebreaks4imagettftext v1.1.2
 *
 * Copyright (c) 2018-2026 Andrew G. Johnson <andrew@andrewgjohnson.com>
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
 * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to the following conditions:
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
 * Software.
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
 * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
 * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 * PHP version 5
 *
 * @category  Andrewgjohnson
 * @package   Linebreaks4imagettftext
 * @author    Andrew G. Johnson <andrew@andrewgjohnson.com>
 * @copyright 2018-2026 Andrew G. Johnson <andrew@andrewgjohnson.com>
 * @license   https://opensource.org/licenses/mit/ The MIT License
 * @link      https://github.com/andrewgjohnson/linebreaks4imagettftext
 */

namespace AndrewGJohnson\AgjGd;

if (!function_exists('AndrewGJohnson\\AgjGd\\linebreaks4imagettftext')) {
    /**
     * Linebreaks4imagettftext is a function to automatically insert line breaks into your text while using PHP’s
     * imagettftext() function.
     *
     * Examples:
     * ```
     * <?php
     * // You can use linebreaks4imagettftext() to add line breaks ("\n") to long strings to help format text when using
     * // imagettftext()
     * $string = 'This is a long sentence that could not fit on a single line.';
     * $stringWithLineBreaks = \AndrewGJohnson\AgjGd\linebreaks4imagettftext(20, 0, $font, $string, imagesx($im) * 0.8);
     *
     * // This will work but there will be no line breaks so your text will likely overflow horizontally
     * imagettftext($im, 20, 0, imagesx($im) * 0.1, 0, $color, $font, $string);
     *
     * // This will work and you will not have to worry about text overflowing regardless of string length
     * imagettftext($im, 20, 0, imagesx($im) * 0.1, 0, $color, $font, $stringWithLineBreaks);
     * ?>
     * ```
     *
     * @param float  $size                    The font size in points.
     * @param float  $angle                   The angle in degrees, with 0 degrees being left-to-right reading text.
     * Higher values represent a counter-clockwise rotation. For example, a value of 90 would result in bottom-to-top
     * reading text.
     * @param string $fontFilename            The path to the TrueType font you wish to use.
     *
     * Depending on which version of the GD library PHP is using, when fontfile does not begin with a leading / then
     * .ttf will be appended to the filename and the library will attempt to search for that filename along a
     * library-defined font path.
     *
     * When using versions of the GD library lower than 2.0.18, a space character, rather than a semicolon, was used as
     * the 'path separator' for different font files. Unintentional use of this feature will result in the warning
     * message: Warning: Could not find/open font. For these affected versions, the only solution is moving the font to
     * a path which does not contain spaces.
     *
     * In many cases where a font resides in the same directory as the script using it the following trick will
     * alleviate any include problems.
     *
     * ```
     * <?php
     * // Set the environment variable for GD
     * putenv('GDFONTPATH=' . realpath('.'));
     *
     * // Name the font to be used (note the lack of the .ttf extension)
     * $font = 'SomeFont';
     * ?>
     * ```
     * @param string $text                    The text string in UTF-8 encoding.
     *
     * May include decimal numeric character references (of the form: €) to access characters in a font beyond
     * position 127. The hexadecimal format (like ©) is supported. Strings in UTF-8 encoding can be passed
     * directly.
     *
     * Named entities, such as ©, are not supported. Consider using html_entity_decode() to decode these named
     * entities into UTF-8 strings.
     *
     * If a character is used in the string which is not supported by the font, a hollow rectangle will replace the
     * character.
     * @param int    $maximumWidth            The maximum width (in pixels) a line should be before adding a line break.
     * @param string $lineBreakCharacter      The character(s) to use when adding a line break.
     * @param bool   $attemptToBreakOnHyphens Whether or not to attempt to break words on the hyphen(s) appearing
     * within.
     * @param bool   $forceBreakOnSingleWords Whether or not to force breaks into single words that extend beyond a
     * single line.
     * @param bool   $preventWidows           Whether or not to try to prevent widows which are single words appearing
     * alone on a final line.
     *
     * @return string Returns a string that is nearly identical to $text with the only difference being newly added line
     * breaks.
     */
    function linebreaks4imagettftext(
        $size,
        $angle,
        $fontFilename,
        $text,
        $maximumWidth,
        $lineBreakCharacter = PHP_EOL,
        $attemptToBreakOnHyphens = false,
        $forceBreakOnSingleWords = false,
        $preventWidows = false
    ) {
        // Define the _linebreaks4imagettftext_ttfWidth function for later use
        if (!function_exists('AndrewGJohnson\\AgjGd\\_linebreaks4imagettftext_ttfWidth')) {
            /**
             * Calculate the width in pixels of a string rendered by imagettftext().
             *
             * @param float  $size         The font size in points.
             * @param float  $angle        The angle in degrees, with 0 degrees being left-to-right reading text. Higher
             * values represent a counter-clockwise rotation. For example, a value of 90 would result in bottom-to-top
             * reading text.
             * @param string $fontFilename The path to the TrueType font you wish to use.
             *
             * Depending on which version of the GD library PHP is using, when fontfile does not begin with a leading /
             * then .ttf will be appended to the filename and the library will attempt to search for that filename along
             * a library-defined font path.
             *
             * When using versions of the GD library lower than 2.0.18, a space character, rather than a semicolon, was
             * used as the 'path separator' for different font files. Unintentional use of this feature will result in
             * the warning message: Warning: Could not find/open font. For these affected versions, the only solution is
             * moving the font to a path which does not contain spaces.
             *
             * In many cases where a font resides in the same directory as the script using it the following trick will
             * alleviate any include problems.
             *
             * ```
             * <?php
             * // Set the environment variable for GD
             * putenv('GDFONTPATH=' . realpath('.'));
             *
             * // Name the font to be used (note the lack of the .ttf extension)
             * $font = 'SomeFont';
             * ?>
             * ```
             * @param string $text         The text string in UTF-8 encoding.
             *
             * May include decimal numeric character references (of the form: €) to access characters in a font
             * beyond position 127. The hexadecimal format (like ©) is supported. Strings in UTF-8 encoding can be
             * passed directly.
             *
             * Named entities, such as ©, are not supported. Consider using html_entity_decode() to decode these
             * named entities into UTF-8 strings.
             *
             * If a character is used in the string which is not supported by the font, a hollow rectangle will replace
             * the character.
             *
             * @return false|int Returns the width in pixels of a string rendered by imagettftext(). Returns FALSE on
             * error.
             */
            function _linebreaks4imagettftext_ttfWidth(
                $size,
                $angle,
                $fontFilename,
                $text
            ) {
                $imagettfbbox = imagettfbbox(
                    $size,
                    $angle,
                    $fontFilename,
                    $text
                );
                if ($imagettfbbox === false) {
                    return false;
                } else {
                    $left  = min($imagettfbbox[0], $imagettfbbox[2], $imagettfbbox[4], $imagettfbbox[6]);
                    $right = max($imagettfbbox[0], $imagettfbbox[2], $imagettfbbox[4], $imagettfbbox[6]);
                    return $right - $left;
                }
            }
        }

        // Create an array with all the string’s words.
        $words = explode(' ', $text);

        // Process all words to generate $textWithLineBreaks.
        $textWithLineBreaks = '';

        $currentLine = '';
        foreach ($words as $position => $word) {
            // Place the first word into $currentLine without further processing. If it is too wide, later logic can
            // only force-break it when another word causes the loop to enter the normal processing branch.
            if ($position === 0) {
                $currentLine = $word;
            } else {
                $addedWord = false;

                // Check whether adding the new word to the current line still fits within the maximum width.
                $textWidth = _linebreaks4imagettftext_ttfWidth(
                    $size,
                    $angle,
                    $fontFilename,
                    $currentLine . ' ' . $word
                );
                if ($textWidth <= $maximumWidth) {
                    $currentLine .= ' ';
                    $currentLine .= $word;

                    $addedWord = true;
                }

                // If the final word would appear alone on the last line, try moving the previous word down with it.
                if (!$addedWord && $preventWidows && $position === count($words) - 1) {
                    $lastSpacePosition = strrpos($currentLine, ' ');

                    if ($lastSpacePosition !== false) {
                        $previousLine = substr($currentLine, 0, $lastSpacePosition);
                        $lastWord     = substr($currentLine, $lastSpacePosition + 1);
                        $testLine     = $lastWord . ' ' . $word;
                        $textWidth    = _linebreaks4imagettftext_ttfWidth(
                            $size,
                            $angle,
                            $fontFilename,
                            $testLine
                        );

                        if ($textWidth <= $maximumWidth) {
                            $textWithLineBreaks .= $previousLine;
                            $textWithLineBreaks .= $lineBreakCharacter;

                            $currentLine = $testLine;

                            $addedWord = true;
                        }
                    }
                }

                if (!$addedWord && $attemptToBreakOnHyphens) {
                    // Attempt to split the word on hyphens and fit as much of it as possible on the current line.
                    if (strpos($word, '-') !== false) {
                        $hyphenParts = explode('-', $word);
                        $rebuiltWord = '';

                        foreach ($hyphenParts as $index => $part) {
                            // Rebuild the word progressively, re-adding hyphens between parts.
                            $candidate = ($rebuiltWord === '' ? $part : $rebuiltWord . '-' . $part);

                            $testLine  = $currentLine . ' ' . $candidate;
                            $textWidth = _linebreaks4imagettftext_ttfWidth(
                                $size,
                                $angle,
                                $fontFilename,
                                $testLine
                            );

                            if ($textWidth <= $maximumWidth) {
                                $rebuiltWord = $candidate;
                                continue;
                            }

                            // If we have something that fits, commit it.
                            if ($rebuiltWord !== '') {
                                $currentLine .= ' ' . $rebuiltWord . '-';

                                $textWithLineBreaks .= $currentLine;
                                $textWithLineBreaks .= $lineBreakCharacter;

                                // Remaining parts become the next word.
                                $remainingParts = array_slice($hyphenParts, $index);
                                $word           = implode('-', $remainingParts);

                                $currentLine = $word;

                                $addedWord = true;
                            }

                            break;
                        }
                    }
                }

                if (!$addedWord && $forceBreakOnSingleWords) {
                    // Only force-break a word when it is starting a new line. If the word failed to fit because the
                    // current line already contains text, commit the current line first, then process the word from an
                    // empty line.
                    if ($currentLine !== '') {
                        $textWithLineBreaks .= $currentLine;
                        $textWithLineBreaks .= $lineBreakCharacter;

                        $currentLine = '';
                    }

                    $remainingCharacters = preg_split('//u', $word, -1, PREG_SPLIT_NO_EMPTY);

                    if ($remainingCharacters !== false) {
                        while (count($remainingCharacters) > 0) {
                            $candidateCharacters = array();
                            $candidateWord       = '';

                            foreach ($remainingCharacters as $index => $character) {
                                $testCandidateCharacters   = $candidateCharacters;
                                $testCandidateCharacters[] = $character;

                                $testCandidateWord      = implode('', $testCandidateCharacters);
                                $hasRemainingCharacters = ($index < count($remainingCharacters) - 1);
                                $testWord               = $testCandidateWord . ($hasRemainingCharacters ? '-' : '');

                                $testLine  = ($currentLine === '' ? $testWord : $currentLine . ' ' . $testWord);
                                $textWidth = _linebreaks4imagettftext_ttfWidth(
                                    $size,
                                    $angle,
                                    $fontFilename,
                                    $testLine
                                );

                                if ($textWidth <= $maximumWidth) {
                                    $candidateCharacters = $testCandidateCharacters;
                                    $candidateWord       = $testCandidateWord;
                                    continue;
                                }

                                break;
                            }

                            if ($candidateWord === '') {
                                // If even one character plus a hyphen cannot fit, commit the current line and retry.
                                if ($currentLine !== '') {
                                    $textWithLineBreaks .= $currentLine;
                                    $textWithLineBreaks .= $lineBreakCharacter;

                                    $currentLine = '';
                                    continue;
                                }

                                // If a single character still cannot fit on an empty line, use the whole remaining
                                // word to avoid an infinite loop.
                                $currentLine = implode('', $remainingCharacters);

                                $addedWord = true;
                                break;
                            }

                            $remainingCharacters = array_slice(
                                $remainingCharacters,
                                count($candidateCharacters)
                            );

                            if (count($remainingCharacters) > 0) {
                                // More characters remain, so append a hyphen and commit this forced-break segment.
                                $currentLine = $currentLine === ''
                                    ? $candidateWord . '-'
                                    : $currentLine . ' ' . $candidateWord . '-';

                                $textWithLineBreaks .= $currentLine;
                                $textWithLineBreaks .= $lineBreakCharacter;

                                $currentLine = '';
                                continue;
                            }

                            $currentLine = $currentLine === '' ? $candidateWord : $currentLine . ' ' . $candidateWord;

                            $addedWord = true;
                        }
                    }
                }

                // If the word still has not been added, start a new line with this word.
                if (!$addedWord) {
                    // The text is too wide with the added word, so add a line break and start a new line with only
                    // that word.
                    $textWithLineBreaks .= $currentLine;
                    $textWithLineBreaks .= $lineBreakCharacter;

                    $currentLine = $word;
                }
            }
        }

        // Append the final line to the processed text.
        $textWithLineBreaks .= $currentLine;

        // Return $text with line breaks added.
        return $textWithLineBreaks;
    }
}