PHP Source
You can view the full source code for the AgjGd class below. It is the whole library; the public static methods and the private helpers they share.
AgjGd.php
<?php
/**
* AgjGd v2.0.0
*
* Copyright (c) 2013–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 8
*
* @category AndrewGJohnson
* @package AgjGd
* @author Andrew G. Johnson <andrew@andrewgjohnson.com>
* @copyright 2013–2026 Andrew G. Johnson <andrew@andrewgjohnson.com>
* @license https://opensource.org/licenses/mit/ The MIT License
* @link https://github.com/andrewgjohnson/AgjGd
*/
declare(strict_types=1);
namespace AndrewGJohnson;
use GdImage;
use InvalidArgumentException;
/**
* AgjGd is a project that extends the functionality of PHP’s GD library started by Andrew G. Johnson.
*
* Examples:
*
* ```
* <?php
*
* use AndrewGJohnson\AgjGd;
*
* AgjGd::imageblendedcolorallocate($image, $red, $yellow);
* AgjGd::imagecolorallocatefromstring($image, '#ff0000');
* AgjGd::imagegradientrectangle($image, 10, 10, 100, 100, $red, $blue);
* AgjGd::imagefttextfilter($image, 20, 0, 0, 0, $color, $font, $text, [], 10);
* AgjGd::imagefttextgradient($image, 20, 0, 0, 0, $color, $font, $text, [], $gradientColor);
* AgjGd::linebreaksfortext(20, 0, $font, $text, 480);
* ```
*
* @category AndrewGJohnson
* @package AgjGd
* @author Andrew G. Johnson <andrew@andrewgjohnson.com>
* @license https://opensource.org/licenses/mit/ The MIT License
* @link https://github.com/andrewgjohnson/AgjGd
*/
class AgjGd
{
/**
* The lowest possible GD alpha value, which is completely opaque.
*/
private const ALPHA_OPAQUE = 0;
/**
* The highest possible GD alpha value, which is completely transparent.
*/
private const ALPHA_TRANSPARENT = 127;
/**
* The highest possible value for a single 8-bit RGB color component.
*/
private const COLOR_COMPONENT_MAX = 255;
/**
* The CSS color keywords recognized by imagecolorallocatefromstring() and their RGB values.
*
* Source: https://www.w3.org/wiki/CSS/Properties/color/keywords
*
* @var array<string, array{0: int, 1: int, 2: int}>
*/
private const CSS_COLOR_KEYWORDS = [
'aliceblue' => [0xF0, 0xF8, 0xFF],
'antiquewhite' => [0xFA, 0xEB, 0xD7],
'aqua' => [0x00, 0xFF, 0xFF],
'aquamarine' => [0x7F, 0xFF, 0xD4],
'azure' => [0xF0, 0xFF, 0xFF],
'beige' => [0xF5, 0xF5, 0xDC],
'bisque' => [0xFF, 0xE4, 0xC4],
'black' => [0x00, 0x00, 0x00],
'blanchedalmond' => [0xFF, 0xEB, 0xCD],
'blue' => [0x00, 0x00, 0xFF],
'blueviolet' => [0x8A, 0x2B, 0xE2],
'brown' => [0xA5, 0x2A, 0x2A],
'burlywood' => [0xDE, 0xB8, 0x87],
'cadetblue' => [0x5F, 0x9E, 0xA0],
'chartreuse' => [0x7F, 0xFF, 0x00],
'chocolate' => [0xD2, 0x69, 0x1E],
'coral' => [0xFF, 0x7F, 0x50],
'cornflowerblue' => [0x64, 0x95, 0xED],
'cornsilk' => [0xFF, 0xF8, 0xDC],
'crimson' => [0xDC, 0x14, 0x3C],
'cyan' => [0x00, 0xFF, 0xFF],
'darkblue' => [0x00, 0x00, 0x8B],
'darkcyan' => [0x00, 0x8B, 0x8B],
'darkgoldenrod' => [0xB8, 0x86, 0x0B],
'darkgray' => [0xA9, 0xA9, 0xA9],
'darkgreen' => [0x00, 0x64, 0x00],
'darkgrey' => [0xA9, 0xA9, 0xA9],
'darkkhaki' => [0xBD, 0xB7, 0x6B],
'darkmagenta' => [0x8B, 0x00, 0x8B],
'darkolivegreen' => [0x55, 0x6B, 0x2F],
'darkorange' => [0xFF, 0x8C, 0x00],
'darkorchid' => [0x99, 0x32, 0xCC],
'darkred' => [0x8B, 0x00, 0x00],
'darksalmon' => [0xE9, 0x96, 0x7A],
'darkseagreen' => [0x8F, 0xBC, 0x8F],
'darkslateblue' => [0x48, 0x3D, 0x8B],
'darkslategray' => [0x2F, 0x4F, 0x4F],
'darkslategrey' => [0x2F, 0x4F, 0x4F],
'darkturquoise' => [0x00, 0xCE, 0xD1],
'darkviolet' => [0x94, 0x00, 0xD3],
'deeppink' => [0xFF, 0x14, 0x93],
'deepskyblue' => [0x00, 0xBF, 0xFF],
'dimgray' => [0x69, 0x69, 0x69],
'dimgrey' => [0x69, 0x69, 0x69],
'dodgerblue' => [0x1E, 0x90, 0xFF],
'firebrick' => [0xB2, 0x22, 0x22],
'floralwhite' => [0xFF, 0xFA, 0xF0],
'forestgreen' => [0x22, 0x8B, 0x22],
'fuchsia' => [0xFF, 0x00, 0xFF],
'gainsboro' => [0xDC, 0xDC, 0xDC],
'ghostwhite' => [0xF8, 0xF8, 0xFF],
'gold' => [0xFF, 0xD7, 0x00],
'goldenrod' => [0xDA, 0xA5, 0x20],
'gray' => [0x80, 0x80, 0x80],
'green' => [0x00, 0x80, 0x00],
'greenyellow' => [0xAD, 0xFF, 0x2F],
'grey' => [0x80, 0x80, 0x80],
'honeydew' => [0xF0, 0xFF, 0xF0],
'hotpink' => [0xFF, 0x69, 0xB4],
'indianred' => [0xCD, 0x5C, 0x5C],
'indigo' => [0x4B, 0x00, 0x82],
'ivory' => [0xFF, 0xFF, 0xF0],
'khaki' => [0xF0, 0xE6, 0x8C],
'lavender' => [0xE6, 0xE6, 0xFA],
'lavenderblush' => [0xFF, 0xF0, 0xF5],
'lawngreen' => [0x7C, 0xFC, 0x00],
'lemonchiffon' => [0xFF, 0xFA, 0xCD],
'lightblue' => [0xAD, 0xD8, 0xE6],
'lightcoral' => [0xF0, 0x80, 0x80],
'lightcyan' => [0xE0, 0xFF, 0xFF],
'lightgoldenrodyellow' => [0xFA, 0xFA, 0xD2],
'lightgray' => [0xD3, 0xD3, 0xD3],
'lightgreen' => [0x90, 0xEE, 0x90],
'lightgrey' => [0xD3, 0xD3, 0xD3],
'lightpink' => [0xFF, 0xB6, 0xC1],
'lightsalmon' => [0xFF, 0xA0, 0x7A],
'lightseagreen' => [0x20, 0xB2, 0xAA],
'lightskyblue' => [0x87, 0xCE, 0xFA],
'lightslategray' => [0x77, 0x88, 0x99],
'lightslategrey' => [0x77, 0x88, 0x99],
'lightsteelblue' => [0xB0, 0xC4, 0xDE],
'lightyellow' => [0xFF, 0xFF, 0xE0],
'lime' => [0x00, 0xFF, 0x00],
'limegreen' => [0x32, 0xCD, 0x32],
'linen' => [0xFA, 0xF0, 0xE6],
'magenta' => [0xFF, 0x00, 0xFF],
'maroon' => [0x80, 0x00, 0x00],
'mediumaquamarine' => [0x66, 0xCD, 0xAA],
'mediumblue' => [0x00, 0x00, 0xCD],
'mediumorchid' => [0xBA, 0x55, 0xD3],
'mediumpurple' => [0x93, 0x70, 0xDB],
'mediumseagreen' => [0x3C, 0xB3, 0x71],
'mediumslateblue' => [0x7B, 0x68, 0xEE],
'mediumspringgreen' => [0x00, 0xFA, 0x9A],
'mediumturquoise' => [0x48, 0xD1, 0xCC],
'mediumvioletred' => [0xC7, 0x15, 0x85],
'midnightblue' => [0x19, 0x19, 0x70],
'mintcream' => [0xF5, 0xFF, 0xFA],
'mistyrose' => [0xFF, 0xE4, 0xE1],
'moccasin' => [0xFF, 0xE4, 0xB5],
'navajowhite' => [0xFF, 0xDE, 0xAD],
'navy' => [0x00, 0x00, 0x80],
'oldlace' => [0xFD, 0xF5, 0xE6],
'olive' => [0x80, 0x80, 0x00],
'olivedrab' => [0x6B, 0x8E, 0x23],
'orange' => [0xFF, 0xA5, 0x00],
'orangered' => [0xFF, 0x45, 0x00],
'orchid' => [0xDA, 0x70, 0xD6],
'palegoldenrod' => [0xEE, 0xE8, 0xAA],
'palegreen' => [0x98, 0xFB, 0x98],
'paleturquoise' => [0xAF, 0xEE, 0xEE],
'palevioletred' => [0xDB, 0x70, 0x93],
'papayawhip' => [0xFF, 0xEF, 0xD5],
'peachpuff' => [0xFF, 0xDA, 0xB9],
'peru' => [0xCD, 0x85, 0x3F],
'pink' => [0xFF, 0xC0, 0xCB],
'plum' => [0xDD, 0xA0, 0xDD],
'powderblue' => [0xB0, 0xE0, 0xE6],
'purple' => [0x80, 0x00, 0x80],
'red' => [0xFF, 0x00, 0x00],
'rosybrown' => [0xBC, 0x8F, 0x8F],
'royalblue' => [0x41, 0x69, 0xE1],
'saddlebrown' => [0x8B, 0x45, 0x13],
'salmon' => [0xFA, 0x80, 0x72],
'sandybrown' => [0xF4, 0xA4, 0x60],
'seagreen' => [0x2E, 0x8B, 0x57],
'seashell' => [0xFF, 0xF5, 0xEE],
'sienna' => [0xA0, 0x52, 0x2D],
'silver' => [0xC0, 0xC0, 0xC0],
'skyblue' => [0x87, 0xCE, 0xEB],
'slateblue' => [0x6A, 0x5A, 0xCD],
'slategray' => [0x70, 0x80, 0x90],
'slategrey' => [0x70, 0x80, 0x90],
'snow' => [0xFF, 0xFA, 0xFA],
'springgreen' => [0x00, 0xFF, 0x7F],
'steelblue' => [0x46, 0x82, 0xB4],
'tan' => [0xD2, 0xB4, 0x8C],
'teal' => [0x00, 0x80, 0x80],
'thistle' => [0xD8, 0xBF, 0xD8],
'tomato' => [0xFF, 0x63, 0x47],
'turquoise' => [0x40, 0xE0, 0xD0],
'violet' => [0xEE, 0x82, 0xEE],
'wheat' => [0xF5, 0xDE, 0xB3],
'white' => [0xFF, 0xFF, 0xFF],
'whitesmoke' => [0xF5, 0xF5, 0xF5],
'yellow' => [0xFF, 0xFF, 0x00],
'yellowgreen' => [0x9A, 0xCD, 0x32]
];
/**
* Allocates a new blended color based on two existing allocated colors.
*
* Examples:
* ```
* <?php
*
* use AndrewGJohnson\AgjGd;
*
* // Allocate red and yellow using the standard method then blend the two to allocate orange
* $red = imagecolorallocate($image, 0xFF, 0x00, 0x00);
* $yellow = imagecolorallocate($image, 0xFF, 0xFF, 0x00);
* $orange = AgjGd::imageblendedcolorallocate($image, $red, $yellow);
*
* // You can also allocate RGBA colors as well as RGB
* $opaqueBlack = imagecolorallocatealpha($image, 0x00, 0x00, 0x00, 0);
* $translucentBlack = imagecolorallocatealpha($image, 0x00, 0x00, 0x00, 63);
* $blendedBlack = AgjGd::imageblendedcolorallocate($image, $opaqueBlack, $translucentBlack);
*
* // By default, we allocate with a 50/50 blend where we average the red, blue, green and alpha values for each
* // color but also support alternative blends
* $blue = imagecolorallocate($image, 0x00, 0x00, 0xFF);
* $cyan = imagecolorallocate($image, 0x00, 0xFF, 0xFF);
* $blendedMostlyCyan = AgjGd::imageblendedcolorallocate($image, $blue, $cyan, 0.25); // 25% blue, 75% cyan
* $blendedEvenly = AgjGd::imageblendedcolorallocate($image, $blue, $cyan); // 50% blue, 50% cyan
* $blendedMostlyBlue = AgjGd::imageblendedcolorallocate($image, $blue, $cyan, 0.75); // 75% blue, 25% cyan
* ```
*
* @param GdImage $image A GdImage object, returned by one of the image creation functions, such as
* imagecreatetruecolor().
* @param int|false $color1 A color identifier created with imagecolorallocate(). Passing FALSE, which is
* what the GD allocation functions return on failure, returns FALSE.
* @param int|false $color2 A color identifier created with imagecolorallocate(). Passing FALSE, which is
* what the GD allocation functions return on failure, returns FALSE.
* @param float $opacityColor1 The blend ratio for color1, between 0 and 1. At 1 the result is entirely color1;
* at 0 it is entirely color2; 0.5 (the default) produces an even blend. Values outside of that range fall back to
* an even blend.
*
* @return int|false Returns a color identifier or FALSE if the allocation failed.
*/
public static function imageblendedcolorallocate(
GdImage $image,
int|false $color1,
int|false $color2,
float $opacityColor1 = 0.5
): int|false {
// Return false if either color identifier is invalid.
if ($color1 === false || $color2 === false) {
return false;
}
// Calculate $opacityColor2 based on $opacityColor1, falling back to an even blend when $opacityColor1 is out
// of range.
if ($opacityColor1 < 0 || $opacityColor1 > 1) {
$opacityColor1 = 0.5;
}
$opacityColor2 = 1 - $opacityColor1;
$componentsColor1 = self::colorComponents($image, $color1);
$componentsColor2 = self::colorComponents($image, $color2);
$red = (int)round(
($componentsColor1['red'] * $opacityColor1) + ($componentsColor2['red'] * $opacityColor2)
);
$green = (int)round(
($componentsColor1['green'] * $opacityColor1) + ($componentsColor2['green'] * $opacityColor2)
);
$blue = (int)round(
($componentsColor1['blue'] * $opacityColor1) + ($componentsColor2['blue'] * $opacityColor2)
);
$alpha = (int)round(
($componentsColor1['alpha'] * $opacityColor1) + ($componentsColor2['alpha'] * $opacityColor2)
);
return self::allocateColor($image, $red, $green, $blue, $alpha);
}
/**
* Allocates a color based on a string.
*
* Examples:
* ```
* <?php
*
* use AndrewGJohnson\AgjGd;
*
* // Allocate red with imagecolorallocate() or with imagecolorallocatefromstring() via a string
* $red = imagecolorallocate($image, 0xFF, 0x00, 0x00);
* $redFromString = AgjGd::imagecolorallocatefromstring($image, '#FF0000');
* $redFromString = AgjGd::imagecolorallocatefromstring($image, '#f00');
* $redFromString = AgjGd::imagecolorallocatefromstring($image, 'rgb(255 0 0)');
* $redFromString = AgjGd::imagecolorallocatefromstring($image, 'rgb(255, 0, 0)');
* $redFromString = AgjGd::imagecolorallocatefromstring($image, 'rgba(255, 0, 0, 1)');
* $redFromString = AgjGd::imagecolorallocatefromstring($image, 'rgba(255 0 0 / 100%)');
* $redFromString = AgjGd::imagecolorallocatefromstring($image, 'red');
* ```
*
* @param GdImage $image A GdImage object, returned by one of the image creation functions, such as
* imagecreatetruecolor().
* @param string $string A string describing the color. You can pass a hex code (e.g. '#ff0000'), an RGB value
* (e.g. 'rgb(255, 0, 0)'), an RGBA value (e.g. 'rgba(255, 0, 0, 1)') or a CSS color keyword (e.g. 'red').
* @param int $alpha A value between 0 and 127. 0 indicates completely opaque while 127 indicates completely
* transparent. Default is zero. Ignored when $string is an rgb()/rgba() value that specifies its own alpha.
*
* @throws InvalidArgumentException If the $string or $alpha parameter is invalid.
*
* @return int|false Returns a color identifier or FALSE if the allocation failed.
*/
public static function imagecolorallocatefromstring(
GdImage $image,
string $string,
int $alpha = self::ALPHA_OPAQUE
): int|false {
// Convert the string to lowercase and remove surrounding whitespace.
$string = strtolower(trim($string));
// Track whether an alpha value was supplied, which forces an RGBA color identifier even when the color
// is fully opaque.
$hasAlpha = false;
if (preg_match('/^#?([a-f0-9]{6}|[a-f0-9]{3})$/', $string) === 1) {
// Remove the pound/hashtag sign.
$string = ltrim($string, '#');
// If a short color code was passed convert it to a full color code.
if (strlen($string) === 3) {
$string = str_repeat($string[0], 2) . str_repeat($string[1], 2) . str_repeat($string[2], 2);
}
// Transform the hex values to decimal values.
$red = (int)hexdec(substr($string, 0, 2));
$green = (int)hexdec(substr($string, 2, 2));
$blue = (int)hexdec(substr($string, 4, 2));
} elseif (
preg_match(
'/^(rgba?)\(([0-9]+)(?:, *| +)([0-9]+)(?:, *| +)([0-9]+)(?:(?:, *| *\/ *)(' .
'(?:(?:0|1)(?:\.[0-9]+)?|\.[0-9]+)|(?:[0-9]+(?:\.[0-9]+)?%)))?\)$/',
$string,
$matches
) === 1
) {
// Track whether the caller explicitly passed an alpha value. The alpha group is the last one in the
// pattern, so when it goes unmatched PHP leaves it out of $matches entirely.
$hasAlpha = isset($matches[5]);
// Transform the RGBA values to integers.
$red = (int)$matches[2];
$green = (int)$matches[3];
$blue = (int)$matches[4];
if ($hasAlpha) {
$alphaValue = $matches[5];
// If the alpha value is a percentage convert it to a decimal value.
if (str_ends_with($alphaValue, '%')) {
$alphaValue = ((float)substr($alphaValue, 0, -1)) / 100;
} else {
$alphaValue = (float)$alphaValue;
}
if ($alphaValue < 0 || $alphaValue > 1) {
throw new InvalidArgumentException(
'imagecolorallocatefromstring() received an invalid value for $string, input was: ' . $string
);
}
// Convert CSS opacity to GD alpha. CSS uses 1 as opaque and 0 as transparent while GD uses 0 as
// opaque and 127 as transparent.
$alpha = self::ALPHA_TRANSPARENT - (int)round(self::ALPHA_TRANSPARENT * $alphaValue);
}
} elseif (isset(self::CSS_COLOR_KEYWORDS[$string])) {
[$red, $green, $blue] = self::CSS_COLOR_KEYWORDS[$string];
} else {
throw new InvalidArgumentException(
'imagecolorallocatefromstring() received an invalid value for $string, input was: ' . $string
);
}
if (
$red > self::COLOR_COMPONENT_MAX
|| $green > self::COLOR_COMPONENT_MAX
|| $blue > self::COLOR_COMPONENT_MAX
) {
throw new InvalidArgumentException(
'imagecolorallocatefromstring() received an invalid value for $string, input was: ' . $string
);
}
if ($alpha < self::ALPHA_OPAQUE || $alpha > self::ALPHA_TRANSPARENT) {
throw new InvalidArgumentException(
'imagecolorallocatefromstring() received an invalid value for $alpha, input was: ' . $alpha
);
}
return self::allocateColor($image, $red, $green, $blue, $alpha, $hasAlpha);
}
/**
* Draws a gradient filled rectangle.
*
* Examples:
* ```
* <?php
*
* use AndrewGJohnson\AgjGd;
*
* $red = imagecolorallocate($image, 0xFF, 0x00, 0x00);
* $blue = imagecolorallocate($image, 0x00, 0x00, 0xFF);
*
* // Standard method to draw solid filled rectangles
* imagefilledrectangle($image, 10, 10, 100, 100, $red);
*
* // This will draw a solid filled rectangle too, as no gradient color was passed
* AgjGd::imagegradientrectangle($image, 10, 10, 100, 100, $red);
*
* // This will draw a red-to-blue gradient filled rectangle (vertical gradient)
* AgjGd::imagegradientrectangle($image, 10, 10, 100, 100, $red, $blue);
*
* // This will draw a red-to-blue gradient filled rectangle (horizontal gradient)
* AgjGd::imagegradientrectangle($image, 10, 10, 100, 100, $red, $blue, true);
* ```
*
* @param GdImage $image A GdImage object, returned by one of the image creation functions, such as
* imagecreatetruecolor().
* @param int $x1 x-coordinate for point 1.
* @param int $y1 y-coordinate for point 1.
* @param int $x2 x-coordinate for point 2.
* @param int $y2 y-coordinate for point 2.
* @param int $color The start color. A color identifier created with imagecolorallocate().
* @param ?int $gradientColor The finish color. A color identifier created with imagecolorallocate().
* Passing NULL, the default, draws a solid filled rectangle in $color.
* @param bool $horizontalGradient Whether or not to use a horizontal gradient versus a vertical gradient.
*
* @return bool Returns TRUE on success or FALSE on failure.
*/
public static function imagegradientrectangle(
GdImage $image,
int $x1,
int $y1,
int $x2,
int $y2,
int $color,
?int $gradientColor = null,
bool $horizontalGradient = false
): bool {
// Without a gradient color there is no gradient to draw, so fall back to a solid filled rectangle.
if ($gradientColor === null) {
return imagefilledrectangle($image, $x1, $y1, $x2, $y2, $color);
}
if ($horizontalGradient) {
$left = min($x1, $x2);
$width = max($x1, $x2) - $left;
if ($width <= 0) {
return false;
}
for ($offset = 0; $offset <= $width; $offset++) {
$offsetColor = self::imageblendedcolorallocate(
$image,
$color,
$gradientColor,
1 - ($offset / $width)
);
if ($offsetColor === false) {
return false;
}
imagefilledrectangle($image, $left + $offset, $y1, $left + $offset, $y2, $offsetColor);
}
return true;
}
$top = min($y1, $y2);
$height = max($y1, $y2) - $top;
if ($height <= 0) {
return false;
}
for ($offset = 0; $offset <= $height; $offset++) {
$offsetColor = self::imageblendedcolorallocate(
$image,
$color,
$gradientColor,
1 - ($offset / $height)
);
if ($offsetColor === false) {
return false;
}
imagefilledrectangle($image, $x1, $top + $offset, $x2, $top + $offset, $offsetColor);
}
return true;
}
/**
* A drop-in replacement for imagettftext() with added parameters to add filtered text enabling blur, glow and
* shadow effects on your PHP GD images.
*
* Examples:
* ```
* <?php
*
* use AndrewGJohnson\AgjGd;
*
* imagettftext($image, 20, 0, 0, 0, $color, $font, $text, []); // Add text to a GD image
* AgjGd::imagefttextfilter($image, 20, 0, 0, 0, $color, $font, $text, []); // Works the same as the line above
* AgjGd::imagefttextfilter($image, 20, 0, 0, 0, $color, $font, $text, [], 1); // Adds the same text only blurred
* ```
*
* @param GdImage $image A GdImage object, returned by one of the image creation
* functions, such as imagecreatetruecolor().
* @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 int $x The coordinates given by x and y will define the basepoint
* of the first character (roughly the lower-left corner of the character). This is different from the
* imagestring(), where x and y define the upper-left corner of the first character. For example, "top left" is 0,
* 0.
* @param int $y The y-ordinate. This sets the position of the font’s
* baseline, not the very bottom of the character.
* @param int $color The color index. A negative color index disables
* antialiasing only on the fallback path, when no filter is applied; when a filter is applied only the color’s
* RGBA components are used. See imagecolorallocate().
* @param string $fontFilename The path to the TrueType font you wish to use.
* @param string $text The text string in UTF-8 encoding.
* @param array{linespacing?: float} $options The options passed through to imagettftext(). An array with
* a linespacing key holding a float value.
* @param int $filterIntensity The number of times you would like to apply the filter to
* your text. Passing zero (the default) or a negative value applies no filter at all.
* @param int $filter The filter you would like applied to your text. Defaults to
* a Gaussian blur. See imagefilter() for the available filters.
*
* @return array<int, int>|false Returns an array with 8 elements representing four points making the bounding box
* of the text. The order of the points is lower left, lower right, upper right, upper left. The points are
* relative to the text regardless of the angle, so "upper left" means in the top left-hand corner when you see the
* text horizontally. Returns FALSE on error.
*/
public static function imagefttextfilter(
GdImage $image,
float $size,
float $angle,
int $x,
int $y,
int $color,
string $fontFilename,
string $text,
array $options = [],
int $filterIntensity = 0,
int $filter = IMG_FILTER_GAUSSIAN_BLUR
): array|false {
// Without a filter intensity there is nothing to apply, so fall back to imagettftext().
if ($filterIntensity <= 0) {
return imagettftext($image, $size, $angle, $x, $y, $color, $fontFilename, $text, $options);
}
$renderedMask = self::renderTextMask($image, $size, $angle, $x, $y, $fontFilename, $text, $options);
if ($renderedMask === false) {
return false;
}
[$mask] = $renderedMask;
// Apply the filter to the mask $filterIntensity times.
for ($pass = 1; $pass <= $filterIntensity; $pass++) {
imagefilter($mask, $filter);
}
$components = self::colorComponents($image, $color);
// A fully transparent color scales every pixel’s visibility to zero, which leaves the image untouched and
// makes plotTextMask() report the failure.
$colorOpacity = (self::ALPHA_TRANSPARENT - $components['alpha']) / self::ALPHA_TRANSPARENT;
return self::plotTextMask(
$image,
$mask,
static function (int $maskX, int $maskY, float $visibility) use ($image, $components): int|false {
// The filtered mask fades the glyphs out at their edges, so a pixel’s visibility becomes its opacity.
return self::allocateColor(
$image,
$components['red'],
$components['green'],
$components['blue'],
(int)round((1 - $visibility) * self::ALPHA_TRANSPARENT),
true
);
},
$colorOpacity
);
}
/**
* An alias of imagefttextfilter(), using the more common “ttf” spelling of imagettftext().
*
* @param GdImage $image A GdImage object, returned by one of the image creation
* functions, such as imagecreatetruecolor().
* @param float $size The font size in points.
* @param float $angle The angle in degrees.
* @param int $x The x-ordinate of the basepoint of the first character.
* @param int $y The y-ordinate of the font’s baseline.
* @param int $color The color index. See imagecolorallocate().
* @param string $fontFilename The path to the TrueType font you wish to use.
* @param string $text The text string in UTF-8 encoding.
* @param array{linespacing?: float} $options The options passed through to imagettftext().
* @param int $filterIntensity The number of times you would like to apply the filter to your
* text. Passing zero (the default) or a negative value applies no filter at all.
* @param int $filter The filter you would like applied to your text. Defaults to a
* Gaussian blur. See imagefilter() for the available filters.
*
* @alias imagefttextfilter()
*
* @return array<int, int>|false Returns an array with 8 elements representing four points making the bounding box
* of the text, or FALSE on error.
*/
public static function imagettftextfilter(
GdImage $image,
float $size,
float $angle,
int $x,
int $y,
int $color,
string $fontFilename,
string $text,
array $options = [],
int $filterIntensity = 0,
int $filter = IMG_FILTER_GAUSSIAN_BLUR
): array|false {
return self::imagefttextfilter(
$image,
$size,
$angle,
$x,
$y,
$color,
$fontFilename,
$text,
$options,
$filterIntensity,
$filter
);
}
/**
* An alias of imagefttextfilter(). The filter defaults to a Gaussian blur, so this is a convenient name for
* blurring text and for the glow and shadow effects built on top of a blur.
*
* @param GdImage $image A GdImage object, returned by one of the image creation
* functions, such as imagecreatetruecolor().
* @param float $size The font size in points.
* @param float $angle The angle in degrees.
* @param int $x The x-ordinate of the basepoint of the first character.
* @param int $y The y-ordinate of the font’s baseline.
* @param int $color The color index. See imagecolorallocate().
* @param string $fontFilename The path to the TrueType font you wish to use.
* @param string $text The text string in UTF-8 encoding.
* @param array{linespacing?: float} $options The options passed through to imagettftext().
* @param int $blurIntensity The number of times you would like to apply the blur to your
* text. Passing zero (the default) or a negative value applies no blur at all.
* @param int $blurFilter The filter you would like applied to your text. Defaults to a
* Gaussian blur. See imagefilter() for the available filters.
*
* @alias imagefttextfilter()
*
* @return array<int, int>|false Returns an array with 8 elements representing four points making the bounding box
* of the text, or FALSE on error.
*/
public static function imagettftextblur(
GdImage $image,
float $size,
float $angle,
int $x,
int $y,
int $color,
string $fontFilename,
string $text,
array $options = [],
int $blurIntensity = 0,
int $blurFilter = IMG_FILTER_GAUSSIAN_BLUR
): array|false {
return self::imagefttextfilter(
$image,
$size,
$angle,
$x,
$y,
$color,
$fontFilename,
$text,
$options,
$blurIntensity,
$blurFilter
);
}
/**
* An alias of imagefttextfilter(), pairing the “ft” spelling with the blur-oriented name. The filter defaults to a
* Gaussian blur, so this is a convenient name for blurring text and for the glow and shadow effects built on top of
* a blur.
*
* @param GdImage $image A GdImage object, returned by one of the image creation
* functions, such as imagecreatetruecolor().
* @param float $size The font size in points.
* @param float $angle The angle in degrees.
* @param int $x The x-ordinate of the basepoint of the first character.
* @param int $y The y-ordinate of the font’s baseline.
* @param int $color The color index. See imagecolorallocate().
* @param string $fontFilename The path to the TrueType font you wish to use.
* @param string $text The text string in UTF-8 encoding.
* @param array{linespacing?: float} $options The options passed through to imagettftext().
* @param int $blurIntensity The number of times you would like to apply the blur to your
* text. Passing zero (the default) or a negative value applies no blur at all.
* @param int $blurFilter The filter you would like applied to your text. Defaults to a
* Gaussian blur. See imagefilter() for the available filters.
*
* @alias imagefttextfilter()
*
* @return array<int, int>|false Returns an array with 8 elements representing four points making the bounding box
* of the text, or FALSE on error.
*/
public static function imagefttextblur(
GdImage $image,
float $size,
float $angle,
int $x,
int $y,
int $color,
string $fontFilename,
string $text,
array $options = [],
int $blurIntensity = 0,
int $blurFilter = IMG_FILTER_GAUSSIAN_BLUR
): array|false {
return self::imagefttextfilter(
$image,
$size,
$angle,
$x,
$y,
$color,
$fontFilename,
$text,
$options,
$blurIntensity,
$blurFilter
);
}
/**
* A drop-in replacement for imagettftext() with added parameters to add gradient coloring effects.
*
* Examples:
* ```
* <?php
*
* use AndrewGJohnson\AgjGd;
*
* imagettftext($image, 20, 0, 0, 0, $color, $font, $text, []); // Add text to a GD image
* AgjGd::imagefttextgradient($image, 20, 0, 0, 0, $color, $font, $text, []); // Works the same as the line above
*
* // This will add the same text only with a vertical gradient instead of a solid color
* AgjGd::imagefttextgradient($image, 20, 0, 0, 0, $color, $font, $text, [], $gradientColor);
*
* // This will add the same text only with a horizontal gradient instead of a solid color
* AgjGd::imagefttextgradient($image, 20, 0, 0, 0, $color, $font, $text, [], $gradientColor, true);
* ```
*
* @param GdImage $image A GdImage object, returned by one of the image creation
* functions, such as imagecreatetruecolor().
* @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 int $x The coordinates given by x and y will define the basepoint
* of the first character (roughly the lower-left corner of the character). This is different from the
* imagestring(), where x and y define the upper-left corner of the first character. For example, "top left" is 0,
* 0.
* @param int $y The y-ordinate. This sets the position of the font’s
* baseline, not the very bottom of the character.
* @param int $color The start color. The color index. A negative color index
* disables antialiasing only on the fallback path, when no gradient is applied; when a gradient is applied only
* the color’s RGBA components are used. See imagecolorallocate().
* @param string $fontFilename The path to the TrueType font you wish to use.
* @param string $text The text string in UTF-8 encoding.
* @param array{linespacing?: float} $options The options passed through to imagettftext(). An array
* with a linespacing key holding a float value.
* @param ?int $gradientColor The finish color. A color identifier created with
* imagecolorallocate(). A negative color index has no effect on antialiasing here; only the color’s RGBA
* components are used. Passing NULL, the default, draws the text in a solid $color.
* @param bool $horizontalGradient Whether or not to use a horizontal gradient versus a
* vertical gradient.
*
* @return array<int, int>|false Returns an array with 8 elements representing four points making the bounding box
* of the text. The order of the points is lower left, lower right, upper right, upper left. The points are
* relative to the text regardless of the angle, so "upper left" means in the top left-hand corner when you see the
* text horizontally. Returns FALSE on error.
*/
public static function imagefttextgradient(
GdImage $image,
float $size,
float $angle,
int $x,
int $y,
int $color,
string $fontFilename,
string $text,
array $options = [],
?int $gradientColor = null,
bool $horizontalGradient = false
): array|false {
// Without a gradient color there is no gradient to draw, so fall back to imagettftext().
if ($gradientColor === null) {
return imagettftext($image, $size, $angle, $x, $y, $color, $fontFilename, $text, $options);
}
$renderedMask = self::renderTextMask($image, $size, $angle, $x, $y, $fontFilename, $text, $options);
if ($renderedMask === false) {
return false;
}
// The text’s own bounding box is what the gradient is measured against, so a pixel’s position within the text
// — not within the image — decides its color.
[$mask, $textBoundingBox] = $renderedMask;
$textLeft = min($textBoundingBox[0], $textBoundingBox[6]);
$textRight = max($textBoundingBox[2], $textBoundingBox[4]);
$textTop = min($textBoundingBox[5], $textBoundingBox[7]);
$textBottom = max($textBoundingBox[1], $textBoundingBox[3]);
$textWidth = $textRight - $textLeft;
$textHeight = $textBottom - $textTop;
$components = self::colorComponents($image, $color);
$gradientComponents = self::colorComponents($image, $gradientColor);
return self::plotTextMask(
$image,
$mask,
static function (
int $maskX,
int $maskY,
float $visibility
) use (
$image,
$components,
$gradientComponents,
$horizontalGradient,
$textLeft,
$textTop,
$textWidth,
$textHeight
): int|false {
// Calculate how far through the gradient this pixel sits, as a value between 0 and 1. Antialiased
// pixels can spill just outside the text’s bounding box, so the position is clamped to keep the
// interpolated color in range.
if ($horizontalGradient) {
$gradientPosition = $textWidth > 0 ? ($maskX - $textLeft) / $textWidth : 0.0;
} else {
$gradientPosition = $textHeight > 0 ? ($maskY - $textTop) / $textHeight : 0.0;
}
$gradientPosition = min(1.0, max(0.0, $gradientPosition));
$red = (int)round(
($components['red'] * (1 - $gradientPosition))
+ ($gradientComponents['red'] * $gradientPosition)
);
$green = (int)round(
($components['green'] * (1 - $gradientPosition))
+ ($gradientComponents['green'] * $gradientPosition)
);
$blue = (int)round(
($components['blue'] * (1 - $gradientPosition))
+ ($gradientComponents['blue'] * $gradientPosition)
);
$gradientAlpha = ($components['alpha'] * (1 - $gradientPosition))
+ ($gradientComponents['alpha'] * $gradientPosition);
return self::allocateColor(
$image,
$red,
$green,
$blue,
(int)round(
self::ALPHA_TRANSPARENT - (self::ALPHA_TRANSPARENT - $gradientAlpha) * $visibility
),
true
);
}
);
}
/**
* An alias of imagefttextgradient(), using the more common “ttf” spelling of imagettftext().
*
* @param GdImage $image A GdImage object, returned by one of the image creation
* functions, such as imagecreatetruecolor().
* @param float $size The font size in points.
* @param float $angle The angle in degrees.
* @param int $x The x-ordinate of the basepoint of the first character.
* @param int $y The y-ordinate of the font’s baseline.
* @param int $color The start color. See imagecolorallocate().
* @param string $fontFilename The path to the TrueType font you wish to use.
* @param string $text The text string in UTF-8 encoding.
* @param array{linespacing?: float} $options The options passed through to imagettftext().
* @param ?int $gradientColor The finish color. Passing NULL, the default, draws the text
* in a solid $color.
* @param bool $horizontalGradient Whether or not to use a horizontal gradient versus a
* vertical gradient.
*
* @alias imagefttextgradient()
*
* @return array<int, int>|false Returns an array with 8 elements representing four points making the bounding box
* of the text, or FALSE on error.
*/
public static function imagettftextgradient(
GdImage $image,
float $size,
float $angle,
int $x,
int $y,
int $color,
string $fontFilename,
string $text,
array $options = [],
?int $gradientColor = null,
bool $horizontalGradient = false
): array|false {
return self::imagefttextgradient(
$image,
$size,
$angle,
$x,
$y,
$color,
$fontFilename,
$text,
$options,
$gradientColor,
$horizontalGradient
);
}
/**
* Automatically inserts line breaks into text intended for imagettftext().
*
* Examples:
* ```
* <?php
*
* use AndrewGJohnson\AgjGd;
*
* // You can use linebreaksfortext() to add line breaks ("\n") to long strings to help format text when
* // using imagettftext()
* $text = 'This is a long sentence that could not fit on a single line.';
* $textWithLineBreaks = AgjGd::linebreaksfortext(20, 0, $font, $text, (int)(imagesx($image) * 0.8));
*
* // This will work but there will be no line breaks so your text will likely overflow horizontally
* imagettftext($image, 20, 0, (int)(imagesx($image) * 0.1), 0, $color, $font, $text);
*
* // This will work and you will not have to worry about text overflowing regardless of string length
* imagettftext($image, 20, 0, (int)(imagesx($image) * 0.1), 0, $color, $font, $textWithLineBreaks);
* ```
*
* @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.
* @param string $text The text string in UTF-8 encoding.
* @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.
*/
public static function linebreaksfortext(
float $size,
float $angle,
string $fontFilename,
string $text,
int $maximumWidth,
string $lineBreakCharacter = PHP_EOL,
bool $attemptToBreakOnHyphens = false,
bool $forceBreakOnSingleWords = false,
bool $preventWidows = false
): string {
// 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;
continue;
}
$addedWord = false;
// Check whether adding the new word to the current line still fits within the maximum width.
if (self::ttfWidthFits($size, $angle, $fontFilename, $currentLine . ' ' . $word, $maximumWidth)) {
$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;
if (self::ttfWidthFits($size, $angle, $fontFilename, $testLine, $maximumWidth)) {
$textWithLineBreaks .= $previousLine . $lineBreakCharacter;
$currentLine = $testLine;
$addedWord = true;
}
}
}
// Attempt to split the word on hyphens and fit as much of it as possible on the current line.
if (!$addedWord && $attemptToBreakOnHyphens && str_contains($word, '-')) {
$hyphenParts = explode('-', $word);
$rebuiltWord = '';
foreach ($hyphenParts as $index => $part) {
// Rebuild the word progressively, re-adding hyphens between parts.
$candidate = ($rebuiltWord === '' ? $part : $rebuiltWord . '-' . $part);
if (
self::ttfWidthFits(
$size,
$angle,
$fontFilename,
$currentLine . ' ' . $candidate,
$maximumWidth
)
) {
$rebuiltWord = $candidate;
continue;
}
// If we have something that fits, commit it.
if ($rebuiltWord !== '') {
$currentLine .= ' ' . $rebuiltWord . '-';
$textWithLineBreaks .= $currentLine . $lineBreakCharacter;
// Remaining parts become the next word.
$word = implode('-', array_slice($hyphenParts, $index));
$currentLine = $word;
$addedWord = true;
}
break;
}
}
if (!$addedWord && $forceBreakOnSingleWords) {
[$textWithLineBreaks, $currentLine, $addedWord] = self::forceBreakWord(
$size,
$angle,
$fontFilename,
$word,
$maximumWidth,
$lineBreakCharacter,
$textWithLineBreaks,
$currentLine
);
}
// If the word still has not been added, the text is too wide with the added word, so add a line break and
// start a new line with only that word.
if (!$addedWord) {
$textWithLineBreaks .= $currentLine . $lineBreakCharacter;
$currentLine = $word;
}
}
// Append the final line to the processed text.
return $textWithLineBreaks . $currentLine;
}
/**
* An alias of linebreaksfortext(), using the original standalone function’s name.
*
* @param float $size The font size in points.
* @param float $angle The angle in degrees.
* @param string $fontFilename The path to the TrueType font you wish to use.
* @param string $text The text string in UTF-8 encoding.
* @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.
*
* @alias linebreaksfortext()
*
* @return string Returns $text with line breaks added.
*/
public static function linebreaks4imagettftext(
float $size,
float $angle,
string $fontFilename,
string $text,
int $maximumWidth,
string $lineBreakCharacter = PHP_EOL,
bool $attemptToBreakOnHyphens = false,
bool $forceBreakOnSingleWords = false,
bool $preventWidows = false
): string {
return self::linebreaksfortext(
$size,
$angle,
$fontFilename,
$text,
$maximumWidth,
$lineBreakCharacter,
$attemptToBreakOnHyphens,
$forceBreakOnSingleWords,
$preventWidows
);
}
/**
* An alias of linebreaksfortext(), pairing the “4image” naming with the “ft” spelling of imagefttext().
*
* @param float $size The font size in points.
* @param float $angle The angle in degrees.
* @param string $fontFilename The path to the TrueType font you wish to use.
* @param string $text The text string in UTF-8 encoding.
* @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.
*
* @alias linebreaksfortext()
*
* @return string Returns $text with line breaks added.
*/
public static function linebreaks4imagefttext(
float $size,
float $angle,
string $fontFilename,
string $text,
int $maximumWidth,
string $lineBreakCharacter = PHP_EOL,
bool $attemptToBreakOnHyphens = false,
bool $forceBreakOnSingleWords = false,
bool $preventWidows = false
): string {
return self::linebreaksfortext(
$size,
$angle,
$fontFilename,
$text,
$maximumWidth,
$lineBreakCharacter,
$attemptToBreakOnHyphens,
$forceBreakOnSingleWords,
$preventWidows
);
}
/**
* An alias of linebreaksfortext(), using a shorter name for the same behaviour.
*
* @param float $size The font size in points.
* @param float $angle The angle in degrees.
* @param string $fontFilename The path to the TrueType font you wish to use.
* @param string $text The text string in UTF-8 encoding.
* @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.
*
* @alias linebreaksfortext()
*
* @return string Returns $text with line breaks added.
*/
public static function linebreaks4text(
float $size,
float $angle,
string $fontFilename,
string $text,
int $maximumWidth,
string $lineBreakCharacter = PHP_EOL,
bool $attemptToBreakOnHyphens = false,
bool $forceBreakOnSingleWords = false,
bool $preventWidows = false
): string {
return self::linebreaksfortext(
$size,
$angle,
$fontFilename,
$text,
$maximumWidth,
$lineBreakCharacter,
$attemptToBreakOnHyphens,
$forceBreakOnSingleWords,
$preventWidows
);
}
/**
* An alias of linebreaksfortext(), pairing the “for” naming with the “ft” spelling of imagefttext().
*
* @param float $size The font size in points.
* @param float $angle The angle in degrees.
* @param string $fontFilename The path to the TrueType font you wish to use.
* @param string $text The text string in UTF-8 encoding.
* @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.
*
* @alias linebreaksfortext()
*
* @return string Returns $text with line breaks added.
*/
public static function linebreaksforimagefttext(
float $size,
float $angle,
string $fontFilename,
string $text,
int $maximumWidth,
string $lineBreakCharacter = PHP_EOL,
bool $attemptToBreakOnHyphens = false,
bool $forceBreakOnSingleWords = false,
bool $preventWidows = false
): string {
return self::linebreaksfortext(
$size,
$angle,
$fontFilename,
$text,
$maximumWidth,
$lineBreakCharacter,
$attemptToBreakOnHyphens,
$forceBreakOnSingleWords,
$preventWidows
);
}
/**
* An alias of linebreaksfortext(), pairing the “for” naming with the “ttf” spelling of imagettftext().
*
* @param float $size The font size in points.
* @param float $angle The angle in degrees.
* @param string $fontFilename The path to the TrueType font you wish to use.
* @param string $text The text string in UTF-8 encoding.
* @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.
*
* @alias linebreaksfortext()
*
* @return string Returns $text with line breaks added.
*/
public static function linebreaksforimagettftext(
float $size,
float $angle,
string $fontFilename,
string $text,
int $maximumWidth,
string $lineBreakCharacter = PHP_EOL,
bool $attemptToBreakOnHyphens = false,
bool $forceBreakOnSingleWords = false,
bool $preventWidows = false
): string {
return self::linebreaksfortext(
$size,
$angle,
$fontFilename,
$text,
$maximumWidth,
$lineBreakCharacter,
$attemptToBreakOnHyphens,
$forceBreakOnSingleWords,
$preventWidows
);
}
/**
* Breaks a single word that is too wide for one line across as many lines as it needs, hyphenating each segment.
*
* @param float $size The font size in points.
* @param float $angle The angle in degrees.
* @param string $fontFilename The path to the TrueType font you wish to use.
* @param string $word The word to force a break into.
* @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 string $textWithLineBreaks The processed text so far.
* @param string $currentLine The line currently being built.
*
* @return array{0: string, 1: string, 2: bool} The processed text, the current line and whether the word was
* added.
*/
private static function forceBreakWord(
float $size,
float $angle,
string $fontFilename,
string $word,
int $maximumWidth,
string $lineBreakCharacter,
string $textWithLineBreaks,
string $currentLine
): array {
// A word is only ever force-broken from the start of a line, so commit whatever the current line already holds
// and work from an empty one. Every segment below is therefore measured on its own.
if ($currentLine !== '') {
$textWithLineBreaks .= $currentLine . $lineBreakCharacter;
}
$remainingCharacters = preg_split('//u', $word, -1, PREG_SPLIT_NO_EMPTY);
if ($remainingCharacters === false) {
// The word cannot be split (invalid UTF-8). The current line was already committed above, so leave the
// word whole as the new current line and report it as handled — otherwise the caller's !$addedWord
// fallback appends a second line break against an empty line, producing a spurious blank line.
return [$textWithLineBreaks, $word, true];
}
while (count($remainingCharacters) > 0) {
// Take as many characters as will fit on a line, one at a time.
$characterCount = 0;
$candidateWord = '';
foreach ($remainingCharacters as $index => $character) {
$testCandidateWord = $candidateWord . $character;
// Every segment but the last one gets a trailing hyphen, so the hyphen has to fit on the line too.
$hasRemainingCharacters = ($index < count($remainingCharacters) - 1);
$testLine = $testCandidateWord . ($hasRemainingCharacters ? '-' : '');
if (!self::ttfWidthFits($size, $angle, $fontFilename, $testLine, $maximumWidth)) {
break;
}
$candidateWord = $testCandidateWord;
$characterCount++;
}
// If not even a single character and a hyphen fit on an empty line, no break is possible at all, so take
// the whole remainder to avoid looping forever.
if ($candidateWord === '') {
return [$textWithLineBreaks, implode('', $remainingCharacters), true];
}
$remainingCharacters = array_slice($remainingCharacters, $characterCount);
// The final segment stays on the current line so that the words after it can still join it.
if (count($remainingCharacters) === 0) {
return [$textWithLineBreaks, $candidateWord, true];
}
// More characters remain, so hyphenate this segment and commit it as its own line.
$textWithLineBreaks .= $candidateWord . '-' . $lineBreakCharacter;
}
return [$textWithLineBreaks, '', true];
}
/**
* Allocates a color, using an RGBA color identifier when the color is translucent and an RGB color identifier
* otherwise.
*
* @param GdImage $image A GdImage object.
* @param int $red A value between 0 and 255.
* @param int $green A value between 0 and 255.
* @param int $blue A value between 0 and 255.
* @param int $alpha A value between 0 and 127. 0 indicates completely opaque while 127 indicates
* completely transparent.
* @param bool $forceAlphaChannel Whether to allocate an RGBA color identifier even when $alpha is opaque.
*
* @return int|false Returns a color identifier or FALSE if the allocation failed.
*/
private static function allocateColor(
GdImage $image,
int $red,
int $green,
int $blue,
int $alpha = self::ALPHA_OPAQUE,
bool $forceAlphaChannel = false
): int|false {
// GD raises a ValueError for a component outside its range, and the blending and gradient math rounds floats,
// so every component is clamped here rather than trusted.
$red = max(0, min(self::COLOR_COMPONENT_MAX, $red));
$green = max(0, min(self::COLOR_COMPONENT_MAX, $green));
$blue = max(0, min(self::COLOR_COMPONENT_MAX, $blue));
$alpha = max(self::ALPHA_OPAQUE, min(self::ALPHA_TRANSPARENT, $alpha));
if ($forceAlphaChannel || $alpha > self::ALPHA_OPAQUE) {
return imagecolorallocatealpha($image, $red, $green, $blue, $alpha);
}
return imagecolorallocate($image, $red, $green, $blue);
}
/**
* Resolves a color identifier into its red, green, blue and alpha components.
*
* A negative color index turns off antialiasing in imagettftext(), so the sign is dropped before the components
* are read.
*
* @param GdImage $image A GdImage object.
* @param int $color A color identifier created with imagecolorallocate().
*
* @return array{red: int, green: int, blue: int, alpha: int} The color’s components.
*/
private static function colorComponents(GdImage $image, int $color): array
{
return imagecolorsforindex($image, abs($color));
}
/**
* Renders white text on a black background in an image the same size as $image.
*
* Both imagefttextfilter() and imagefttextgradient() work by rendering the text once into this grayscale mask and
* then reading each pixel’s brightness back out as that pixel’s visibility, which is what lets them recolor the
* antialiased edges of the glyphs.
*
* @param GdImage $image A GdImage object.
* @param float $size The font size in points.
* @param float $angle The angle in degrees.
* @param int $x The x-ordinate of the basepoint of the first character.
* @param int $y The y-ordinate of the font’s baseline.
* @param string $fontFilename The path to the TrueType font you wish to use.
* @param string $text The text string in UTF-8 encoding.
* @param array{linespacing?: float} $options The options passed through to imagettftext().
*
* @return array{0: GdImage, 1: array<int, int>}|false Returns the mask and the text’s bounding box, as reported by
* imagettftext(), or FALSE if the mask could not be rendered.
*/
private static function renderTextMask(
GdImage $image,
float $size,
float $angle,
int $x,
int $y,
string $fontFilename,
string $text,
array $options
): array|false {
$mask = imagecreatetruecolor(imagesx($image), imagesy($image));
// The dimensions come from an existing image so are always >= 1; the only route to false is memory
// exhaustion, which cannot be triggered deterministically in a test.
// @codeCoverageIgnoreStart
if ($mask === false) {
return false;
}
// @codeCoverageIgnoreEnd
$black = imagecolorallocate($mask, 0x00, 0x00, 0x00);
$white = imagecolorallocate($mask, 0xFF, 0xFF, 0xFF);
// imagecolorallocate() only returns false when a palette image runs out of slots, and $mask is a truecolor
// image with no palette; the literal components cannot be out of range.
// @codeCoverageIgnoreStart
if ($black === false || $white === false) {
return false;
}
// @codeCoverageIgnoreEnd
imagefill($mask, 0, 0, $black);
$textBoundingBox = imagettftext($mask, $size, $angle, $x, $y, $white, $fontFilename, $text, $options);
if ($textBoundingBox === false) {
return false;
}
return [$mask, $textBoundingBox];
}
/**
* Copies every visible pixel of a text mask onto an image, coloring each one with $allocatePixelColor.
*
* @param GdImage $image A GdImage object.
* @param GdImage $mask A mask from renderTextMask().
* @param callable(int, int, float): (int|false) $allocatePixelColor Receives the pixel’s x-ordinate, y-ordinate
* and visibility, and returns the color identifier to set that pixel to.
* @param float $visibilityScale Scales every pixel’s visibility. A scale of
* zero makes every pixel invisible, which leaves $image untouched and returns FALSE.
*
* @return array<int, int>|false Returns the bounding box of the pixels that were set, or FALSE if no pixel was
* visible.
*/
private static function plotTextMask(
GdImage $image,
GdImage $mask,
callable $allocatePixelColor,
float $visibilityScale = 1.0
): array|false {
// The bounding box starts inverted — every coordinate at the opposite extreme — so that the first visible
// pixel replaces all eight values and each pixel after it can only widen the box.
$emptyBoundingBox = [
imagesx($image), // Lower left (x coordinate)
-1, // Lower left (y coordinate)
-1, // Lower right (x coordinate)
-1, // Lower right (y coordinate)
-1, // Upper right (x coordinate)
imagesy($image), // Upper right (y coordinate)
imagesx($image), // Upper left (x coordinate)
imagesy($image) // Upper left (y coordinate)
];
$boundingBox = $emptyBoundingBox;
$width = imagesx($mask);
$height = imagesy($mask);
for ($maskX = 0; $maskX < $width; $maskX++) {
for ($maskY = 0; $maskY < $height; $maskY++) {
// The mask is grayscale, so any one channel is the pixel’s brightness, which is its visibility.
$brightness = (int)imagecolorat($mask, $maskX, $maskY) & self::COLOR_COMPONENT_MAX;
$visibility = $brightness / self::COLOR_COMPONENT_MAX * $visibilityScale;
if ($visibility <= 0) {
continue;
}
$boundingBox = [
min($boundingBox[0], $maskX),
max($boundingBox[1], $maskY),
max($boundingBox[2], $maskX),
max($boundingBox[3], $maskY),
max($boundingBox[4], $maskX),
min($boundingBox[5], $maskY),
min($boundingBox[6], $maskX),
min($boundingBox[7], $maskY)
];
$pixelColor = $allocatePixelColor($maskX, $maskY, $visibility);
if ($pixelColor !== false) {
imagesetpixel($image, $maskX, $maskY, $pixelColor);
}
}
}
// An unchanged bounding box means no pixel was ever visible, which is a failure.
if ($boundingBox === $emptyBoundingBox) {
return false;
}
return $boundingBox;
}
/**
* Calculates whether a string rendered by imagettftext() fits within a maximum width.
*
* A string whose width cannot be measured is treated as fitting, which leaves the text unbroken rather than
* breaking it at every word.
*
* @param float $size The font size in points.
* @param float $angle The angle in degrees.
* @param string $fontFilename The path to the TrueType font you wish to use.
* @param string $text The text string in UTF-8 encoding.
* @param int $maximumWidth The maximum width in pixels.
*
* @return bool Returns TRUE if the text fits within $maximumWidth.
*/
private static function ttfWidthFits(
float $size,
float $angle,
string $fontFilename,
string $text,
int $maximumWidth
): bool {
$boundingBox = imagettfbbox($size, $angle, $fontFilename, $text);
if ($boundingBox === false) {
return true;
}
$left = min($boundingBox[0], $boundingBox[2], $boundingBox[4], $boundingBox[6]);
$right = max($boundingBox[0], $boundingBox[2], $boundingBox[4], $boundingBox[6]);
return ($right - $left) <= $maximumWidth;
}
}