Initial commit

This commit is contained in:
2026-08-30 22:02:02 +00:00
commit b6bd2277f5
2334 changed files with 646393 additions and 0 deletions
@@ -0,0 +1,183 @@
<?php
/**
* ImageCacheTest.php
*
* @since 2026-06-16
* @category Library
* @package PdfImage
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-image
*
* This file is part of tc-lib-pdf-image software library.
*/
namespace Test;
use Com\Tecnick\Pdf\Image\ImageCacheInterface;
use Com\Tecnick\Pdf\Image\Import;
/**
* External image cache test.
*
* @since 2026-06-16
* @category Library
* @package PdfImage
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-image
*/
class ImageCacheTest extends TestUtil
{
private function getImport(?ImageCacheInterface $cache): Import
{
return new Import(0.75, $this->getTestEncrypt(), $this->getTestFileHelper(), imageCache: $cache);
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testWriteThroughAndReadThrough(): void
{
$src = __DIR__ . '/images/200x100_RGB.png';
$spy = new SpyImageCache();
$img1 = $this->getImport($spy);
$img1->add($src);
// one external lookup (miss) + one write-through
$this->assertSame(1, $spy->getCount);
$this->assertCount(1, $spy->setKeys);
// doctor the stored entry with a sentinel: a hit must be returned verbatim
$key = $spy->setKeys[0] ?? '';
$this->assertArrayHasKey($key, $spy->store);
if (isset($spy->store[$key])) {
$spy->store[$key]['width'] = 4321;
}
// a fresh Import (empty in-process cache) reusing the same backend
$img2 = $this->getImport($spy);
$img2->add($src);
$data = $img2->getImageDataByKey($img2->getKey($src));
// proves the external entry was used as-is (no recomputation)
$this->assertSame(4321, $data['width']);
// no additional write-through happened on a hit
$this->assertCount(1, $spy->setKeys);
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testDisabledByDefault(): void
{
$src = __DIR__ . '/images/200x100_RGB.png';
$img = $this->getImport(null);
$img->add($src);
$data = $img->getImageDataByKey($img->getKey($src));
// with no external cache, the in-process data keeps the original bytes
$this->assertNotEmpty($data['data']);
$this->assertNotEmpty($data['raw']);
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testPersistedSnapshotIsClean(): void
{
$spy = new SpyImageCache();
$img = $this->getImport($spy);
$img->add(__DIR__ . '/images/200x100_RGBALPHA.png');
$stored = $spy->fetch($spy->setKeys[0] ?? '');
// original bytes dropped to shrink the payload
$this->assertSame('', $stored['raw']);
// no PDF object numbers / output flag are persisted
$this->assertSame(0, $stored['obj']);
$this->assertSame(0, $stored['obj_alt']);
$this->assertSame(0, $stored['obj_icc']);
$this->assertSame(0, $stored['obj_pal']);
$this->assertArrayNotHasKey('out', $stored);
// alpha image splits into plain + mask: their raw bytes are dropped too
$this->assertArrayHasKey('mask', $stored);
if (isset($stored['mask'])) {
$this->assertSame('', $stored['mask']['raw']);
}
if (isset($stored['plain'])) {
$this->assertSame('', $stored['plain']['raw']);
}
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testStaleFileProducesDifferentKey(): void
{
$tmp = $this->makeTempFile('.png');
\copy(__DIR__ . '/images/200x100_RGB.png', $tmp);
\clearstatcache(true, $tmp);
$spy = new SpyImageCache();
$this->getImport($spy)->add($tmp);
$this->assertCount(1, $spy->setKeys);
$firstKey = $spy->setKeys[0] ?? '';
// edit the file in place: different content/size and a newer mtime
\copy(__DIR__ . '/images/200x100_GRAY.png', $tmp);
\touch($tmp, \time() + 5);
\clearstatcache(true, $tmp);
$this->getImport($spy)->add($tmp);
$this->assertCount(2, $spy->setKeys);
// mtime+size folded into the persistent key invalidates the stale entry
$this->assertNotSame($firstKey, $spy->setKeys[1] ?? '');
\unlink($tmp);
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testOutputIdenticalWithAndWithoutCache(): void
{
$src = __DIR__ . '/images/200x100_RGB.png';
$ref = $this->getImport(null);
$ref->add($src);
$refOut = $ref->getOutImagesBlock(10);
$spy = new SpyImageCache();
// warm the cache, then import again from a fresh instance (external hit)
$this->getImport($spy)->add($src);
$cached = $this->getImport($spy);
$cached->add($src);
$cachedOut = $cached->getOutImagesBlock(10);
// stripping raw never affects the emitted PDF objects
$this->assertSame($refOut, $cachedOut);
}
private function makeTempFile(string $ext): string
{
return \sys_get_temp_dir() . \DIRECTORY_SEPARATOR . 'tclpi_' . \uniqid('', true) . $ext;
}
}
@@ -0,0 +1,421 @@
<?php
/**
* ImportEdgeCasesTest.php
*
* @since 2026-04-19
* @category Library
* @package PdfImage
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-image
*
* This file is part of tc-lib-pdf-image software library.
*/
namespace Test;
/**
* Import edge cases test
*
* @since 2026-04-19
* @category Library
* @package PdfImage
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-image
*/
class ImportEdgeCasesTest extends TestUtil
{
protected function getTestObject(): \Com\Tecnick\Pdf\Image\Import
{
$encrypt = $this->getTestEncrypt();
return new \Com\Tecnick\Pdf\Image\Import(0.75, $encrypt, $this->getTestFileHelper());
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testAddWithZeroWidth(): void
{
$this->bcExpectException(\Com\Tecnick\Pdf\Image\Exception::class);
$import = $this->getTestObject();
$import->add(__DIR__ . '/images/200x100_RGB.png', 0, 50);
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testAddWithZeroHeight(): void
{
$this->bcExpectException(\Com\Tecnick\Pdf\Image\Exception::class);
$import = $this->getTestObject();
$import->add(__DIR__ . '/images/200x100_RGB.png', 100, 0);
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testAddWithOnlyWidthKeepsAspectRatio(): void
{
$import = $this->getTestObject();
$import->add(__DIR__ . '/images/200x100_RGB.png', 100, null);
$key = $import->getKey(__DIR__ . '/images/200x100_RGB.png', 100, 0, 100);
$data = $import->getImageDataByKey($key);
$this->assertSame(100, $data['width']);
$this->assertSame(50, $data['height']);
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testAddWithOnlyHeightKeepsAspectRatio(): void
{
$import = $this->getTestObject();
$import->add(__DIR__ . '/images/200x100_RGB.png', null, 50);
$key = $import->getKey(__DIR__ . '/images/200x100_RGB.png', 0, 50, 100);
$data = $import->getImageDataByKey($key);
$this->assertSame(100, $data['width']);
$this->assertSame(50, $data['height']);
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testAddWithQualityLow(): void
{
$import = $this->getTestObject();
$iid = $import->add(__DIR__ . '/images/200x100_RGB.jpg', null, null, false, 0);
$this->assertGreaterThan(0, $iid);
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testAddWithQualityHigh(): void
{
$import = $this->getTestObject();
$iid = $import->add(__DIR__ . '/images/200x100_RGB.jpg', null, null, false, 100);
$this->assertGreaterThan(0, $iid);
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testAddWithQualityExceeding100(): void
{
$import = $this->getTestObject();
// Quality > 100 should be clamped to 100
$iid = $import->add(__DIR__ . '/images/200x100_RGB.jpg', null, null, false, 150);
$this->assertGreaterThan(0, $iid);
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testAddWithNegativeQuality(): void
{
$import = $this->getTestObject();
// Negative quality should be clamped to 0
$iid = $import->add(__DIR__ . '/images/200x100_RGB.jpg', null, null, false, -50);
$this->assertGreaterThan(0, $iid);
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testAddWithMaskAndAlphaParameters(): void
{
$import = $this->getTestObject();
// Add image as mask
$iid = $import->add(__DIR__ . '/images/200x100_RGB.png', null, null, true);
$this->assertGreaterThan(0, $iid);
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testAddWithDefprintParameter(): void
{
$import = $this->getTestObject();
// Add with defprint=true
$iid = $import->add(__DIR__ . '/images/200x100_RGB.png', null, null, false, 100, true);
$this->assertGreaterThan(0, $iid);
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testAddWithAlternateImages(): void
{
$import = $this->getTestObject();
$iid1 = $import->add(__DIR__ . '/images/200x100_RGB.png');
$iid2 = $import->add(__DIR__ . '/images/200x100_GRAY.jpg');
// Add with alternate images
$iid3 = $import->add(__DIR__ . '/images/200x100_RGBALPHA.png', null, null, false, 100, false, [$iid1, $iid2]);
$this->assertGreaterThan(0, $iid3);
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testAddImageFromStringData(): void
{
$import = $this->getTestObject();
$fileData = \file_get_contents(__DIR__ . '/images/200x100_RGB.png');
$this->assertIsString($fileData);
// Add image from raw data
$iid = $import->add('@' . $fileData);
$this->assertGreaterThan(0, $iid);
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testGetKeyConsistency(): void
{
$import = $this->getTestObject();
// Same image parameters should produce same key
$key1 = $import->getKey('/path/image.png', 100, 200, 75);
$key2 = $import->getKey('/path/image.png', 100, 200, 75);
$this->assertEquals($key1, $key2);
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testGetKeyDifference(): void
{
$import = $this->getTestObject();
// Different parameters should produce different keys
$key1 = $import->getKey('/path/image.png', 100, 200, 75);
$key2 = $import->getKey('/path/image.png', 100, 200, 80);
$this->assertNotEquals($key1, $key2);
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testMultipleAddOperations(): void
{
$import = $this->getTestObject();
$iid1 = $import->add(__DIR__ . '/images/200x100_RGB.png');
$iid2 = $import->add(__DIR__ . '/images/200x100_GRAY.jpg');
$iid3 = $import->add(__DIR__ . '/images/200x100_RGBALPHA.png');
$this->assertEquals(1, $iid1);
$this->assertEquals(2, $iid2);
$this->assertEquals(3, $iid3);
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testRepeatedImageAddition(): void
{
$import = $this->getTestObject();
// Adding the same image twice with same params should reuse cache
$iid1 = $import->add(__DIR__ . '/images/200x100_RGB.png', 100, 50);
$iid2 = $import->add(__DIR__ . '/images/200x100_RGB.png', 100, 50);
// Should have different image IDs but share same cached data
$this->assertEquals(1, $iid1);
$this->assertEquals(2, $iid2);
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testAddWithResizeDownscale(): void
{
$import = $this->getTestObject();
// Downscale image
$iid = $import->add(__DIR__ . '/images/200x100_RGB.png', 100, 50);
$this->assertGreaterThan(0, $iid);
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testAddWithResizeUpscale(): void
{
$import = $this->getTestObject();
// Upscale image
$iid = $import->add(__DIR__ . '/images/200x100_RGB.png', 400, 200);
$this->assertGreaterThan(0, $iid);
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testGetSetImageAfterMultipleAdds(): void
{
$import = $this->getTestObject();
$iid1 = $import->add(__DIR__ . '/images/200x100_RGB.png');
$iid2 = $import->add(__DIR__ . '/images/200x100_GRAY.jpg');
$iid3 = $import->add(__DIR__ . '/images/200x100_RGBALPHA.png');
$result1 = $import->getSetImage($iid1, 0, 0, 100, 100, 600);
$result2 = $import->getSetImage($iid2, 0, 0, 100, 100, 600);
$result3 = $import->getSetImage($iid3, 0, 0, 100, 100, 600);
$this->assertNotEmpty($result1);
$this->assertNotEmpty($result2);
$this->assertNotEmpty($result3);
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testInvalidImageFile(): void
{
$this->bcExpectException(\Com\Tecnick\Pdf\Image\Exception::class);
$import = $this->getTestObject();
$import->add(__DIR__ . '/images/nonexistent.png');
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testInvalidImageData(): void
{
$this->bcExpectException(\Com\Tecnick\Pdf\Image\Exception::class);
$import = $this->getTestObject();
$import->add('@invalidbinarydata');
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testGetImageDimensionsByKeyReturnsOriginal(): void
{
$import = $this->getTestObject();
$image = __DIR__ . '/images/200x100_RGB.png';
$import->add($image);
$key = $import->getKey($image, 0, 0, 100);
$this->assertSame(['width' => 200, 'height' => 100], $import->getImageDimensionsByKey($key));
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testGetImageDimensionsByKeyDerivesHeightFromWidth(): void
{
$import = $this->getTestObject();
$image = __DIR__ . '/images/200x100_RGB.png';
$import->add($image);
$key = $import->getKey($image, 0, 0, 100);
$this->assertSame(['width' => 100, 'height' => 50], $import->getImageDimensionsByKey($key, 100));
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testGetImageDimensionsByKeyDerivesWidthFromHeight(): void
{
$import = $this->getTestObject();
$image = __DIR__ . '/images/200x100_RGB.png';
$import->add($image);
$key = $import->getKey($image, 0, 0, 100);
$this->assertSame(['width' => 100, 'height' => 50], $import->getImageDimensionsByKey($key, 0, 50));
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testGetImageDimensionsByKeyKeepsBothWhenSpecified(): void
{
$import = $this->getTestObject();
$image = __DIR__ . '/images/200x100_RGB.png';
$import->add($image);
$key = $import->getKey($image, 0, 0, 100);
$this->assertSame(['width' => 80, 'height' => 80], $import->getImageDimensionsByKey($key, 80, 80));
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testGetImageDimensionsByKeyFitWithinBox(): void
{
$import = $this->getTestObject();
$image = __DIR__ . '/images/200x100_RGB.png';
$import->add($image);
$key = $import->getKey($image, 0, 0, 100);
// box 80x80, source 200x100: scale = min(0.4, 0.8) = 0.4 => 80x40
$this->assertSame(['width' => 80, 'height' => 40], $import->getImageDimensionsByKey($key, 80, 80, true));
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testGetImageDimensionsByKeyUnknownKeyThrows(): void
{
$this->bcExpectException(\Com\Tecnick\Pdf\Image\Exception::class);
$import = $this->getTestObject();
$import->getImageDimensionsByKey('nonexistent-key');
}
}
@@ -0,0 +1,353 @@
<?php
/**
* ImportOutputPngEdgeCasesTest.php
*
* @since 2026-05-21
* @category Library
* @package PdfImage
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-image
*
* This file is part of tc-lib-pdf-image software library.
*/
declare(strict_types=1);
namespace Test;
require_once __DIR__ . '/ImportProtectedMethodsHarness.php';
require_once __DIR__ . '/PngChunkParsingHarness.php';
/**
* Extra branch-focused tests for import, output and PNG chunk parsing.
*
* @phpstan-import-type ImageBaseData from \Com\Tecnick\Pdf\Image\Import
* @phpstan-import-type ImageRawData from \Com\Tecnick\Pdf\Image\Import
*/
class ImportOutputPngEdgeCasesTest extends TestUtil
{
/**
* @return ImageBaseData
*/
protected function getBaseData(string $key = 'test'): array
{
return [
'bits' => 8,
'channels' => 3,
'colspace' => 'DeviceRGB',
'data' => 'abc',
'exturl' => false,
'file' => '',
'filter' => 'FlateDecode',
'height' => 1,
'icc' => '',
'ismask' => false,
'key' => $key,
'mapto' => IMAGETYPE_PNG,
'native' => true,
'obj' => 0,
'obj_alt' => 0,
'obj_icc' => 0,
'obj_pal' => 0,
'pal' => '',
'parms' => '',
'raw' => 'raw',
'recode' => false,
'recoded' => false,
'splitalpha' => false,
'trns' => [],
'type' => IMAGETYPE_PNG,
'width' => 1,
];
}
/**
* @return ImageRawData
*/
protected function getRawData(string $key = 'test'): array
{
return $this->getBaseData($key);
}
protected function getImportHarness(bool $pdfa = false): ImportProtectedMethodsHarness
{
return new ImportProtectedMethodsHarness(
0.75,
$this->getTestEncrypt(),
$this->getTestFileHelper(),
$pdfa,
false,
);
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
*/
public function testGetDataThrowsWhenImageIsNotNative(): void
{
$this->bcExpectException(\Com\Tecnick\Pdf\Image\Exception::class);
$import = $this->getImportHarness();
$data = $this->getRawData();
$data['native'] = false;
$import->callGetData($data, 10, 10, 90);
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
*/
public function testGetDataThrowsWhenNativeTypeIsUnknown(): void
{
$this->bcExpectException(\Com\Tecnick\Pdf\Image\Exception::class);
$import = $this->getImportHarness();
$data = $this->getRawData();
$data['type'] = IMAGETYPE_GIF;
$import->callGetData($data, 10, 10, 90);
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
*/
public function testGetResizedRawDataRejectsInvalidRawImage(): void
{
$this->bcExpectException(\Com\Tecnick\Pdf\Image\Exception::class);
$import = $this->getImportHarness();
$data = $this->getBaseData();
$data['raw'] = 'not-image-binary';
\set_error_handler(static fn(): bool => true);
try {
$import->callGetResizedRawData($data, 10, 10, true, 90);
} finally {
\restore_error_handler();
}
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
*/
public function testGetAlphaChannelRawDataRejectsInvalidRawImage(): void
{
$this->bcExpectException(\Com\Tecnick\Pdf\Image\Exception::class);
$import = $this->getImportHarness();
$data = $this->getBaseData();
$data['raw'] = 'not-image-binary';
\set_error_handler(static fn(): bool => true);
try {
$import->callGetAlphaChannelRawData($data);
} finally {
\restore_error_handler();
}
}
/**
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testGetOutImageSupportsExternalStreamWithFilter(): void
{
$import = $this->getImportHarness();
$import->setPon(10);
$img = [
'iid' => 1,
'key' => 'ext',
'width' => 10,
'height' => 5,
'defprint' => false,
'altimgs' => [],
];
$data = $this->getRawData('ext');
$data['exturl'] = true;
$data['filter'] = 'ASCIIHexDecode';
$data['width'] = 10;
$data['height'] = 5;
$import->setCacheEntry('ext', $data);
$out = $import->callGetOutImage($img, $data);
$this->assertStringContainsString('/Length 0 /F << /FS /URL /F (true) >>', $out);
$this->assertStringContainsString('/FFilter /ASCIIHexDecode', $out);
}
public function testGetXobjectDictByKeysFallsBackToPlainAndMaskEntries(): void
{
$import = $this->getImportHarness();
$import->setXobjdict([
'IMGplain2' => 42,
'IMGmask3' => 43,
]);
$out = $import->getXobjectDictByKeys([2, 3]);
$this->assertSame(' /IMGplain2 42 0 R /IMGmask3 43 0 R', $out);
}
public function testGetOutAltImagesSkipsUnknownOrUnbuiltAlternateImages(): void
{
$import = $this->getImportHarness();
$import->setPon(20);
$import->setImageEntry(2, [
'iid' => 2,
'key' => 'alt-no-object',
'width' => 10,
'height' => 5,
'defprint' => true,
'altimgs' => [],
]);
$altData = $this->getRawData('alt-no-object');
$altData['obj'] = 0;
$import->setCacheEntry('alt-no-object', $altData);
$img = [
'iid' => 1,
'key' => 'main',
'width' => 10,
'height' => 5,
'defprint' => false,
'altimgs' => [999, 2],
];
$data = $this->getRawData('main');
$out = $import->callGetOutAltImages($img, $data);
$this->assertStringContainsString('21 0 obj', $out);
$this->assertStringContainsString('[ ]', $out);
}
public function testGetOutTransparencyIndexedSkipsNonZeroValues(): void
{
$import = $this->getImportHarness();
$data = $this->getRawData();
$data['colspace'] = 'Indexed';
$data['trns'] = [0 => 0, 1 => 9, 2 => 0];
// Indexed: the fully-transparent palette indices (alpha 0) are masked.
$out = $import->callGetOutTransparency($data);
$this->assertSame('0 0 2 2 ', $out);
}
public function testGetOutTransparencyRgbUsesColorValues(): void
{
$import = $this->getImportHarness();
$data = $this->getRawData();
$data['colspace'] = 'DeviceRGB';
// trns holds the transparent colour sample values (R, G, B).
$data['trns'] = [255, 128, 0];
$out = $import->callGetOutTransparency($data);
$this->assertSame('255 255 128 128 0 0 ', $out);
}
public function testGetOutTransparencyGrayUsesColorValue(): void
{
$import = $this->getImportHarness();
$data = $this->getRawData();
$data['colspace'] = 'DeviceGray';
$data['trns'] = [128];
$out = $import->callGetOutTransparency($data);
$this->assertSame('128 128 ', $out);
}
public function testGetTrnsChunkHandlesGrayAndRgbFormats(): void
{
$png = new PngChunkParsingHarness();
$gray = $this->getBaseData();
$gray['colspace'] = 'DeviceGray';
$gray['raw'] = "\x00\x7f";
$gray['trns'] = [];
$grayOffset = 0;
$gray = $png->callGetTrnsChunk($gray, $grayOffset, 2);
$this->assertSame([127], $gray['trns']);
$this->assertSame(6, $grayOffset);
$rgb = $this->getBaseData();
$rgb['colspace'] = 'DeviceRGB';
$rgb['raw'] = "\x00\x11\x00\x22\x00\x33";
$rgb['trns'] = [];
$rgbOffset = 0;
$rgb = $png->callGetTrnsChunk($rgb, $rgbOffset, 6);
$this->assertSame([17, 34, 51], $rgb['trns']);
$this->assertSame(10, $rgbOffset);
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \RangeException
*/
public function testGetIccpChunkThrowsOnInvalidCompressedProfile(): void
{
$this->bcExpectException(\Com\Tecnick\Pdf\Image\Exception::class);
$png = new PngChunkParsingHarness();
$data = $this->getBaseData();
$data['raw'] = "ICC\x00\x00BAD";
$offset = 0;
$byte = new \Com\Tecnick\File\Byte($data['raw']);
\set_error_handler(static fn(): bool => true);
try {
$png->callGetIccpChunk($byte, $data, $offset, 8);
} finally {
\restore_error_handler();
}
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
*/
public function testGetResizedRawDataPreservesIndexedTransparency(): void
{
$import = $this->getImportHarness();
$raw = \file_get_contents(__DIR__ . '/images/200x100_INDEXALPHA.png');
$this->assertIsString($raw);
$data = $this->getBaseData();
$data['raw'] = $raw;
$data['width'] = 200;
$data['height'] = 100;
// alpha=false exercises the indexed-palette transparency branch.
\set_error_handler(static fn(): bool => true);
try {
$resized = $import->callGetResizedRawData($data, 100, 50, false, 90);
} finally {
\restore_error_handler();
}
$this->assertSame(100, $resized['width']);
$this->assertSame(50, $resized['height']);
$this->assertTrue($resized['recoded']);
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testGetImageDimensionsByKeyFitWithZeroSourceReturnsBox(): void
{
$import = $this->getImportHarness();
$data = $this->getRawData('zero-source');
$data['width'] = 0;
$data['height'] = 0;
$import->setCacheEntry('zero-source', $data);
// A zero-sized source falls back to the requested bounding box as-is.
$this->assertSame(
['width' => 80, 'height' => 80],
$import->getImageDimensionsByKey('zero-source', 80, 80, true),
);
}
}
@@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
namespace Test;
/**
* @phpstan-import-type ImageBaseData from \Com\Tecnick\Pdf\Image\Import
* @phpstan-import-type ImageRawData from \Com\Tecnick\Pdf\Image\Import
* @phpstan-type ImageRef array{'iid': int, 'key': string, 'width': int, 'height': int, 'defprint': bool, 'altimgs'?: array<int, int>}
*/
class ImportProtectedMethodsHarness extends \Com\Tecnick\Pdf\Image\Import
{
/**
* @param ImageRawData $data
*
* @return ImageRawData
*
* @throws \Com\Tecnick\Pdf\Image\Exception
*/
public function callGetData(array $data, int $width, int $height, int $quality): array
{
return $this->getData($data, $width, $height, $quality);
}
/**
* @param ImageBaseData $data
*
* @return ImageRawData
*
* @throws \Com\Tecnick\Pdf\Image\Exception
*/
public function callGetResizedRawData(
array $data,
int $width,
int $height,
bool $alpha = true,
int $quality = 100,
): array {
return $this->getResizedRawData($data, $width, $height, $alpha, $quality);
}
/**
* @param ImageBaseData $data
*
* @return ImageRawData
*
* @throws \Com\Tecnick\Pdf\Image\Exception
*/
public function callGetAlphaChannelRawData(array $data): array
{
return $this->getAlphaChannelRawData($data);
}
/**
* @param ImageRef $img
* @param ImageRawData $data
*
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function callGetOutImage(array &$img, array &$data, string $sub = ''): string
{
return $this->getOutImage($img, $data, $sub);
}
/**
* @param ImageRef $img
* @param ImageRawData $data
*/
public function callGetOutAltImages(array $img, array &$data, string $sub = ''): string
{
return $this->getOutAltImages($img, $data, $sub);
}
/**
* @param ImageRawData $data
*/
public function callGetOutTransparency(array $data): string
{
return $this->getOutTransparency($data);
}
public function setPon(int $pon): void
{
$this->pon = $pon;
}
/**
* @param ImageRawData $data
*/
public function setCacheEntry(string $key, array $data): void
{
$this->cache[$key] = $data;
}
/**
* @param array<string, int> $xobjdict
*/
public function setXobjdict(array $xobjdict): void
{
$this->xobjdict = $xobjdict;
}
/**
* @param ImageRef $img
*/
public function setImageEntry(int $iid, array $img): void
{
$this->image[$iid] = $img;
}
}
+321
View File
@@ -0,0 +1,321 @@
<?php
/**
* ImportTest.php
*
* @since 2011-05-23
* @category Library
* @package PdfImage
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-image
*
* This file is part of tc-lib-pdf-image software library.
*/
namespace Test;
use PHPUnit\Framework\Attributes\DataProvider;
/**
* Unit Test
*
* @since 2011-05-23
* @category Library
* @package PdfImage
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-image
*/
class ImportTest extends TestUtil
{
protected function getTestObject(): \Com\Tecnick\Pdf\Image\Import
{
$encrypt = $this->getTestEncrypt();
return new \Com\Tecnick\Pdf\Image\Import(0.75, $encrypt, $this->getTestFileHelper());
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testGetKey(): void
{
$import = $this->getTestObject();
$result = $import->getKey('/images/200x100_RGB.png', 200, 100, 100);
$this->assertEquals('0i9dDNrAwOZdFa6L6u1zfg', $result);
// The parts are delimited, so inputs that would concatenate to the same
// string ('img' + 12 + 3 vs 'img1' + 2 + 3) must not collide.
$this->assertNotEquals($import->getKey('img', 12, 3, 100), $import->getKey('img1', 2, 3, 100));
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testConstructorUsesConfiguredFileHelper(): void
{
$encrypt = $this->getTestEncrypt();
$allowedHosts = ['localhost', 'example.test'];
$allowedPaths = [__DIR__ . '/images'];
$maxRemoteSize = 10485760;
$curlopts = [CURLOPT_TIMEOUT => 5];
$defaultCurlOpts = [CURLOPT_TIMEOUT => 12];
$fixedCurlOpts = [CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_SSL_VERIFYPEER => true];
$fileHelper = new \Com\Tecnick\File\File(
allowedHosts: $allowedHosts,
maxRemoteSize: $maxRemoteSize,
curlopts: $curlopts,
defaultCurlOpts: $defaultCurlOpts,
fixedCurlOpts: $fixedCurlOpts,
allowedPaths: $allowedPaths,
);
$import = new \Com\Tecnick\Pdf\Image\Import(0.75, $encrypt, $fileHelper);
$method = new \ReflectionProperty($import, 'fileHelper');
/** @var \Com\Tecnick\File\File $file */
$file = $method->getValue($import);
$ref = new \ReflectionClass($file);
$allowedHostsProp = $ref->getProperty('allowedHosts');
$allowedPathsProp = $ref->getProperty('allowedPaths');
$maxRemoteSizeProp = $ref->getProperty('maxRemoteSize');
$curloptsProp = $ref->getProperty('curlopts');
$defaultProp = $ref->getProperty('defaultCurlOpts');
$fixedProp = $ref->getProperty('fixedCurlOpts');
$this->assertSame($allowedHosts, $allowedHostsProp->getValue($file));
$this->assertSame($allowedPaths, $allowedPathsProp->getValue($file));
$this->assertSame($maxRemoteSize, $maxRemoteSizeProp->getValue($file));
$this->assertSame($curlopts, $curloptsProp->getValue($file));
$this->assertSame($defaultCurlOpts, $defaultProp->getValue($file));
$this->assertSame($fixedCurlOpts, $fixedProp->getValue($file));
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testGetImageDataByKeyError(): void
{
$this->bcExpectException(\Com\Tecnick\Pdf\Image\Exception::class);
$import = $this->getTestObject();
$import->getImageDataByKey('missing');
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testGetSetImageError(): void
{
$this->bcExpectException(\Com\Tecnick\Pdf\Image\Exception::class);
$import = $this->getTestObject();
$import->getSetImage(1, 2, 3, 5, 7, 17);
}
/**
* @return array<int, array<int, string>>
*/
public static function getBadAddValues(): array
{
return [
[''],
[__DIR__ . '/images/missing.png'],
['@'],
['@garbage'],
['*'],
['*http://www.example.com/image.png'],
];
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
#[DataProvider('getBadAddValues')]
public function testAddError(string $bad): void
{
$this->bcExpectException(\Com\Tecnick\Pdf\Image\Exception::class);
$import = $this->getTestObject();
$import->add($bad);
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testAdd(): void
{
$import = $this->getTestObject();
$iid = $import->add(__DIR__ . '/images/200x100_RGB.png');
$this->assertEquals('q 150.000000 0 0 75.000000 2.250000 371.250000 cm /IMG1 Do Q' . "\n", $import->getSetImage(
$iid,
3,
5,
200,
100,
600,
));
$iid = $import->add(__DIR__ . '/images/200x100_GRAY.jpg');
$this->assertEquals('q 150.000000 0 0 75.000000 2.250000 371.250000 cm /IMG2 Do Q' . "\n", $import->getSetImage(
$iid,
3,
5,
200,
100,
600,
));
$iid = $import->add(__DIR__ . '/images/200x100_GRAY.png');
$this->assertEquals('q 150.000000 0 0 75.000000 2.250000 371.250000 cm /IMG3 Do Q' . "\n", $import->getSetImage(
$iid,
3,
5,
200,
100,
600,
));
$iid = $import->add(__DIR__ . '/images/200x100_INDEX16.png');
$this->assertEquals('q 150.000000 0 0 75.000000 2.250000 371.250000 cm /IMG4 Do Q' . "\n", $import->getSetImage(
$iid,
3,
5,
200,
100,
600,
));
$iid = $import->add(__DIR__ . '/images/200x100_INDEX256.png');
$this->assertEquals('q 150.000000 0 0 75.000000 2.250000 371.250000 cm /IMG5 Do Q' . "\n", $import->getSetImage(
$iid,
3,
5,
200,
100,
600,
));
$iid = $import->add(__DIR__ . '/images/200x100_RGB.jpg');
$this->assertEquals('q 150.000000 0 0 75.000000 2.250000 371.250000 cm /IMG6 Do Q' . "\n", $import->getSetImage(
$iid,
3,
5,
200,
100,
600,
));
$iid = $import->add(__DIR__ . '/images/200x100_RGB.png');
$this->assertEquals('q 150.000000 0 0 75.000000 2.250000 371.250000 cm /IMG7 Do Q' . "\n", $import->getSetImage(
$iid,
3,
5,
200,
100,
600,
));
$iid = $import->add(__DIR__ . '/images/200x100_RGBALPHA.png');
$this->assertEquals('q 150.000000 0 0 75.000000 2.250000 371.250000 cm /IMGplain8 Do Q'
. "\n", $import->getSetImage($iid, 3, 5, 200, 100, 600));
$iid = $import->add(__DIR__ . '/images/200x100_INDEXALPHA.png');
$this->assertEquals('q 150.000000 0 0 75.000000 2.250000 371.250000 cm /IMG9 Do Q' . "\n", $import->getSetImage(
$iid,
3,
5,
200,
100,
600,
));
// resize
$iid = $import->add(__DIR__ . '/images/200x100_RGB.png', 100, 50, true, 75, true);
$this->assertEquals('q 75.000000 0 0 37.500000 2.250000 408.750000 cm /IMGmask10 Do Q'
. "\n", $import->getSetImage($iid, 3, 5, 100, 50, 600));
$iid = $import->add(__DIR__ . '/images/200x100_RGBALPHA.png', 100, 50, true, 75, true);
$this->assertEquals('q 75.000000 0 0 37.500000 2.250000 408.750000 cm /IMGmask11 Do Q'
. "\n", $import->getSetImage($iid, 3, 5, 100, 50, 600));
$iid = $import->add(__DIR__ . '/images/200x100_INDEXALPHA.png', 100, 50, true, 75, true);
$this->assertEquals('q 75.000000 0 0 37.500000 2.250000 408.750000 cm /IMGmask12 Do Q'
. "\n", $import->getSetImage($iid, 3, 5, 100, 50, 600));
$iid = $import->add(__DIR__ . '/images/200x100_RGB.jpg', 100, 50, false, 75, true, [1, 2, 3]);
$this->assertEquals('q 75.000000 0 0 37.500000 2.250000 408.750000 cm /IMG13 Do Q' . "\n", $import->getSetImage(
$iid,
3,
5,
100,
50,
600,
));
// ICC
$iid = $import->add(__DIR__ . '/images/200x100_RGBICC.png');
$this->assertEquals('q 150.000000 0 0 75.000000 2.250000 371.250000 cm /IMG14 Do Q'
. "\n", $import->getSetImage($iid, 3, 5, 200, 100, 600));
$iid = $import->add(__DIR__ . '/images/200x100_RGBICC.jpg');
$this->assertEquals('q 150.000000 0 0 75.000000 2.250000 371.250000 cm /IMG15 Do Q'
. "\n", $import->getSetImage($iid, 3, 5, 200, 100, 600));
$iid = $import->add(__DIR__ . '/images/200x100_RGBINT.png');
$this->assertEquals('q 150.000000 0 0 75.000000 2.250000 371.250000 cm /IMGplain16 Do Q'
. "\n", $import->getSetImage($iid, 3, 5, 200, 100, 600));
$iid = $import->add(__DIR__ . '/images/200x100_CMYK.jpg');
$this->assertEquals('q 150.000000 0 0 75.000000 2.250000 371.250000 cm /IMG17 Do Q'
. "\n", $import->getSetImage($iid, 3, 5, 200, 100, 600));
$key = $import->getKey(__DIR__ . '/images/200x100_INDEX256.png');
$data = $import->getImageDataByKey($key);
$this->assertEquals($key, $data['key']);
$iid = $import->add('@' . $data['raw']);
$this->assertEquals('q 150.000000 0 0 75.000000 2.250000 371.250000 cm /IMG18 Do Q'
. "\n", $import->getSetImage($iid, 3, 5, 200, 100, 600));
// disabled because of libpngerror
// $iid = $testObj->add('*http://localhost:8000/200x100_INDEX16.png');
// $this->assertEquals(
// 'q 150.000000 0 0 75.000000 2.250000 371.250000 cm /IMG18 Do Q' . "\n",
// $testObj->getSetImage($iid, 3, 5, 200, 100, 600)
// );
$out = $import->getOutImagesBlock(10);
$this->assertNotEmpty($out);
$this->assertEquals(37, $import->getObjectNumber());
$xobjectDict = $import->getXobjectDict();
$this->assertEquals(
' /IMG1 11 0 R /IMG2 12 0 R /IMG3 13 0 R /IMG4 15 0 R'
. ' /IMG5 17 0 R /IMG6 18 0 R /IMG7 11 0 R /IMGplain8 20 0 R'
. ' /IMG9 22 0 R /IMGmask10 23 0 R /IMGmask11 24 0 R'
. ' /IMGmask12 25 0 R /IMG13 27 0 R /IMG14 29 0 R'
. ' /IMG15 31 0 R /IMGplain16 33 0 R /IMG17 35 0 R /IMG18 37 0 R',
$xobjectDict,
);
$xdByKeys = $import->getXobjectDictByKeys([2, 3]);
$this->assertEquals(' /IMG2 12 0 R /IMG3 13 0 R', $xdByKeys);
}
}
+461
View File
@@ -0,0 +1,461 @@
<?php
/**
* JpegTest.php
*
* @since 2026-04-19
* @category Library
* @package PdfImage
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-image
*
* This file is part of tc-lib-pdf-image software library.
*/
namespace Test;
/**
* Jpeg class test
*
* @since 2026-04-19
* @category Library
* @package PdfImage
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-image
*
* @phpstan-import-type ImageBaseData from \Com\Tecnick\Pdf\Image\Import
*/
class JpegTest extends TestUtil
{
/**
* @throws \RangeException
*/
public function testGetDataRgbJpeg(): void
{
$import = new \Com\Tecnick\Pdf\Image\Import\Jpeg();
$file = \file_get_contents(__DIR__ . '/images/200x100_RGB.jpg');
$this->assertIsString($file);
$data = [
'bits' => 8,
'channels' => 3,
'colspace' => 'DeviceRGB',
'data' => '',
'exturl' => false,
'file' => __DIR__ . '/images/200x100_RGB.jpg',
'filter' => '',
'height' => 100,
'icc' => '',
'ismask' => false,
'key' => 'test',
'mapto' => IMAGETYPE_JPEG,
'native' => true,
'obj' => 0,
'obj_alt' => 0,
'obj_icc' => 0,
'obj_pal' => 0,
'pal' => '',
'parms' => '',
'raw' => $file,
'recode' => false,
'recoded' => false,
'splitalpha' => false,
'trns' => [],
'type' => IMAGETYPE_JPEG,
'width' => 200,
];
$result = $import->getData($data);
$this->assertEquals('DCTDecode', $result['filter']);
$this->assertNotEmpty($result['data']);
$this->assertEquals($file, $result['data']);
}
/**
* @throws \RangeException
*/
public function testGetDataGrayJpeg(): void
{
$import = new \Com\Tecnick\Pdf\Image\Import\Jpeg();
$file = \file_get_contents(__DIR__ . '/images/200x100_GRAY.jpg');
$this->assertIsString($file);
$data = [
'bits' => 8,
'channels' => 1,
'colspace' => 'DeviceGray',
'data' => '',
'exturl' => false,
'file' => __DIR__ . '/images/200x100_GRAY.jpg',
'filter' => '',
'height' => 100,
'icc' => '',
'ismask' => false,
'key' => 'test',
'mapto' => IMAGETYPE_JPEG,
'native' => true,
'obj' => 0,
'obj_alt' => 0,
'obj_icc' => 0,
'obj_pal' => 0,
'pal' => '',
'parms' => '',
'raw' => $file,
'recode' => false,
'recoded' => false,
'splitalpha' => false,
'trns' => [],
'type' => IMAGETYPE_JPEG,
'width' => 200,
];
$result = $import->getData($data);
$this->assertEquals('DCTDecode', $result['filter']);
$this->assertEquals(1, $result['channels']);
}
/**
* @throws \RangeException
*/
public function testGetDataCmykJpeg(): void
{
$import = new \Com\Tecnick\Pdf\Image\Import\Jpeg();
$file = \file_get_contents(__DIR__ . '/images/200x100_CMYK.jpg');
$this->assertIsString($file);
$data = [
'bits' => 8,
'channels' => 4,
'colspace' => 'DeviceCMYK',
'data' => '',
'exturl' => false,
'file' => __DIR__ . '/images/200x100_CMYK.jpg',
'filter' => '',
'height' => 100,
'icc' => '',
'ismask' => false,
'key' => 'test',
'mapto' => IMAGETYPE_JPEG,
'native' => true,
'obj' => 0,
'obj_alt' => 0,
'obj_icc' => 0,
'obj_pal' => 0,
'pal' => '',
'parms' => '',
'raw' => $file,
'recode' => false,
'recoded' => false,
'splitalpha' => false,
'trns' => [],
'type' => IMAGETYPE_JPEG,
'width' => 200,
];
$result = $import->getData($data);
$this->assertEquals('DCTDecode', $result['filter']);
$this->assertEquals(4, $result['channels']);
$this->assertEquals('DeviceCMYK', $result['colspace']);
}
/**
* @throws \RangeException
*/
public function testGetDataJpegWithIcc(): void
{
$import = new \Com\Tecnick\Pdf\Image\Import\Jpeg();
$file = \file_get_contents(__DIR__ . '/images/200x100_RGBICC.jpg');
$this->assertIsString($file);
$data = [
'bits' => 8,
'channels' => 3,
'colspace' => 'DeviceRGB',
'data' => '',
'exturl' => false,
'file' => __DIR__ . '/images/200x100_RGBICC.jpg',
'filter' => '',
'height' => 100,
'icc' => '',
'ismask' => false,
'key' => 'test',
'mapto' => IMAGETYPE_JPEG,
'native' => true,
'obj' => 0,
'obj_alt' => 0,
'obj_icc' => 0,
'obj_pal' => 0,
'pal' => '',
'parms' => '',
'raw' => $file,
'recode' => false,
'recoded' => false,
'splitalpha' => false,
'trns' => [],
'type' => IMAGETYPE_JPEG,
'width' => 200,
];
$result = $import->getData($data);
$this->assertEquals('DCTDecode', $result['filter']);
$this->assertNotEmpty($result['icc']);
}
/**
* @throws \RangeException
*/
public function testGetDataJpegWithoutIcc(): void
{
$import = new \Com\Tecnick\Pdf\Image\Import\Jpeg();
$file = \file_get_contents(__DIR__ . '/images/200x100_RGB.jpg');
$this->assertIsString($file);
$data = [
'bits' => 8,
'channels' => 3,
'colspace' => 'DeviceRGB',
'data' => '',
'exturl' => false,
'file' => __DIR__ . '/images/200x100_RGB.jpg',
'filter' => '',
'height' => 100,
'icc' => '',
'ismask' => false,
'key' => 'test',
'mapto' => IMAGETYPE_JPEG,
'native' => true,
'obj' => 0,
'obj_alt' => 0,
'obj_icc' => 0,
'obj_pal' => 0,
'pal' => '',
'parms' => '',
'raw' => $file,
'recode' => false,
'recoded' => false,
'splitalpha' => false,
'trns' => [],
'type' => IMAGETYPE_JPEG,
'width' => 200,
];
$result = $import->getData($data);
$this->assertEquals('DCTDecode', $result['filter']);
$this->assertEmpty($result['icc']);
}
/**
* @throws \RangeException
*/
public function testGetDataPreservesRawData(): void
{
$import = new \Com\Tecnick\Pdf\Image\Import\Jpeg();
$file = \file_get_contents(__DIR__ . '/images/200x100_RGB.jpg');
$this->assertIsString($file);
$data = [
'bits' => 8,
'channels' => 3,
'colspace' => 'DeviceRGB',
'data' => '',
'exturl' => false,
'file' => __DIR__ . '/images/200x100_RGB.jpg',
'filter' => '',
'height' => 100,
'icc' => '',
'ismask' => false,
'key' => 'test',
'mapto' => IMAGETYPE_JPEG,
'native' => true,
'obj' => 0,
'obj_alt' => 0,
'obj_icc' => 0,
'obj_pal' => 0,
'pal' => '',
'parms' => '',
'raw' => $file,
'recode' => false,
'recoded' => false,
'splitalpha' => false,
'trns' => [],
'type' => IMAGETYPE_JPEG,
'width' => 200,
];
$result = $import->getData($data);
$this->assertEquals($file, $result['raw']);
$this->assertEquals($file, $result['data']);
}
/**
* @throws \RangeException
*/
public function testGetDataWithMissingIcc(): void
{
$import = new \Com\Tecnick\Pdf\Image\Import\Jpeg();
$file = \file_get_contents(__DIR__ . '/images/200x100_RGB.jpg');
$this->assertIsString($file);
$data = [
'bits' => 8,
'channels' => 3,
'colspace' => 'DeviceRGB',
'data' => '',
'exturl' => false,
'file' => __DIR__ . '/images/200x100_RGB.jpg',
'filter' => '',
'height' => 100,
'icc' => '',
'ismask' => false,
'key' => 'test',
'mapto' => IMAGETYPE_JPEG,
'native' => true,
'obj' => 0,
'obj_alt' => 0,
'obj_icc' => 0,
'obj_pal' => 0,
'pal' => '',
'parms' => '',
'raw' => $file,
'recode' => false,
'recoded' => false,
'splitalpha' => false,
'trns' => [],
'type' => IMAGETYPE_JPEG,
'width' => 200,
];
$result = $import->getData($data);
$this->assertEquals('DCTDecode', $result['filter']);
$this->assertEquals('DeviceRGB', $result['colspace']);
}
/**
* @throws \RangeException
*/
public function testGetDataPreservesChannels(): void
{
$import = new \Com\Tecnick\Pdf\Image\Import\Jpeg();
$file = \file_get_contents(__DIR__ . '/images/200x100_GRAY.jpg');
$this->assertIsString($file);
$data = [
'bits' => 8,
'channels' => 1,
'colspace' => 'DeviceGray',
'data' => '',
'exturl' => false,
'file' => __DIR__ . '/images/200x100_GRAY.jpg',
'filter' => '',
'height' => 100,
'icc' => '',
'ismask' => false,
'key' => 'test',
'mapto' => IMAGETYPE_JPEG,
'native' => true,
'obj' => 0,
'obj_alt' => 0,
'obj_icc' => 0,
'obj_pal' => 0,
'pal' => '',
'parms' => '',
'raw' => $file,
'recode' => false,
'recoded' => false,
'splitalpha' => false,
'trns' => [],
'type' => IMAGETYPE_JPEG,
'width' => 200,
];
$result = $import->getData($data);
$this->assertEquals(1, $result['channels']);
$this->assertEquals($file, $result['data']);
}
/**
* A crafted "ICC_PROFILE" marker preceded by a 2-byte length of 0 yields a
* negative segment length. The scanner must still move forward instead of
* re-finding the same marker forever (DoS regression).
*
* @throws \RangeException
*/
public function testGetDataDoesNotHangOnMalformedIccMarker(): void
{
$import = new \Com\Tecnick\Pdf\Image\Import\Jpeg();
$raw = 'XX' . "\x00\x00" . 'ICC_PROFILE' . "\x00\x01\x01" . 'PAYLOAD';
$data = $this->makeJpegData($raw);
$result = $import->getData($data);
$this->assertSame('DCTDecode', $result['filter']);
$this->assertSame('', $result['icc']);
$this->assertSame($raw, $result['data']);
}
/**
* A marker at the very start of the stream has too few bytes before it to
* carry a length: it must be skipped without throwing or looping.
*
* @throws \RangeException
*/
public function testGetDataIgnoresIccMarkerWithoutLengthPrefix(): void
{
$import = new \Com\Tecnick\Pdf\Image\Import\Jpeg();
$raw = 'ICC_PROFILE' . "\x00" . 'rest';
$data = $this->makeJpegData($raw);
$result = $import->getData($data);
$this->assertSame('', $result['icc']);
}
/**
* Build a minimal JPEG base-data array wrapping the given raw bytes.
*
* @return ImageBaseData
*/
private function makeJpegData(string $raw): array
{
return [
'bits' => 8,
'channels' => 3,
'colspace' => 'DeviceRGB',
'data' => '',
'exturl' => false,
'file' => '',
'filter' => '',
'height' => 100,
'icc' => '',
'ismask' => false,
'key' => 'test',
'mapto' => IMAGETYPE_JPEG,
'native' => true,
'obj' => 0,
'obj_alt' => 0,
'obj_icc' => 0,
'obj_pal' => 0,
'pal' => '',
'parms' => '',
'raw' => $raw,
'recode' => false,
'recoded' => false,
'splitalpha' => false,
'trns' => [],
'type' => IMAGETYPE_JPEG,
'width' => 200,
];
}
}
+309
View File
@@ -0,0 +1,309 @@
<?php
/**
* OutputTest.php
*
* @since 2026-04-19
* @category Library
* @package PdfImage
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-image
*
* This file is part of tc-lib-pdf-image software library.
*/
namespace Test;
/**
* Output class test
*
* @since 2026-04-19
* @category Library
* @package PdfImage
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-image
*/
class OutputTest extends TestUtil
{
/**
* @throws \RuntimeException
*/
public function throwWithFileHelperCallbackError(): void
{
throw new \RuntimeException('withFileHelper callback error');
}
protected function getTestObject(): \Com\Tecnick\Pdf\Image\Import
{
$encrypt = $this->getTestEncrypt();
return new \Com\Tecnick\Pdf\Image\Import(0.75, $encrypt, $this->getTestFileHelper());
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testGetObjectNumber(): void
{
$import = $this->getTestObject();
// Add some images and check object number increases after calling getOutImagesBlock
$import->add(__DIR__ . '/images/200x100_RGB.png');
$import->getOutImagesBlock(10);
$this->assertGreaterThan(10, $import->getObjectNumber());
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testWithFileHelperRestoresAfterSuccess(): void
{
$import = $this->getTestObject();
$fileHelperProp = new \ReflectionProperty($import, 'fileHelper');
/** @var \Com\Tecnick\File\File $originalFileHelper */
$originalFileHelper = $fileHelperProp->getValue($import);
$tempFileHelper = new \Com\Tecnick\File\File(allowedHosts: ['localhost'], allowedPaths: ['*']);
$result = $import->withFileHelper($tempFileHelper, function () use (
$import,
$fileHelperProp,
$tempFileHelper,
): string {
$this->assertSame($tempFileHelper, $fileHelperProp->getValue($import));
return 'ok';
});
$this->assertSame('ok', $result);
$this->assertSame($originalFileHelper, $fileHelperProp->getValue($import));
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testWithFileHelperRestoresAfterException(): void
{
$import = $this->getTestObject();
$fileHelperProp = new \ReflectionProperty($import, 'fileHelper');
/** @var \Com\Tecnick\File\File $originalFileHelper */
$originalFileHelper = $fileHelperProp->getValue($import);
$tempFileHelper = new \Com\Tecnick\File\File(allowedHosts: ['example.test'], allowedPaths: ['*']);
$this->bcExpectException(\RuntimeException::class);
try {
$import->withFileHelper($tempFileHelper, [$this, 'throwWithFileHelperCallbackError']);
} finally {
$this->assertSame($originalFileHelper, $fileHelperProp->getValue($import));
}
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testGetXobjectDictEmpty(): void
{
$import = $this->getTestObject();
$this->assertEquals('', $import->getXobjectDict());
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testGetXobjectDictByKeysEmpty(): void
{
$import = $this->getTestObject();
$this->assertEquals('', $import->getXobjectDictByKeys([]));
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testGetXobjectDictByKeysInvalid(): void
{
$import = $this->getTestObject();
$import->add(__DIR__ . '/images/200x100_RGB.png');
$import->getOutImagesBlock(10);
// Test with non-existent image IDs
$result = $import->getXobjectDictByKeys([999]);
$this->assertEquals('', $result);
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testGetXobjectDictByKeysWithValidImage(): void
{
$import = $this->getTestObject();
$iid = $import->add(__DIR__ . '/images/200x100_RGB.png');
$import->getOutImagesBlock(10);
$result = $import->getXobjectDictByKeys([$iid]);
$this->assertNotEmpty($result);
$this->assertStringContainsString('IMG' . $iid, $result);
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testGetOutImagesBlockMultiple(): void
{
$import = $this->getTestObject();
$import->add(__DIR__ . '/images/200x100_RGB.png');
$import->add(__DIR__ . '/images/200x100_GRAY.jpg');
$result = $import->getOutImagesBlock(10);
$this->assertNotEmpty($result);
$this->assertStringContainsString('XObject', $result);
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testGetOutImagesBlockObjectNumberIncrement(): void
{
$import = $this->getTestObject();
$import->add(__DIR__ . '/images/200x100_RGB.png');
$import->add(__DIR__ . '/images/200x100_RGBALPHA.png');
$import->getOutImagesBlock(20);
$this->assertGreaterThan(20, $import->getObjectNumber());
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testGetSetImageWithMask(): void
{
$import = $this->getTestObject();
$iid = $import->add(__DIR__ . '/images/200x100_RGBALPHA.png', 100, 50, true, 75, true);
$result = $import->getSetImage($iid, 3, 5, 100, 50, 600);
$this->assertStringContainsString('IMGmask' . $iid, $result);
$this->assertStringContainsString('cm /IMGmask' . $iid . ' Do Q', $result);
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testGetSetImageRealOutput(): void
{
$import = $this->getTestObject();
$iid = $import->add(__DIR__ . '/images/200x100_RGB.png');
$result = $import->getSetImage($iid, 3, 5, 200, 100, 600);
$this->assertStringContainsString('q ', $result);
$this->assertStringContainsString('Do Q', $result);
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testGetSetImageWithoutMaskOrPlain(): void
{
$import = $this->getTestObject();
$iid = $import->add(__DIR__ . '/images/200x100_RGB.png');
$result = $import->getSetImage($iid, 3, 5, 200, 100, 600);
$this->assertStringContainsString('/IMG' . $iid . ' Do Q', $result);
$this->assertStringNotContainsString('IMGplain', $result);
$this->assertStringNotContainsString('IMGmask', $result);
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testGetSetImageWithCoordinates(): void
{
$import = $this->getTestObject();
$iid = $import->add(__DIR__ . '/images/200x100_RGB.png');
// Test with different coordinates and page height
$result = $import->getSetImage($iid, 10, 20, 100, 50, 800);
$this->assertStringContainsString('q 75.000000 0 0 37.500000', $result);
$this->assertStringContainsString('/IMG' . $iid . ' Do Q', $result);
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testGetSetImageErrorInvalidId(): void
{
$this->bcExpectException(\Com\Tecnick\Pdf\Image\Exception::class);
$import = $this->getTestObject();
$import->getSetImage(999, 0, 0, 100, 100, 600);
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testGetSetImageMultipleImages(): void
{
$import = $this->getTestObject();
$iid1 = $import->add(__DIR__ . '/images/200x100_RGB.png');
$iid2 = $import->add(__DIR__ . '/images/200x100_GRAY.jpg');
$result1 = $import->getSetImage($iid1, 0, 0, 100, 100, 600);
$result2 = $import->getSetImage($iid2, 10, 10, 150, 150, 600);
$this->assertNotEmpty($result1);
$this->assertNotEmpty($result2);
$this->assertStringContainsString('IMG' . $iid1, $result1);
$this->assertStringContainsString('IMG' . $iid2, $result2);
}
/**
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Encrypt\Exception
*/
public function testGetSetImageVariousCoordinates(): void
{
$import = $this->getTestObject();
$iid = $import->add(__DIR__ . '/images/200x100_RGB.png');
// Test with zero coordinates
$result = $import->getSetImage($iid, 0, 0, 100, 100, 600);
$this->assertStringContainsString('0.000000', $result);
$this->assertStringContainsString('cm /IMG', $result);
// Test with large coordinates
$result2 = $import->getSetImage($iid, 100, 200, 300, 400, 1000);
$this->assertStringContainsString('300.000000', $result2);
$this->assertStringContainsString('cm /IMG', $result2);
}
}
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Test;
/**
* @phpstan-import-type ImageBaseData from \Com\Tecnick\Pdf\Image\Import
*/
class PngChunkParsingHarness extends \Com\Tecnick\Pdf\Image\Import\Png
{
/**
* @param ImageBaseData $data
*
* @return ImageBaseData
*/
public function callGetTrnsChunk(array $data, int &$offset, int $len): array
{
return $this->getTrnsChunk($data, $offset, $len);
}
/**
* @param ImageBaseData $data
*
* @return ImageBaseData
*
* @throws \Com\Tecnick\Pdf\Image\Exception
* @throws \RangeException
*/
public function callGetIccpChunk(\Com\Tecnick\File\Byte $byte, array $data, int &$offset, int $len): array
{
return $this->getIccpChunk($byte, $data, $offset, $len);
}
}
@@ -0,0 +1,85 @@
<?php
/**
* SpyImageCache.php
*
* @since 2026-06-16
* @category Library
* @package PdfImage
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-image
*
* This file is part of tc-lib-pdf-image software library.
*/
namespace Test;
use Com\Tecnick\Pdf\Image\ImageCacheInterface;
/**
* In-memory ImageCacheInterface implementation that records access for tests.
*
* @phpstan-import-type ImageRawData from \Com\Tecnick\Pdf\Image\Import
*/
class SpyImageCache implements ImageCacheInterface
{
/**
* Backing store.
*
* @var array<string, ImageRawData>
*/
public array $store = [];
/**
* Number of get() calls.
*/
public int $getCount = 0;
/**
* Number of set() calls.
*/
public int $setCount = 0;
/**
* Keys passed to set(), in order.
*
* @var list<string>
*/
public array $setKeys = [];
/**
* @return ImageRawData|null
*/
public function get(string $key): ?array
{
++$this->getCount;
return $this->store[$key] ?? null;
}
/**
* Fetch a stored entry by key, failing loudly when it is absent.
*
* @return ImageRawData
*/
public function fetch(string $key): array
{
$data = $this->store[$key] ?? null;
if ($data === null) {
throw new \LogicException('No cached entry for key: ' . $key);
}
return $data;
}
/**
* @param ImageRawData $data
*/
public function set(string $key, array $data): void
{
++$this->setCount;
$this->setKeys[] = $key;
$this->store[$key] = $data;
}
}
+63
View File
@@ -0,0 +1,63 @@
<?php
/**
* TestUtil.php
*
* @since 2020-12-19
* @category Library
* @package PdfImage
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2015-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-image
*
* This file is part of tc-lib-color software library.
*/
namespace Test;
use PHPUnit\Framework\TestCase;
/**
* Web Color class test
*
* @since 2020-12-19
* @category Library
* @package PdfImage
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2015-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-image
*/
class TestUtil extends TestCase
{
protected function getTestFileHelper(): \Com\Tecnick\File\File
{
return new \Com\Tecnick\File\File(allowedHosts: ['*'], allowedPaths: ['*']);
}
protected function getTestEncrypt(): \Com\Tecnick\Pdf\Encrypt\Encrypt
{
return new class extends \Com\Tecnick\Pdf\Encrypt\Encrypt {
public function __construct() {}
public function encryptString(string $str, int $objnum = 0): string
{
return $str;
}
public function escapeDataString(string $str, int $objnum = 0): string
{
return '(' . $str . ')';
}
};
}
/**
* @param class-string<\Throwable> $exception
*/
public function bcExpectException(string $exception): void
{
parent::expectException($exception);
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 960 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB