PHPUnit Test Suite Source
There is one test suite per method.
ImageBlendedColorAllocateTest.php
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace AndrewGJohnson\AgjGd\Tests;
use AndrewGJohnson\AgjGd;
use PHPUnit\Framework\TestCase;
class ImageBlendedColorAllocateTest extends TestCase
{
private \GdImage $image;
protected function setUp(): void
{
$this->image = imagecreatetruecolor(100, 100);
}
public function testEvenBlend(): void
{
$red = imagecolorallocate($this->image, 0xFF, 0x00, 0x00);
$yellow = imagecolorallocate($this->image, 0xFF, 0xFF, 0x00);
$result = AgjGd::imageblendedcolorallocate($this->image, $red, $yellow);
$this->assertNotFalse($result);
$components = imagecolorsforindex($this->image, $result);
$this->assertSame(255, $components['red']);
$this->assertSame(128, $components['green']); // round(127.5) = 128
$this->assertSame(0, $components['blue']);
$this->assertSame(0, $components['alpha']);
}
public function testCustomRatioBlend(): void
{
$blue = imagecolorallocate($this->image, 0x00, 0x00, 0xFF);
$cyan = imagecolorallocate($this->image, 0x00, 0xFF, 0xFF);
$result = AgjGd::imageblendedcolorallocate($this->image, $blue, $cyan, 0.25);
$this->assertNotFalse($result);
$components = imagecolorsforindex($this->image, $result);
$this->assertSame(0, $components['red']);
$this->assertSame(191, $components['green']); // round(255 * 0.75) = 191
$this->assertSame(255, $components['blue']);
$this->assertSame(0, $components['alpha']);
}
public function testFullColor1Blend(): void
{
$red = imagecolorallocate($this->image, 0xFF, 0x00, 0x00);
$blue = imagecolorallocate($this->image, 0x00, 0x00, 0xFF);
$result = AgjGd::imageblendedcolorallocate($this->image, $red, $blue, 1.0);
$this->assertNotFalse($result);
$components = imagecolorsforindex($this->image, $result);
$this->assertSame(255, $components['red']);
$this->assertSame(0, $components['green']);
$this->assertSame(0, $components['blue']);
$this->assertSame(0, $components['alpha']);
}
public function testFullColor2Blend(): void
{
$red = imagecolorallocate($this->image, 0xFF, 0x00, 0x00);
$blue = imagecolorallocate($this->image, 0x00, 0x00, 0xFF);
$result = AgjGd::imageblendedcolorallocate($this->image, $red, $blue, 0.0);
$this->assertNotFalse($result);
$components = imagecolorsforindex($this->image, $result);
$this->assertSame(0, $components['red']);
$this->assertSame(0, $components['green']);
$this->assertSame(255, $components['blue']);
$this->assertSame(0, $components['alpha']);
}
public function testOutOfRangeOpacityFallsBackToEvenBlend(): void
{
$red = imagecolorallocate($this->image, 0xFF, 0x00, 0x00);
$yellow = imagecolorallocate($this->image, 0xFF, 0xFF, 0x00);
$result = AgjGd::imageblendedcolorallocate($this->image, $red, $yellow, 1.5);
$this->assertNotFalse($result);
$components = imagecolorsforindex($this->image, $result);
$this->assertSame(255, $components['red']);
$this->assertSame(128, $components['green']); // falls back to 50/50, same as testEvenBlend
$this->assertSame(0, $components['blue']);
$this->assertSame(0, $components['alpha']);
}
public function testNegativeOpacityFallsBackToEvenBlend(): void
{
$red = imagecolorallocate($this->image, 0xFF, 0x00, 0x00);
$yellow = imagecolorallocate($this->image, 0xFF, 0xFF, 0x00);
$result = AgjGd::imageblendedcolorallocate($this->image, $red, $yellow, -0.1);
$this->assertNotFalse($result);
$components = imagecolorsforindex($this->image, $result);
$this->assertSame(255, $components['red']);
$this->assertSame(128, $components['green']); // falls back to 50/50, same as testEvenBlend
$this->assertSame(0, $components['blue']);
$this->assertSame(0, $components['alpha']);
}
public function testAlphaBlend(): void
{
$opaqueBlack = imagecolorallocatealpha($this->image, 0x00, 0x00, 0x00, 0);
$translucentBlack = imagecolorallocatealpha($this->image, 0x00, 0x00, 0x00, 63);
$result = AgjGd::imageblendedcolorallocate($this->image, $opaqueBlack, $translucentBlack);
$this->assertNotFalse($result);
$components = imagecolorsforindex($this->image, $result);
$this->assertSame(0, $components['red']);
$this->assertSame(0, $components['green']);
$this->assertSame(0, $components['blue']);
$this->assertSame(32, $components['alpha']); // round(63 * 0.5) = 32
}
public function testInvalidColor1ReturnsFalse(): void
{
$blue = imagecolorallocate($this->image, 0x00, 0x00, 0xFF);
$this->assertFalse(AgjGd::imageblendedcolorallocate($this->image, false, $blue));
}
public function testInvalidColor2ReturnsFalse(): void
{
$red = imagecolorallocate($this->image, 0xFF, 0x00, 0x00);
$this->assertFalse(AgjGd::imageblendedcolorallocate($this->image, $red, false));
}
}
ImageColorAllocateFromStringTest.php
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace AndrewGJohnson\AgjGd\Tests;
use AndrewGJohnson\AgjGd;
use InvalidArgumentException;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
class ImageColorAllocateFromStringTest extends TestCase
{
private \GdImage $image;
protected function setUp(): void
{
$this->image = imagecreatetruecolor(100, 100);
}
public function testHexSixDigitUppercase(): void
{
$components = $this->componentsFor('#FF0000');
$this->assertSame(255, $components['red']);
$this->assertSame(0, $components['green']);
$this->assertSame(0, $components['blue']);
$this->assertSame(0, $components['alpha']);
}
public function testHexSixDigitLowercase(): void
{
$components = $this->componentsFor('#ff0000');
$this->assertSame(255, $components['red']);
$this->assertSame(0, $components['green']);
$this->assertSame(0, $components['blue']);
$this->assertSame(0, $components['alpha']);
}
public function testHexSixDigitWithoutHash(): void
{
$components = $this->componentsFor('ff0000');
$this->assertSame(255, $components['red']);
$this->assertSame(0, $components['green']);
$this->assertSame(0, $components['blue']);
$this->assertSame(0, $components['alpha']);
}
public function testHexThreeDigit(): void
{
$components = $this->componentsFor('#f00');
$this->assertSame(255, $components['red']);
$this->assertSame(0, $components['green']);
$this->assertSame(0, $components['blue']);
$this->assertSame(0, $components['alpha']);
}
public function testRgbWithCommas(): void
{
$components = $this->componentsFor('rgb(0, 255, 0)');
$this->assertSame(0, $components['red']);
$this->assertSame(255, $components['green']);
$this->assertSame(0, $components['blue']);
$this->assertSame(0, $components['alpha']);
}
public function testRgbWithSpaces(): void
{
$components = $this->componentsFor('rgb(0 255 0)');
$this->assertSame(0, $components['red']);
$this->assertSame(255, $components['green']);
$this->assertSame(0, $components['blue']);
$this->assertSame(0, $components['alpha']);
}
public function testRgbaOpaqueDecimalAlpha(): void
{
$components = $this->componentsFor('rgba(0, 0, 255, 1)');
$this->assertSame(0, $components['red']);
$this->assertSame(0, $components['green']);
$this->assertSame(255, $components['blue']);
$this->assertSame(0, $components['alpha']); // CSS alpha 1 (opaque) → GD alpha 0
}
public function testRgbaOpaqueDecimalAlphaWithTrailingZero(): void
{
// "1.0" is a valid CSS opacity; the alpha grammar must accept it, not just the bare "1".
$components = $this->componentsFor('rgba(0, 0, 255, 1.0)');
$this->assertSame(0, $components['alpha']); // 127 - round(127 * 1.0) = 0 (fully opaque)
}
public function testRgbaTransparentDecimalAlpha(): void
{
$components = $this->componentsFor('rgba(0, 0, 255, 0)');
$this->assertSame(127, $components['alpha']); // CSS alpha 0 (transparent) → GD alpha 127
}
public function testRgbaHalfTransparentDecimalAlpha(): void
{
$components = $this->componentsFor('rgba(0, 0, 255, 0.5)');
$this->assertSame(63, $components['alpha']); // 127 - round(127 * 0.5) = 127 - 64 = 63
}
public function testRgbaOpaquePercentageAlpha(): void
{
$components = $this->componentsFor('rgba(0 255 0 / 100%)');
$this->assertSame(0, $components['red']);
$this->assertSame(255, $components['green']);
$this->assertSame(0, $components['blue']);
$this->assertSame(0, $components['alpha']); // CSS alpha 100% (opaque) → GD alpha 0
}
public function testRgbaTransparentPercentageAlpha(): void
{
$components = $this->componentsFor('rgba(0 255 0 / 0%)');
$this->assertSame(127, $components['alpha']); // CSS alpha 0% (transparent) → GD alpha 127
}
/**
* @return array<string, array{0: string, 1: int, 2: int, 3: int}>
*/
public static function cssKeywordProvider(): array
{
return [
'red' => ['red', 255, 0, 0 ],
'green' => ['green', 0, 128, 0 ],
'blue' => ['blue', 0, 0, 255],
'white' => ['white', 255, 255, 255],
'black' => ['black', 0, 0, 0 ],
'lime' => ['lime', 0, 255, 0 ],
];
}
#[DataProvider('cssKeywordProvider')]
public function testCssColorKeyword(string $keyword, int $red, int $green, int $blue): void
{
$components = $this->componentsFor($keyword);
$this->assertSame($red, $components['red']);
$this->assertSame($green, $components['green']);
$this->assertSame($blue, $components['blue']);
}
public function testCssKeywordCaseInsensitive(): void
{
$components = $this->componentsFor('RED');
$this->assertSame(255, $components['red']);
$this->assertSame(0, $components['green']);
$this->assertSame(0, $components['blue']);
}
public function testAlphaParameter(): void
{
$components = $this->componentsFor('#0000ff', 64);
$this->assertSame(0, $components['red']);
$this->assertSame(0, $components['green']);
$this->assertSame(255, $components['blue']);
$this->assertSame(64, $components['alpha']);
}
public function testLeadingAndTrailingWhitespaceTrimmed(): void
{
$components = $this->componentsFor(' #FF0000 ');
$this->assertSame(255, $components['red']);
$this->assertSame(0, $components['green']);
$this->assertSame(0, $components['blue']);
}
public function testInvalidStringThrowsException(): void
{
$this->expectException(InvalidArgumentException::class);
AgjGd::imagecolorallocatefromstring($this->image, 'notacolor');
}
public function testOutOfRangeRgbThrowsException(): void
{
$this->expectException(InvalidArgumentException::class);
AgjGd::imagecolorallocatefromstring($this->image, 'rgb(999, 0, 0)');
}
public function testInvalidAlphaParameterThrowsException(): void
{
$this->expectException(InvalidArgumentException::class);
AgjGd::imagecolorallocatefromstring($this->image, '#ff0000', 128);
}
public function testNegativeAlphaParameterThrowsException(): void
{
$this->expectException(InvalidArgumentException::class);
AgjGd::imagecolorallocatefromstring($this->image, '#ff0000', -1);
}
public function testInvalidRgbaAlphaValueThrowsException(): void
{
$this->expectException(InvalidArgumentException::class);
AgjGd::imagecolorallocatefromstring($this->image, 'rgba(255, 0, 0, 1.5)');
}
public function testOutOfRangePercentageAlphaThrows(): void
{
// 150% is valid rgba() syntax but converts to a 1.5 opacity, which is outside the 0-1 range.
$this->expectException(InvalidArgumentException::class);
AgjGd::imagecolorallocatefromstring($this->image, 'rgba(0 0 0 / 150%)');
}
/**
* @return array{red: int, green: int, blue: int, alpha: int}
*/
private function componentsFor(string $string, int $alpha = 0): array
{
$color = AgjGd::imagecolorallocatefromstring($this->image, $string, $alpha);
$this->assertNotFalse($color);
return imagecolorsforindex($this->image, $color);
}
}
ImageFtTextFilterTest.php
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace AndrewGJohnson\AgjGd\Tests;
use AndrewGJohnson\AgjGd;
use PHPUnit\Framework\TestCase;
class ImageFtTextFilterTest extends TestCase
{
private const FILTER_INTENSITY = 1;
private const FONT_ANGLE = 0;
private const FONT_PATH = __DIR__ . '/NotoSans-Regular.ttf';
private const FONT_SIZE = 12;
private const FONT_X = 10;
private const FONT_Y = 50;
private const IMAGE_WIDTH = 200;
private const IMAGE_HEIGHT = 100;
public function testReturnsArrayWithFilter(): void
{
$image = $this->createImage();
$result = AgjGd::imagefttextfilter(
$image,
self::FONT_SIZE,
self::FONT_ANGLE,
self::FONT_X,
self::FONT_Y,
(int)imagecolorallocate($image, 0, 0, 0),
self::FONT_PATH,
'Hello world!',
[],
self::FILTER_INTENSITY
);
$this->assertIsArray($result);
$this->assertCount(8, $result);
}
public function testReturnsArrayWithoutFilter(): void
{
$image = $this->createImage();
$result = AgjGd::imagefttextfilter(
$image,
self::FONT_SIZE,
self::FONT_ANGLE,
self::FONT_X,
self::FONT_Y,
(int)imagecolorallocate($image, 0, 0, 0),
self::FONT_PATH,
'Hello world!'
);
$this->assertIsArray($result);
$this->assertCount(8, $result);
}
public function testBoundingBoxWithinImageBounds(): void
{
$image = $this->createImage();
$result = AgjGd::imagefttextfilter(
$image,
self::FONT_SIZE,
self::FONT_ANGLE,
self::FONT_X,
self::FONT_Y,
(int)imagecolorallocate($image, 0, 0, 0),
self::FONT_PATH,
'Hello world!',
[],
self::FILTER_INTENSITY
);
$this->assertIsArray($result);
foreach ([0, 2, 4, 6] as $index) {
$this->assertGreaterThanOrEqual(0, $result[$index]);
$this->assertLessThanOrEqual(self::IMAGE_WIDTH, $result[$index]);
}
foreach ([1, 3, 5, 7] as $index) {
$this->assertGreaterThanOrEqual(0, $result[$index]);
$this->assertLessThanOrEqual(self::IMAGE_HEIGHT, $result[$index]);
}
}
public function testFullyTransparentColorReturnsFalse(): void
{
$image = $this->createImage();
$result = AgjGd::imagefttextfilter(
$image,
self::FONT_SIZE,
self::FONT_ANGLE,
self::FONT_X,
self::FONT_Y,
(int)imagecolorallocatealpha($image, 0, 0, 0, 127), // 127 = completely transparent
self::FONT_PATH,
'Hello world!',
[],
self::FILTER_INTENSITY
);
$this->assertFalse($result);
}
public function testInvalidFontReturnsFalse(): void
{
$image = $this->createImage();
// @ suppresses the GD warning emitted by imagettftext() when the font is missing
$result = @AgjGd::imagefttextfilter(
$image,
self::FONT_SIZE,
self::FONT_ANGLE,
self::FONT_X,
self::FONT_Y,
(int)imagecolorallocate($image, 0, 0, 0),
'/nonexistent/font.ttf',
'Hello world!',
[],
self::FILTER_INTENSITY
);
$this->assertFalse($result);
}
public function testImageIsModifiedByUse(): void
{
$image = $this->createImage();
$backgroundColor = (int)imagecolorallocate($image, 255, 255, 255); // RGB(255,255,255) = white
imagefill($image, 0, 0, $backgroundColor);
AgjGd::imagefttextfilter(
$image,
self::FONT_SIZE,
self::FONT_ANGLE,
self::FONT_X,
self::FONT_Y,
(int)imagecolorallocate($image, 0, 0, 0),
self::FONT_PATH,
'Hello world!',
[],
self::FILTER_INTENSITY
);
// At least one pixel somewhere in the image must differ from the background
$modified = false;
for ($x = 0; $x < self::IMAGE_WIDTH && !$modified; $x++) {
for ($y = 0; $y < self::IMAGE_HEIGHT && !$modified; $y++) {
if (imagecolorat($image, $x, $y) !== $backgroundColor) {
$modified = true;
}
}
}
$this->assertTrue($modified);
}
public function testFilterSpreadsTextBeyondTheUnfilteredBoundingBox(): void
{
$unfilteredImage = $this->createImage();
$filteredImage = $this->createImage();
$unfiltered = AgjGd::imagefttextfilter(
$unfilteredImage,
self::FONT_SIZE,
self::FONT_ANGLE,
self::FONT_X,
self::FONT_Y,
(int)imagecolorallocate($unfilteredImage, 0, 0, 0),
self::FONT_PATH,
'Hello world!'
);
$filtered = AgjGd::imagefttextfilter(
$filteredImage,
self::FONT_SIZE,
self::FONT_ANGLE,
self::FONT_X,
self::FONT_Y,
(int)imagecolorallocate($filteredImage, 0, 0, 0),
self::FONT_PATH,
'Hello world!',
[],
10
);
$this->assertIsArray($unfiltered);
$this->assertIsArray($filtered);
// A heavy Gaussian blur bleeds the glyphs outwards, so the filtered bounding box must reach further left and
// right than the bounding box imagettftext() reports for the same unfiltered text.
$this->assertLessThan($unfiltered[0], $filtered[0], 'The filtered text should reach further left');
$this->assertGreaterThan($unfiltered[2], $filtered[2], 'The filtered text should reach further right');
}
public function testImageTtfTextFilterAliasMatchesTheCanonicalMethod(): void
{
$canonical = $this->createImage();
$alias = $this->createImage();
$canonicalBox = AgjGd::imagefttextfilter(
$canonical,
self::FONT_SIZE,
self::FONT_ANGLE,
self::FONT_X,
self::FONT_Y,
(int)imagecolorallocate($canonical, 0, 0, 0),
self::FONT_PATH,
'Hello world!',
[],
10
);
$aliasBox = AgjGd::imagettftextfilter(
$alias,
self::FONT_SIZE,
self::FONT_ANGLE,
self::FONT_X,
self::FONT_Y,
(int)imagecolorallocate($alias, 0, 0, 0),
self::FONT_PATH,
'Hello world!',
[],
10
);
$this->assertSame($canonicalBox, $aliasBox);
$this->assertTrue($this->imagesAreIdentical($canonical, $alias));
}
public function testImageTtfTextBlurAliasMatchesTheCanonicalMethod(): void
{
$canonical = $this->createImage();
$alias = $this->createImage();
$canonicalBox = AgjGd::imagefttextfilter(
$canonical,
self::FONT_SIZE,
self::FONT_ANGLE,
self::FONT_X,
self::FONT_Y,
(int)imagecolorallocate($canonical, 0, 0, 0),
self::FONT_PATH,
'Hello world!',
[],
10
);
$aliasBox = AgjGd::imagettftextblur(
$alias,
self::FONT_SIZE,
self::FONT_ANGLE,
self::FONT_X,
self::FONT_Y,
(int)imagecolorallocate($alias, 0, 0, 0),
self::FONT_PATH,
'Hello world!',
[],
10
);
$this->assertSame($canonicalBox, $aliasBox);
$this->assertTrue($this->imagesAreIdentical($canonical, $alias));
}
public function testImageFtTextBlurAliasMatchesTheCanonicalMethod(): void
{
$canonical = $this->createImage();
$alias = $this->createImage();
$canonicalBox = AgjGd::imagefttextfilter(
$canonical,
self::FONT_SIZE,
self::FONT_ANGLE,
self::FONT_X,
self::FONT_Y,
(int)imagecolorallocate($canonical, 0, 0, 0),
self::FONT_PATH,
'Hello world!',
[],
10
);
$aliasBox = AgjGd::imagefttextblur(
$alias,
self::FONT_SIZE,
self::FONT_ANGLE,
self::FONT_X,
self::FONT_Y,
(int)imagecolorallocate($alias, 0, 0, 0),
self::FONT_PATH,
'Hello world!',
[],
10
);
$this->assertSame($canonicalBox, $aliasBox);
$this->assertTrue($this->imagesAreIdentical($canonical, $alias));
}
private function imagesAreIdentical(\GdImage $a, \GdImage $b): bool
{
for ($x = 0; $x < self::IMAGE_WIDTH; $x++) {
for ($y = 0; $y < self::IMAGE_HEIGHT; $y++) {
if (imagecolorat($a, $x, $y) !== imagecolorat($b, $x, $y)) {
return false;
}
}
}
return true;
}
private function createImage(): \GdImage
{
$image = imagecreatetruecolor(self::IMAGE_WIDTH, self::IMAGE_HEIGHT);
$this->assertNotFalse($image);
return $image;
}
}
ImageFtTextGradientTest.php
<?php
/**
* Copyright (c) 2017–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.
*/
declare(strict_types=1);
namespace AndrewGJohnson\AgjGd\Tests;
use AndrewGJohnson\AgjGd;
use PHPUnit\Framework\TestCase;
class ImageFtTextGradientTest extends TestCase
{
private const FONT_ANGLE = 0;
private const FONT_PATH = __DIR__ . '/NotoSans-Regular.ttf';
private const FONT_SIZE = 40;
private const FONT_X = 10;
private const FONT_Y = 120;
private const IMAGE_WIDTH = 400;
private const IMAGE_HEIGHT = 200;
public function testReturnsArrayWithEightElementsOnSuccess(): void
{
$image = $this->createImage();
$result = AgjGd::imagefttextgradient(
$image,
self::FONT_SIZE,
self::FONT_ANGLE,
self::FONT_X,
self::FONT_Y,
(int)imagecolorallocate($image, 0xFF, 0x00, 0x00),
self::FONT_PATH,
'Hello',
[],
(int)imagecolorallocate($image, 0x00, 0x00, 0xFF)
);
$this->assertIsArray($result);
$this->assertCount(8, $result);
}
public function testBoundingBoxCoordinatesAreWithinImageBounds(): void
{
$image = $this->createImage();
$result = AgjGd::imagefttextgradient(
$image,
self::FONT_SIZE,
self::FONT_ANGLE,
self::FONT_X,
self::FONT_Y,
(int)imagecolorallocate($image, 0xFF, 0x00, 0x00),
self::FONT_PATH,
'Hello',
[],
(int)imagecolorallocate($image, 0x00, 0x00, 0xFF)
);
$this->assertIsArray($result);
// Even indices are x coordinates, odd indices are y coordinates
for ($index = 0; $index < 8; $index += 2) {
$this->assertGreaterThanOrEqual(0, $result[$index]);
$this->assertLessThan(self::IMAGE_WIDTH, $result[$index]);
}
for ($index = 1; $index < 8; $index += 2) {
$this->assertGreaterThanOrEqual(0, $result[$index]);
$this->assertLessThan(self::IMAGE_HEIGHT, $result[$index]);
}
}
public function testReturnsFalseForInvalidFont(): void
{
$image = $this->createImage();
// @ suppresses the GD warning emitted by imagettftext() when the font is missing
$result = @AgjGd::imagefttextgradient(
$image,
self::FONT_SIZE,
self::FONT_ANGLE,
self::FONT_X,
self::FONT_Y,
(int)imagecolorallocate($image, 0xFF, 0x00, 0x00),
'/nonexistent/font.ttf',
'Hello',
[],
(int)imagecolorallocate($image, 0x00, 0x00, 0xFF)
);
$this->assertFalse($result);
}
public function testHorizontalGradientReturnsArray(): void
{
$image = $this->createImage();
$result = AgjGd::imagefttextgradient(
$image,
self::FONT_SIZE,
self::FONT_ANGLE,
self::FONT_X,
self::FONT_Y,
(int)imagecolorallocate($image, 0xFF, 0x00, 0x00),
self::FONT_PATH,
'Hello',
[],
(int)imagecolorallocate($image, 0x00, 0x00, 0xFF),
true
);
$this->assertIsArray($result);
$this->assertCount(8, $result);
}
public function testVerticalGradientColorInterpolation(): void
{
$image = $this->createImage();
$result = AgjGd::imagefttextgradient(
$image,
self::FONT_SIZE,
self::FONT_ANGLE,
self::FONT_X,
self::FONT_Y,
(int)imagecolorallocate($image, 0xFF, 0x00, 0x00),
self::FONT_PATH,
'HELLO',
[],
(int)imagecolorallocate($image, 0x00, 0x00, 0xFF)
);
$this->assertIsArray($result);
$textTop = min($result[5], $result[7]);
$textBottom = max($result[1], $result[3]);
$textLeft = min($result[0], $result[6]);
$textRight = max($result[2], $result[4]);
$band = max(1, (int)(($textBottom - $textTop) * 0.15));
[$topRedSum, $topBlueSum, $topCount] = $this->sumRgbInRegion(
$image,
$textLeft,
$textTop,
$textRight,
$textTop + $band
);
[$bottomRedSum, $bottomBlueSum, $bottomCount] = $this->sumRgbInRegion(
$image,
$textLeft,
$textBottom - $band,
$textRight,
$textBottom
);
$this->assertGreaterThan(0, $topCount, 'Expected text pixels near the top of the bounding box');
$this->assertGreaterThan(0, $bottomCount, 'Expected text pixels near the bottom of the bounding box');
$this->assertGreaterThan(
$topBlueSum / $topCount,
$topRedSum / $topCount,
'Top pixels should have more red than blue'
);
$this->assertGreaterThan(
$bottomRedSum / $bottomCount,
$bottomBlueSum / $bottomCount,
'Bottom pixels should have more blue than red'
);
}
public function testHorizontalGradientColorInterpolation(): void
{
$image = $this->createImage();
$result = AgjGd::imagefttextgradient(
$image,
self::FONT_SIZE,
self::FONT_ANGLE,
self::FONT_X,
self::FONT_Y,
(int)imagecolorallocate($image, 0xFF, 0x00, 0x00),
self::FONT_PATH,
'HELLO',
[],
(int)imagecolorallocate($image, 0x00, 0x00, 0xFF),
true
);
$this->assertIsArray($result);
$textTop = min($result[5], $result[7]);
$textBottom = max($result[1], $result[3]);
$textLeft = min($result[0], $result[6]);
$textRight = max($result[2], $result[4]);
$band = max(1, (int)(($textRight - $textLeft) * 0.15));
[$leftRedSum, $leftBlueSum, $leftCount] = $this->sumRgbInRegion(
$image,
$textLeft,
$textTop,
$textLeft + $band,
$textBottom
);
[$rightRedSum, $rightBlueSum, $rightCount] = $this->sumRgbInRegion(
$image,
$textRight - $band,
$textTop,
$textRight,
$textBottom
);
$this->assertGreaterThan(0, $leftCount, 'Expected text pixels near the left edge of the bounding box');
$this->assertGreaterThan(0, $rightCount, 'Expected text pixels near the right edge of the bounding box');
$this->assertGreaterThan(
$leftBlueSum / $leftCount,
$leftRedSum / $leftCount,
'Left pixels should have more red than blue'
);
$this->assertGreaterThan(
$rightRedSum / $rightCount,
$rightBlueSum / $rightCount,
'Right pixels should have more blue than red'
);
}
public function testAlphaColorsAreInterpolated(): void
{
$image = $this->createImage();
$result = AgjGd::imagefttextgradient(
$image,
self::FONT_SIZE,
self::FONT_ANGLE,
self::FONT_X,
self::FONT_Y,
(int)imagecolorallocatealpha($image, 0xFF, 0x00, 0x00, 0),
self::FONT_PATH,
'Hello',
[],
(int)imagecolorallocatealpha($image, 0x00, 0x00, 0xFF, 64)
);
$this->assertIsArray($result);
$this->assertCount(8, $result);
}
public function testCalledWithoutGradientColorRendersSolidText(): void
{
// Omitting the gradient color falls back to imagettftext(), which draws the text in a solid $color.
$image = $this->createImage();
$result = AgjGd::imagefttextgradient(
$image,
self::FONT_SIZE,
self::FONT_ANGLE,
self::FONT_X,
self::FONT_Y,
(int)imagecolorallocate($image, 0xFF, 0x00, 0x00),
self::FONT_PATH,
'Hello'
);
$this->assertIsArray($result);
$this->assertCount(8, $result);
$expected = imagettftext(
$this->createImage(),
self::FONT_SIZE,
self::FONT_ANGLE,
self::FONT_X,
self::FONT_Y,
0xFF0000,
self::FONT_PATH,
'Hello'
);
$this->assertSame($expected, $result);
}
public function testImageTtfTextGradientAliasMatchesTheCanonicalMethod(): void
{
// imagettftextgradient() is an alias of imagefttextgradient(). It must forward every argument, so rendering the
// same gradient text through each one must produce byte-for-byte identical images and bounding boxes.
$canonical = $this->createImage();
$alias = $this->createImage();
$canonicalBox = AgjGd::imagefttextgradient(
$canonical,
self::FONT_SIZE,
self::FONT_ANGLE,
self::FONT_X,
self::FONT_Y,
(int)imagecolorallocate($canonical, 0xFF, 0x00, 0x00),
self::FONT_PATH,
'Hello',
[],
(int)imagecolorallocate($canonical, 0x00, 0x00, 0xFF),
true
);
$aliasBox = AgjGd::imagettftextgradient(
$alias,
self::FONT_SIZE,
self::FONT_ANGLE,
self::FONT_X,
self::FONT_Y,
(int)imagecolorallocate($alias, 0xFF, 0x00, 0x00),
self::FONT_PATH,
'Hello',
[],
(int)imagecolorallocate($alias, 0x00, 0x00, 0xFF),
true
);
$this->assertSame($canonicalBox, $aliasBox);
$this->assertTrue($this->imagesAreIdentical($canonical, $alias));
}
private function imagesAreIdentical(\GdImage $a, \GdImage $b): bool
{
for ($x = 0; $x < self::IMAGE_WIDTH; $x++) {
for ($y = 0; $y < self::IMAGE_HEIGHT; $y++) {
if (imagecolorat($a, $x, $y) !== imagecolorat($b, $x, $y)) {
return false;
}
}
}
return true;
}
/**
* Scans a rectangular region and sums the red and blue channel values of non-black pixels.
*
* @param \GdImage $image The image to scan.
* @param int $x1 The x-ordinate of the region’s first point.
* @param int $y1 The y-ordinate of the region’s first point.
* @param int $x2 The x-ordinate of the region’s second point.
* @param int $y2 The y-ordinate of the region’s second point.
*
* @return array{0: int, 1: int, 2: int} The summed red channel, the summed blue channel and the pixel count.
*/
private function sumRgbInRegion(\GdImage $image, int $x1, int $y1, int $x2, int $y2): array
{
$redSum = 0;
$blueSum = 0;
$count = 0;
for ($x = $x1; $x <= $x2; $x++) {
for ($y = $y1; $y <= $y2; $y++) {
$rgb = (int)imagecolorat($image, $x, $y);
$red = ($rgb >> 16) & 0xFF;
$blue = $rgb & 0xFF;
if ($red + $blue > 0) {
$redSum += $red;
$blueSum += $blue;
$count++;
}
}
}
return [$redSum, $blueSum, $count];
}
private function createImage(): \GdImage
{
$image = imagecreatetruecolor(self::IMAGE_WIDTH, self::IMAGE_HEIGHT);
$this->assertNotFalse($image);
return $image;
}
}
ImageGradientRectangleTest.php
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace AndrewGJohnson\AgjGd\Tests;
use AndrewGJohnson\AgjGd;
use PHPUnit\Framework\TestCase;
class ImageGradientRectangleTest extends TestCase
{
private \GdImage $image;
private int $red;
private int $blue;
protected function setUp(): void
{
$this->image = imagecreatetruecolor(100, 100);
$this->red = (int)imagecolorallocate($this->image, 0xFF, 0x00, 0x00);
$this->blue = (int)imagecolorallocate($this->image, 0x00, 0x00, 0xFF);
}
public function testVerticalGradientReturnsTrue(): void
{
$this->assertTrue(AgjGd::imagegradientrectangle($this->image, 10, 10, 90, 90, $this->red, $this->blue));
}
public function testHorizontalGradientReturnsTrue(): void
{
$this->assertTrue(AgjGd::imagegradientrectangle($this->image, 10, 10, 90, 90, $this->red, $this->blue, true));
}
public function testZeroHeightVerticalGradientReturnsFalse(): void
{
$this->assertFalse(AgjGd::imagegradientrectangle($this->image, 10, 50, 90, 50, $this->red, $this->blue));
}
public function testZeroWidthHorizontalGradientReturnsFalse(): void
{
$this->assertFalse(AgjGd::imagegradientrectangle($this->image, 50, 10, 50, 90, $this->red, $this->blue, true));
}
public function testSolidFillReturnsTrue(): void
{
$this->assertTrue(AgjGd::imagegradientrectangle($this->image, 10, 10, 90, 90, $this->red));
}
public function testVerticalGradientRunsFromColorToGradientColor(): void
{
AgjGd::imagegradientrectangle($this->image, 10, 10, 90, 90, $this->red, $this->blue);
$top = imagecolorsforindex($this->image, (int)imagecolorat($this->image, 50, 11));
$bottom = imagecolorsforindex($this->image, (int)imagecolorat($this->image, 50, 89));
$this->assertGreaterThan($top['blue'], $top['red'], 'The top of the rectangle should be mostly red');
$this->assertGreaterThan($bottom['red'], $bottom['blue'], 'The bottom of the rectangle should be mostly blue');
}
public function testHorizontalGradientRunsFromColorToGradientColor(): void
{
AgjGd::imagegradientrectangle($this->image, 10, 10, 90, 90, $this->red, $this->blue, true);
$left = imagecolorsforindex($this->image, (int)imagecolorat($this->image, 11, 50));
$right = imagecolorsforindex($this->image, (int)imagecolorat($this->image, 89, 50));
$this->assertGreaterThan($left['blue'], $left['red'], 'The left of the rectangle should be mostly red');
$this->assertGreaterThan($right['red'], $right['blue'], 'The right of the rectangle should be mostly blue');
}
public function testVerticalGradientReachesTheFinishColorAtTheFarEdge(): void
{
// Regression test for the off-by-one that stopped one pixel short of $y2 and never reached
// ratio 0, so the far edge must now be painted with the pure finish color (blue).
AgjGd::imagegradientrectangle($this->image, 10, 10, 90, 90, $this->red, $this->blue);
$components = imagecolorsforindex($this->image, (int)imagecolorat($this->image, 50, 90));
$this->assertSame(0, $components['red']);
$this->assertSame(0, $components['green']);
$this->assertSame(255, $components['blue']);
}
public function testHorizontalGradientReachesTheFinishColorAtTheFarEdge(): void
{
// Regression test for the off-by-one that stopped one pixel short of $x2 and never reached
// ratio 0, so the far edge must now be painted with the pure finish color (blue).
AgjGd::imagegradientrectangle($this->image, 10, 10, 90, 90, $this->red, $this->blue, true);
$components = imagecolorsforindex($this->image, (int)imagecolorat($this->image, 90, 50));
$this->assertSame(0, $components['red']);
$this->assertSame(0, $components['green']);
$this->assertSame(255, $components['blue']);
}
public function testSolidFillUsesTheColorExactly(): void
{
AgjGd::imagegradientrectangle($this->image, 10, 10, 90, 90, $this->red);
$components = imagecolorsforindex($this->image, (int)imagecolorat($this->image, 50, 50));
$this->assertSame(255, $components['red']);
$this->assertSame(0, $components['green']);
$this->assertSame(0, $components['blue']);
}
public function testReturnsFalseWhenTheVerticalGradientExhaustsThePalette(): void
{
// A palette image caps at 256 colors; a 400px vertical gradient needs more, so partway through the loop
// imageblendedcolorallocate() can no longer allocate a color and the method reports the failure.
$image = imagecreate(400, 400);
$this->assertNotFalse($image);
$red = (int)imagecolorallocate($image, 0xFF, 0x00, 0x00);
$blue = (int)imagecolorallocate($image, 0x00, 0x00, 0xFF);
$this->assertFalse(AgjGd::imagegradientrectangle($image, 0, 0, 399, 399, $red, $blue));
}
public function testReturnsFalseWhenTheHorizontalGradientExhaustsThePalette(): void
{
$image = imagecreate(400, 400);
$this->assertNotFalse($image);
$red = (int)imagecolorallocate($image, 0xFF, 0x00, 0x00);
$blue = (int)imagecolorallocate($image, 0x00, 0x00, 0xFF);
$this->assertFalse(AgjGd::imagegradientrectangle($image, 0, 0, 399, 399, $red, $blue, true));
}
}
LineBreaksForTextTest.php
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace AndrewGJohnson\AgjGd\Tests;
use AndrewGJohnson\AgjGd;
use PHPUnit\Framework\TestCase;
class LineBreaksForTextTest extends TestCase
{
private const FONT_ANGLE = 0;
private const FONT_PATH = __DIR__ . '/NotoSans-Regular.ttf';
private const FONT_SIZE = 10;
public function testTextIsReturnedUnchangedWhenItAlreadyFits(): void
{
$this->assertSame('Hello world!', $this->lineBreaks('Hello world!', 10000));
}
public function testEmptyStringReturnsEmptyString(): void
{
$this->assertSame('', $this->lineBreaks('', 10000));
}
public function testSingleWordReturnedUnchanged(): void
{
// A single word should always be returned unchanged regardless of the maximum width.
$this->assertSame('Hello', $this->lineBreaks('Hello', 1));
}
public function testTextFittingMaximumWidthHasNoLineBreaks(): void
{
$result = $this->lineBreaks('Hello world!', 10000);
$this->assertStringNotContainsString("\n", $result);
$this->assertStringNotContainsString("\r", $result);
}
public function testTextExceedingMaximumWidthGetsLineBreak(): void
{
$this->assertStringContainsString("\n", $this->lineBreaks('Hello world!', 1, "\n"));
}
public function testCustomLineBreakCharacter(): void
{
$result = $this->lineBreaks('Hello world!', 1, '<br>');
$this->assertStringContainsString('<br>', $result);
$this->assertStringNotContainsString(PHP_EOL, $result);
}
public function testAllWordsArePresentInOutput(): void
{
$words = ['Hello', 'world', 'foo', 'bar'];
$result = $this->lineBreaks(implode(' ', $words), 1, "\n");
foreach ($words as $word) {
$this->assertStringContainsString($word, $result);
}
}
public function testForceBreakOnSingleWordsSplitsLongWord(): void
{
// With forceBreakOnSingleWords disabled a long word that does not fit should still appear intact on a line;
// with the flag enabled the word should be split across lines.
$longWord = 'Pneumonoultramicroscopicsilicovolcanoconiosis';
$halfWidth = (int)($this->textWidth($longWord) / 2);
$text = 'A ' . $longWord;
$withoutForce = $this->lineBreaks($text, $halfWidth, "\n", false, false);
$withForce = $this->lineBreaks($text, $halfWidth, "\n", false, true);
$foundIntact = false;
foreach (explode("\n", $withoutForce) as $line) {
if (str_contains($line, $longWord)) {
$foundIntact = true;
break;
}
}
$this->assertTrue(
$foundIntact,
'Without forceBreakOnSingleWords the long word should appear intact on a single line'
);
$this->assertGreaterThan(
count(explode("\n", $withoutForce)),
count(explode("\n", $withForce)),
'With forceBreakOnSingleWords there should be more lines because the word is split'
);
}
public function testAttemptToBreakOnHyphensBreaksAtHyphen(): void
{
// maximumWidth is exactly the pixel width of 'A B-', so 'A B-C' overflows and the hyphen-break logic commits
// 'A B-' and carries 'C' to the next line.
$text = 'A B-C';
$maximumWidth = $this->textWidth('A B-');
$withHyphens = $this->lineBreaks($text, $maximumWidth, "\n", true);
$withoutHyphens = $this->lineBreaks($text, $maximumWidth, "\n", false);
$this->assertNotSame(
$withoutHyphens,
$withHyphens,
'attemptToBreakOnHyphens should produce a different result when a break at a hyphen is possible'
);
$hasLineEndingWithHyphen = false;
foreach (explode("\n", $withHyphens) as $line) {
if (str_ends_with($line, '-')) {
$hasLineEndingWithHyphen = true;
break;
}
}
$this->assertTrue(
$hasLineEndingWithHyphen,
'With attemptToBreakOnHyphens at least one line should end with a trailing hyphen'
);
}
public function testPreventWidowsMovesPreviousWordToLastLine(): void
{
// When the last word would appear alone on the final line (a widow), preventWidows should pull the previous
// word down so the two words share the last line.
$text = 'Hello world x';
// Width fits 'Hello world' exactly, so 'Hello world x' overflows and 'x' becomes a widow.
$maximumWidth = $this->textWidth('Hello world');
$withoutPrevent = $this->lineBreaks($text, $maximumWidth, "\n", false, false, false);
$withPrevent = $this->lineBreaks($text, $maximumWidth, "\n", false, false, true);
$linesWithout = explode("\n", $withoutPrevent);
$this->assertSame(
'x',
end($linesWithout),
'Without preventWidows the last word should appear alone on the final line'
);
$linesWith = explode("\n", $withPrevent);
$lastLine = end($linesWith);
$this->assertStringContainsString(
'world',
$lastLine,
'With preventWidows the second-to-last word should be moved to the final line'
);
$this->assertStringContainsString(
'x',
$lastLine,
'With preventWidows the last word should still appear on the final line'
);
}
public function testEveryLineFitsWithinTheMaximumWidth(): void
{
$text = 'It was the best of times, it was the worst of times, it was the age of wisdom.';
$maximumWidth = 120;
foreach (explode("\n", $this->lineBreaks($text, $maximumWidth, "\n")) as $line) {
$this->assertLessThanOrEqual(
$maximumWidth,
$this->textWidth($line),
'Every line should fit within the maximum width: ' . $line
);
}
}
public function testForceBreakReturnsTheWholeWordWhenNoSingleCharacterFits(): void
{
// With a maximum width smaller than any single glyph, the force-break loop cannot place even one character,
// so it returns the whole remaining word rather than looping forever.
$result = $this->lineBreaks('A verylongword', 1, "\n", false, true);
$this->assertSame("A\nverylongword", $result);
}
public function testForceBreakHandlesEmptyWordsFromConsecutiveSpaces(): void
{
// A trailing space produces an empty final word; force-breaking an empty word must terminate cleanly.
$result = $this->lineBreaks('A ', 1, "\n", false, true);
$this->assertSame("A\n", $result);
}
public function testForceBreakGivesUpOnMalformedUtf8(): void
{
// Force-breaking splits a word into characters with preg_split() in UTF-8 mode, which fails on malformed
// input. GD still measures the word, so it is seen as too wide rather than unmeasurable and the force-break
// path is entered, but the split cannot happen and the word is left whole on its own line.
$result = $this->lineBreaks("A verylongword\xFF", 1, "\n", false, true);
$this->assertSame("A\nverylongword\xFF", $result);
}
public function testInvalidFontLeavesTextUnbroken(): void
{
// When imagettfbbox() cannot measure the text (here, a missing font) the width is treated as fitting, so no
// line breaks are added. The @ suppresses the GD warning.
$result = @AgjGd::linebreaksfortext(
self::FONT_SIZE,
self::FONT_ANGLE,
'/nonexistent/font.ttf',
'Hello world foo',
100
);
$this->assertSame('Hello world foo', $result);
}
public function testAliasesMatchTheCanonicalMethod(): void
{
// linebreaks4imagettftext(), linebreaks4imagefttext(), linebreaks4text(), linebreaksforimagefttext() and
// linebreaksforimagettftext() are all aliases of linebreaksfortext(). Each must forward every argument, so
// wrapping the same text through each one must return an identical string.
$text = 'The quick brown fox jumps over the lazy dog';
$width = 100;
$canonical = AgjGd::linebreaksfortext(
self::FONT_SIZE,
self::FONT_ANGLE,
self::FONT_PATH,
$text,
$width
);
// The input must actually wrap for the comparison to be meaningful.
$this->assertStringContainsString(PHP_EOL, $canonical);
$this->assertSame($canonical, AgjGd::linebreaks4imagettftext(
self::FONT_SIZE,
self::FONT_ANGLE,
self::FONT_PATH,
$text,
$width
));
$this->assertSame($canonical, AgjGd::linebreaks4imagefttext(
self::FONT_SIZE,
self::FONT_ANGLE,
self::FONT_PATH,
$text,
$width
));
$this->assertSame($canonical, AgjGd::linebreaks4text(
self::FONT_SIZE,
self::FONT_ANGLE,
self::FONT_PATH,
$text,
$width
));
$this->assertSame($canonical, AgjGd::linebreaksforimagefttext(
self::FONT_SIZE,
self::FONT_ANGLE,
self::FONT_PATH,
$text,
$width
));
$this->assertSame($canonical, AgjGd::linebreaksforimagettftext(
self::FONT_SIZE,
self::FONT_ANGLE,
self::FONT_PATH,
$text,
$width
));
}
private function lineBreaks(
string $text,
int $maximumWidth,
string $lineBreakCharacter = PHP_EOL,
bool $attemptToBreakOnHyphens = false,
bool $forceBreakOnSingleWords = false,
bool $preventWidows = false
): string {
return AgjGd::linebreaksfortext(
self::FONT_SIZE,
self::FONT_ANGLE,
self::FONT_PATH,
$text,
$maximumWidth,
$lineBreakCharacter,
$attemptToBreakOnHyphens,
$forceBreakOnSingleWords,
$preventWidows
);
}
private function textWidth(string $text): int
{
$boundingBox = imagettfbbox(self::FONT_SIZE, self::FONT_ANGLE, self::FONT_PATH, $text);
$this->assertNotFalse($boundingBox);
$left = min($boundingBox[0], $boundingBox[2], $boundingBox[4], $boundingBox[6]);
$right = max($boundingBox[0], $boundingBox[2], $boundingBox[4], $boundingBox[6]);
return $right - $left;
}
}