generated from jric11/baseProject
Initial commit
This commit is contained in:
@@ -0,0 +1,982 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* PageResolverTest.php
|
||||
*
|
||||
* @since 2002-08-03
|
||||
* @category Library
|
||||
* @package Pdf
|
||||
* @author Nicola Asuni <info@tecnick.com>
|
||||
* @copyright 2002-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
|
||||
*
|
||||
* This file is part of tc-lib-pdf software library.
|
||||
*/
|
||||
|
||||
namespace Test\Import;
|
||||
|
||||
use Com\Tecnick\Pdf\Import\ImportCorruptedSourceException;
|
||||
use Com\Tecnick\Pdf\Import\ImportPageOutOfRangeException;
|
||||
use Com\Tecnick\Pdf\Import\PageResolver;
|
||||
use Com\Tecnick\Pdf\Import\SourceDocument;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class PageResolverTest extends TestCase
|
||||
{
|
||||
private function invokeResolverMethod(PageResolver $resolver, string $method, mixed ...$args): mixed
|
||||
{
|
||||
$ref = new \ReflectionClass($resolver);
|
||||
return $ref->getMethod($method)->invokeArgs($resolver, $args);
|
||||
}
|
||||
|
||||
/** @throws \Throwable */
|
||||
private function loadDoc(): SourceDocument
|
||||
{
|
||||
$path = __DIR__ . '/../fixtures/simple_import.pdf';
|
||||
$data = file_get_contents($path);
|
||||
$this->assertNotFalse($data);
|
||||
return new SourceDocument($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, mixed> $pairs
|
||||
* @return array<int, mixed>
|
||||
*/
|
||||
private function dictObject(array $pairs): array
|
||||
{
|
||||
return [['<<', $pairs]];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array<int, mixed>> $objects
|
||||
* @throws \Throwable
|
||||
*/
|
||||
private function mockDoc(array $objects): SourceDocument
|
||||
{
|
||||
$doc = $this->createStub(SourceDocument::class);
|
||||
$doc->method('getTrailer')->willReturn(['root' => '1 0 R']);
|
||||
$doc->method('getObject')->willReturnCallback(static fn(string $ref): array => $objects[$ref] ?? []);
|
||||
$doc->method('findObject')->willReturnCallback(static fn(string $ref): ?array => $objects[$ref] ?? null);
|
||||
|
||||
return $doc;
|
||||
}
|
||||
|
||||
/** @throws \Throwable */
|
||||
public function testResolvePage1ReturnsExpectedMediaBox(): void
|
||||
{
|
||||
$resolver = new PageResolver();
|
||||
$resolved = $resolver->resolve($this->loadDoc(), 1);
|
||||
$this->assertArrayHasKey('mediaBox', $resolved);
|
||||
$this->assertCount(4, $resolved['mediaBox']);
|
||||
$this->assertEqualsWithDelta(612.0, $resolved['mediaBox'][2], 0.001);
|
||||
$this->assertEqualsWithDelta(792.0, $resolved['mediaBox'][3], 0.001);
|
||||
}
|
||||
|
||||
/** @throws \Throwable */
|
||||
public function testResolvePage1HasResources(): void
|
||||
{
|
||||
$resolver = new PageResolver();
|
||||
$resolved = $resolver->resolve($this->loadDoc(), 1);
|
||||
$this->assertArrayHasKey('resources', $resolved);
|
||||
}
|
||||
|
||||
/** @throws \Throwable */
|
||||
public function testResolvePage1RotateIsZero(): void
|
||||
{
|
||||
$resolver = new PageResolver();
|
||||
$resolved = $resolver->resolve($this->loadDoc(), 1);
|
||||
$this->assertSame(0, $resolved['rotate']);
|
||||
}
|
||||
|
||||
/** @throws \Throwable */
|
||||
public function testResolveThrowsForPageZero(): void
|
||||
{
|
||||
$resolver = new PageResolver();
|
||||
$this->expectException(ImportPageOutOfRangeException::class);
|
||||
$resolver->resolve($this->loadDoc(), 0);
|
||||
}
|
||||
|
||||
/** @throws \Throwable */
|
||||
public function testResolveThrowsForPageOutOfRange(): void
|
||||
{
|
||||
$resolver = new PageResolver();
|
||||
$this->expectException(ImportPageOutOfRangeException::class);
|
||||
$resolver->resolve($this->loadDoc(), 999);
|
||||
}
|
||||
|
||||
/** @throws \Throwable */
|
||||
public function testResolveThrowsWhenRootPagesEntryIsMissing(): void
|
||||
{
|
||||
$resolver = new PageResolver();
|
||||
$doc = $this->mockDoc([
|
||||
'1_0' => $this->dictObject([
|
||||
['/', 'Type'],
|
||||
['/', 'Catalog'],
|
||||
]),
|
||||
]);
|
||||
|
||||
$this->expectException(ImportCorruptedSourceException::class);
|
||||
$this->expectExceptionMessageMatches('/' . preg_quote('missing /Pages entry', '/') . '/');
|
||||
$resolver->resolve($doc, 1);
|
||||
}
|
||||
|
||||
/** @throws \Throwable */
|
||||
public function testResolveParsesInheritedRotateAndIndirectResources(): void
|
||||
{
|
||||
$resolver = new PageResolver();
|
||||
$doc = $this->mockDoc([
|
||||
'1_0' => $this->dictObject([
|
||||
['/', 'Pages'],
|
||||
['objref', '2 0 R'],
|
||||
]),
|
||||
'2_0' => $this->dictObject([
|
||||
['/', 'Type'],
|
||||
['/', 'Pages'],
|
||||
['/', 'Kids'],
|
||||
[
|
||||
'[',
|
||||
[
|
||||
['objref', '3 0 R'],
|
||||
],
|
||||
],
|
||||
['/', 'MediaBox'],
|
||||
[
|
||||
'[',
|
||||
[
|
||||
['numeric', 0],
|
||||
['numeric', 0],
|
||||
['numeric', 612],
|
||||
['numeric', 792],
|
||||
],
|
||||
],
|
||||
['/', 'Rotate'],
|
||||
['numeric', '180'],
|
||||
['/', 'Resources'],
|
||||
['objref', '4 0 R'],
|
||||
]),
|
||||
'3_0' => $this->dictObject([
|
||||
['/', 'Type'],
|
||||
['/', 'Page'],
|
||||
]),
|
||||
'4_0' => $this->dictObject([
|
||||
['/', 'Font'],
|
||||
[
|
||||
'<<',
|
||||
[
|
||||
['/', 'F1'],
|
||||
['objref', '5 0 R'],
|
||||
],
|
||||
],
|
||||
]),
|
||||
'5_0' => $this->dictObject([
|
||||
['/', 'Type'],
|
||||
['/', 'Font'],
|
||||
]),
|
||||
]);
|
||||
|
||||
$resolved = $resolver->resolve($doc, 1);
|
||||
|
||||
$this->assertSame(180, $resolved['rotate']);
|
||||
$this->assertArrayHasKey('Font', $resolved['resources']);
|
||||
if (!isset($resolved['resources']['Font']) || !\is_array($resolved['resources']['Font'])) {
|
||||
$this->fail('Expected Font resources array.');
|
||||
}
|
||||
|
||||
$this->assertArrayHasKey('F1', $resolved['resources']['Font']);
|
||||
}
|
||||
|
||||
/** @throws \Throwable */
|
||||
public function testResolveSkipsInvalidKidsAndUsesInlineResourcesAndBoxFallbacks(): void
|
||||
{
|
||||
$resolver = new PageResolver();
|
||||
$doc = $this->mockDoc([
|
||||
'1_0' => $this->dictObject([
|
||||
['/', 'Pages'],
|
||||
['objref', '2 0 R'],
|
||||
]),
|
||||
'2_0' => $this->dictObject([
|
||||
['/', 'Type'],
|
||||
['/', 'Pages'],
|
||||
['/', 'Kids'],
|
||||
[
|
||||
'[',
|
||||
[
|
||||
['numeric', 7],
|
||||
['objref', '3 0 R'],
|
||||
],
|
||||
],
|
||||
['/', 'MediaBox'],
|
||||
[
|
||||
'[',
|
||||
[
|
||||
['numeric', 0],
|
||||
['numeric', 0],
|
||||
['numeric', 400],
|
||||
['numeric', 600],
|
||||
],
|
||||
],
|
||||
]),
|
||||
'3_0' => $this->dictObject([
|
||||
['/', 'Type'],
|
||||
['/', 'Page'],
|
||||
['/', 'CropBox'],
|
||||
['string', 'invalid'],
|
||||
['/', 'Rotate'],
|
||||
['numeric', 90],
|
||||
['/', 'Resources'],
|
||||
[
|
||||
'<<',
|
||||
[
|
||||
['/', 'ProcSet'],
|
||||
[
|
||||
'[',
|
||||
[
|
||||
['/', '/PDF'],
|
||||
['/', '/Text'],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
]),
|
||||
]);
|
||||
|
||||
$resolved = $resolver->resolve($doc, 1);
|
||||
|
||||
$this->assertSame(90, $resolved['rotate']);
|
||||
$this->assertSame([0.0, 0.0, 400.0, 600.0], $resolved['mediaBox']);
|
||||
$this->assertSame($resolved['mediaBox'], $resolved['cropBox']);
|
||||
$this->assertSame($resolved['cropBox'], $resolved['bleedBox']);
|
||||
$this->assertSame($resolved['cropBox'], $resolved['trimBox']);
|
||||
$this->assertSame($resolved['cropBox'], $resolved['artBox']);
|
||||
$this->assertSame(['/PDF', '/Text'], $resolved['resources']['ProcSet'] ?? null);
|
||||
}
|
||||
|
||||
/** @throws \Throwable */
|
||||
public function testResolveThrowsWhenResolvedPageIsMissingMediaBox(): void
|
||||
{
|
||||
$resolver = new PageResolver();
|
||||
$doc = $this->mockDoc([
|
||||
'1_0' => $this->dictObject([
|
||||
['/', 'Pages'],
|
||||
['objref', '2 0 R'],
|
||||
]),
|
||||
'2_0' => $this->dictObject([
|
||||
['/', 'Type'],
|
||||
['/', 'Pages'],
|
||||
['/', 'Kids'],
|
||||
[
|
||||
'[',
|
||||
[
|
||||
['objref', '3 0 R'],
|
||||
],
|
||||
],
|
||||
]),
|
||||
'3_0' => $this->dictObject([
|
||||
['/', 'Type'],
|
||||
['/', 'Page'],
|
||||
]),
|
||||
]);
|
||||
|
||||
$this->expectException(ImportCorruptedSourceException::class);
|
||||
$this->expectExceptionMessageMatches('/' . preg_quote('missing /MediaBox', '/') . '/');
|
||||
$resolver->resolve($doc, 1);
|
||||
}
|
||||
|
||||
/** @throws \Throwable */
|
||||
public function testResolveAcceptsIndirectMediaBoxReference(): void
|
||||
{
|
||||
$resolver = new PageResolver();
|
||||
$doc = $this->mockDoc([
|
||||
'1_0' => $this->dictObject([
|
||||
['/', 'Pages'],
|
||||
['objref', '2 0 R'],
|
||||
]),
|
||||
'2_0' => $this->dictObject([
|
||||
['/', 'Type'],
|
||||
['/', 'Pages'],
|
||||
['/', 'Kids'],
|
||||
[
|
||||
'[',
|
||||
[
|
||||
['objref', '3 0 R'],
|
||||
],
|
||||
],
|
||||
]),
|
||||
'3_0' => $this->dictObject([
|
||||
['/', 'Type'],
|
||||
['/', 'Page'],
|
||||
['/', 'MediaBox'],
|
||||
['objref', '8 0 R'],
|
||||
]),
|
||||
'8_0' => [
|
||||
[
|
||||
'[',
|
||||
[
|
||||
['numeric', 0],
|
||||
['numeric', 0],
|
||||
['numeric', 612],
|
||||
['numeric', 792],
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$resolved = $resolver->resolve($doc, 1);
|
||||
|
||||
$this->assertSame([0.0, 0.0, 612.0, 792.0], $resolved['mediaBox']);
|
||||
$this->assertSame($resolved['mediaBox'], $resolved['cropBox']);
|
||||
}
|
||||
|
||||
/** @throws \Throwable */
|
||||
public function testResolveThrowsWhenIndirectMediaBoxReferenceIsMissing(): void
|
||||
{
|
||||
$resolver = new PageResolver();
|
||||
$doc = $this->mockDoc([
|
||||
'1_0' => $this->dictObject([
|
||||
['/', 'Pages'],
|
||||
['objref', '2 0 R'],
|
||||
]),
|
||||
'2_0' => $this->dictObject([
|
||||
['/', 'Type'],
|
||||
['/', 'Pages'],
|
||||
['/', 'Kids'],
|
||||
[
|
||||
'[',
|
||||
[
|
||||
['objref', '3 0 R'],
|
||||
],
|
||||
],
|
||||
]),
|
||||
'3_0' => $this->dictObject([
|
||||
['/', 'Type'],
|
||||
['/', 'Page'],
|
||||
['/', 'MediaBox'],
|
||||
['objref', '999 0 R'],
|
||||
]),
|
||||
]);
|
||||
|
||||
$this->expectException(ImportCorruptedSourceException::class);
|
||||
$this->expectExceptionMessageMatches('/' . preg_quote('missing /MediaBox', '/') . '/');
|
||||
$resolver->resolve($doc, 1);
|
||||
}
|
||||
|
||||
/** @throws \Throwable */
|
||||
public function testResolveThrowsWhenIndirectMediaBoxObjectIsMalformed(): void
|
||||
{
|
||||
$resolver = new PageResolver();
|
||||
$doc = $this->mockDoc([
|
||||
'1_0' => $this->dictObject([
|
||||
['/', 'Pages'],
|
||||
['objref', '2 0 R'],
|
||||
]),
|
||||
'2_0' => $this->dictObject([
|
||||
['/', 'Type'],
|
||||
['/', 'Pages'],
|
||||
['/', 'Kids'],
|
||||
[
|
||||
'[',
|
||||
[
|
||||
['objref', '3 0 R'],
|
||||
],
|
||||
],
|
||||
]),
|
||||
'3_0' => $this->dictObject([
|
||||
['/', 'Type'],
|
||||
['/', 'Page'],
|
||||
['/', 'MediaBox'],
|
||||
['objref', '8 0 R'],
|
||||
]),
|
||||
'8_0' => [
|
||||
['keyword', 'invalid-box-token'],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->expectException(ImportCorruptedSourceException::class);
|
||||
$this->expectExceptionMessageMatches('/' . preg_quote('missing /MediaBox', '/') . '/');
|
||||
$resolver->resolve($doc, 1);
|
||||
}
|
||||
|
||||
/** @throws \Throwable */
|
||||
public function testResolveMergesParentAndChildResourceDictionaries(): void
|
||||
{
|
||||
$resolver = new PageResolver();
|
||||
$doc = $this->mockDoc([
|
||||
'1_0' => $this->dictObject([
|
||||
['/', 'Pages'],
|
||||
['objref', '2 0 R'],
|
||||
]),
|
||||
'2_0' => $this->dictObject([
|
||||
['/', 'Type'],
|
||||
['/', 'Pages'],
|
||||
['/', 'Kids'],
|
||||
[
|
||||
'[',
|
||||
[
|
||||
['objref', '3 0 R'],
|
||||
],
|
||||
],
|
||||
['/', 'MediaBox'],
|
||||
[
|
||||
'[',
|
||||
[
|
||||
['numeric', 0],
|
||||
['numeric', 0],
|
||||
['numeric', 200],
|
||||
['numeric', 300],
|
||||
],
|
||||
],
|
||||
['/', 'Resources'],
|
||||
[
|
||||
'<<',
|
||||
[
|
||||
['/', 'Font'],
|
||||
[
|
||||
'<<',
|
||||
[
|
||||
['/', 'F1'],
|
||||
['objref', '10 0 R'],
|
||||
],
|
||||
],
|
||||
['/', 'XObject'],
|
||||
[
|
||||
'<<',
|
||||
[
|
||||
['/', 'Im1'],
|
||||
['objref', '11 0 R'],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
]),
|
||||
'3_0' => $this->dictObject([
|
||||
['/', 'Type'],
|
||||
['/', 'Page'],
|
||||
['/', 'Resources'],
|
||||
[
|
||||
'<<',
|
||||
[
|
||||
['/', 'Font'],
|
||||
[
|
||||
'<<',
|
||||
[
|
||||
['/', 'F2'],
|
||||
['objref', '12 0 R'],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
]),
|
||||
'10_0' => $this->dictObject([
|
||||
['/', 'Type'],
|
||||
['/', 'Font'],
|
||||
]),
|
||||
'11_0' => $this->dictObject([
|
||||
['/', 'Type'],
|
||||
['/', 'XObject'],
|
||||
]),
|
||||
'12_0' => $this->dictObject([
|
||||
['/', 'Type'],
|
||||
['/', 'Font'],
|
||||
]),
|
||||
]);
|
||||
|
||||
$resolved = $resolver->resolve($doc, 1);
|
||||
|
||||
$resources = $resolved['resources'];
|
||||
$this->assertIsArray($resources);
|
||||
|
||||
if (!isset($resources['Font']) || !\is_array($resources['Font'])) {
|
||||
$this->fail('Expected merged Font dictionary.');
|
||||
}
|
||||
|
||||
if (!isset($resources['XObject']) || !\is_array($resources['XObject'])) {
|
||||
$this->fail('Expected inherited XObject dictionary.');
|
||||
}
|
||||
|
||||
$this->assertArrayHasKey('F1', $resources['Font']);
|
||||
$this->assertArrayHasKey('F2', $resources['Font']);
|
||||
$this->assertArrayHasKey('Im1', $resources['XObject']);
|
||||
}
|
||||
|
||||
/** @throws \Throwable */
|
||||
public function testResolveThrowsForPagesNodeWithoutKidsArray(): void
|
||||
{
|
||||
$resolver = new PageResolver();
|
||||
$doc = $this->mockDoc([
|
||||
'1_0' => $this->dictObject([
|
||||
['/', 'Pages'],
|
||||
['objref', '2 0 R'],
|
||||
]),
|
||||
'2_0' => $this->dictObject([
|
||||
['/', 'Type'],
|
||||
['/', 'Pages'],
|
||||
]),
|
||||
]);
|
||||
|
||||
$this->expectException(ImportCorruptedSourceException::class);
|
||||
$this->expectExceptionMessageMatches('/' . preg_quote('/Kids', '/') . '/');
|
||||
$resolver->resolve($doc, 1);
|
||||
}
|
||||
|
||||
/** @throws \Throwable */
|
||||
public function testResolveThrowsForUnexpectedPageTreeNodeType(): void
|
||||
{
|
||||
$resolver = new PageResolver();
|
||||
$doc = $this->mockDoc([
|
||||
'1_0' => $this->dictObject([
|
||||
['/', 'Pages'],
|
||||
['objref', '2 0 R'],
|
||||
]),
|
||||
'2_0' => $this->dictObject([
|
||||
['/', 'Type'],
|
||||
['/', 'Catalog'],
|
||||
['/', 'Kids'],
|
||||
[
|
||||
'[',
|
||||
[
|
||||
['objref', '3 0 R'],
|
||||
],
|
||||
],
|
||||
]),
|
||||
'3_0' => $this->dictObject([
|
||||
['/', 'Type'],
|
||||
['/', 'Page'],
|
||||
]),
|
||||
]);
|
||||
|
||||
$this->expectException(ImportCorruptedSourceException::class);
|
||||
$this->expectExceptionMessageMatches('/' . preg_quote('Unexpected page tree node type', '/') . '/');
|
||||
$resolver->resolve($doc, 1);
|
||||
}
|
||||
|
||||
public function testObjectToDictThrowsWhenDictionaryElementIsMissing(): void
|
||||
{
|
||||
$resolver = new PageResolver();
|
||||
|
||||
$this->expectException(ImportCorruptedSourceException::class);
|
||||
$this->expectExceptionMessageMatches('/' . preg_quote('Expected dictionary object', '/') . '/');
|
||||
$this->invokeResolverMethod($resolver, 'objectToDict', ['not-an-array-element']);
|
||||
}
|
||||
|
||||
public function testParseDictArraySkipsMalformedKeysAndParsesFallbackValueTypes(): void
|
||||
{
|
||||
$resolver = new PageResolver();
|
||||
|
||||
/** @var array<string, mixed> $parsed */
|
||||
$parsed = $this->invokeResolverMethod($resolver, 'parseDictArray', [
|
||||
['numeric', 1],
|
||||
['string', 'ignored-non-name-key'],
|
||||
['/', 123],
|
||||
['string', 'ignored-non-string-name'],
|
||||
['/', '/Literal'],
|
||||
'plain-text',
|
||||
['/', '/Ref'],
|
||||
['objref'],
|
||||
['/', '/Unknown'],
|
||||
['keyword', 'fallback-value'],
|
||||
['/', '/Empty'],
|
||||
[],
|
||||
]);
|
||||
|
||||
$this->assertSame(
|
||||
[
|
||||
'Literal' => 'plain-text',
|
||||
'Ref' => '',
|
||||
'Unknown' => 'fallback-value',
|
||||
'Empty' => null,
|
||||
],
|
||||
\array_intersect_key($parsed, [
|
||||
'Literal' => true,
|
||||
'Ref' => true,
|
||||
'Unknown' => true,
|
||||
'Empty' => true,
|
||||
]),
|
||||
);
|
||||
$this->assertArrayNotHasKey('123', $parsed);
|
||||
}
|
||||
|
||||
/**
|
||||
* A cyclic /Pages tree (intermediate nodes referencing one another, with no
|
||||
* Page leaf to terminate the descent) must be rejected instead of recursing
|
||||
* until the stack/memory is exhausted.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function testResolveThrowsOnCyclicPageTree(): void
|
||||
{
|
||||
$resolver = new PageResolver();
|
||||
$doc = $this->mockDoc([
|
||||
'1_0' => $this->dictObject([
|
||||
['/', 'Pages'],
|
||||
['objref', '2 0 R'],
|
||||
]),
|
||||
'2_0' => $this->dictObject([
|
||||
['/', 'Type'],
|
||||
['/', 'Pages'],
|
||||
['/', 'Kids'],
|
||||
[
|
||||
'[',
|
||||
[
|
||||
['objref', '3 0 R'],
|
||||
],
|
||||
],
|
||||
]),
|
||||
'3_0' => $this->dictObject([
|
||||
['/', 'Type'],
|
||||
['/', 'Pages'],
|
||||
['/', 'Kids'],
|
||||
[
|
||||
'[',
|
||||
[
|
||||
['objref', '2 0 R'],
|
||||
],
|
||||
],
|
||||
]),
|
||||
]);
|
||||
|
||||
$this->expectException(ImportCorruptedSourceException::class);
|
||||
$this->expectExceptionMessageMatches('/' . preg_quote('Duplicate or cyclic reference', '/') . '/');
|
||||
$resolver->resolve($doc, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* A /Pages node listing the same kid twice is malformed (every node has a
|
||||
* single /Parent) and, if tolerated, would multiply the traversal
|
||||
* exponentially with tree depth besides corrupting page numbering.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function testResolveThrowsOnDuplicateSiblingReference(): void
|
||||
{
|
||||
$resolver = new PageResolver();
|
||||
$doc = $this->mockDoc([
|
||||
'1_0' => $this->dictObject([
|
||||
['/', 'Pages'],
|
||||
['objref', '2 0 R'],
|
||||
]),
|
||||
'2_0' => $this->dictObject([
|
||||
['/', 'Type'],
|
||||
['/', 'Pages'],
|
||||
['/', 'Kids'],
|
||||
[
|
||||
'[',
|
||||
[
|
||||
['objref', '3 0 R'],
|
||||
['objref', '3 0 R'],
|
||||
],
|
||||
],
|
||||
]),
|
||||
'3_0' => $this->dictObject([
|
||||
['/', 'Type'],
|
||||
['/', 'Page'],
|
||||
]),
|
||||
]);
|
||||
|
||||
$this->expectException(ImportCorruptedSourceException::class);
|
||||
$this->expectExceptionMessageMatches('/' . preg_quote('Duplicate or cyclic reference', '/') . '/');
|
||||
$resolver->resolve($doc, 2);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// countPages
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/** @throws \Throwable */
|
||||
public function testCountPagesReturnsReachablePageCount(): void
|
||||
{
|
||||
$resolver = new PageResolver();
|
||||
$this->assertSame(1, $resolver->countPages($this->loadDoc()));
|
||||
}
|
||||
|
||||
/** @throws \Throwable */
|
||||
public function testCountPagesThrowsWhenRootPagesEntryIsMissing(): void
|
||||
{
|
||||
$resolver = new PageResolver();
|
||||
$doc = $this->mockDoc([
|
||||
'1_0' => $this->dictObject([
|
||||
['/', 'Type'],
|
||||
['/', 'Catalog'],
|
||||
]),
|
||||
]);
|
||||
|
||||
$this->expectException(ImportCorruptedSourceException::class);
|
||||
$this->expectExceptionMessageMatches('/' . preg_quote('missing /Pages entry', '/') . '/');
|
||||
$resolver->countPages($doc);
|
||||
}
|
||||
|
||||
/** @throws \Throwable */
|
||||
public function testCountPagesReturnsZeroForEmptyKidsArray(): void
|
||||
{
|
||||
$resolver = new PageResolver();
|
||||
$doc = $this->mockDoc([
|
||||
'1_0' => $this->dictObject([
|
||||
['/', 'Pages'],
|
||||
['objref', '2 0 R'],
|
||||
]),
|
||||
'2_0' => $this->dictObject([
|
||||
['/', 'Type'],
|
||||
['/', 'Pages'],
|
||||
['/', 'Kids'],
|
||||
['[', []],
|
||||
]),
|
||||
]);
|
||||
|
||||
$this->assertSame(0, $resolver->countPages($doc));
|
||||
}
|
||||
|
||||
/** @throws \Throwable */
|
||||
public function testCountPagesThrowsOnDuplicateKidReference(): void
|
||||
{
|
||||
$resolver = new PageResolver();
|
||||
$doc = $this->mockDoc([
|
||||
'1_0' => $this->dictObject([
|
||||
['/', 'Pages'],
|
||||
['objref', '2 0 R'],
|
||||
]),
|
||||
'2_0' => $this->dictObject([
|
||||
['/', 'Type'],
|
||||
['/', 'Pages'],
|
||||
['/', 'Kids'],
|
||||
[
|
||||
'[',
|
||||
[
|
||||
['objref', '3 0 R'],
|
||||
['objref', '3 0 R'],
|
||||
],
|
||||
],
|
||||
]),
|
||||
'3_0' => $this->dictObject([
|
||||
['/', 'Type'],
|
||||
['/', 'Page'],
|
||||
]),
|
||||
]);
|
||||
|
||||
$this->expectException(ImportCorruptedSourceException::class);
|
||||
$this->expectExceptionMessageMatches('/' . preg_quote('Duplicate or cyclic reference', '/') . '/');
|
||||
$resolver->countPages($doc);
|
||||
}
|
||||
|
||||
/** @throws \Throwable */
|
||||
public function testCountPagesThrowsWhenNodeBudgetExceeded(): void
|
||||
{
|
||||
$resolver = new PageResolver();
|
||||
$doc = $this->mockDoc([
|
||||
'1_0' => $this->dictObject([
|
||||
['/', 'Pages'],
|
||||
['objref', '2 0 R'],
|
||||
]),
|
||||
'2_0' => $this->dictObject([
|
||||
['/', 'Type'],
|
||||
['/', 'Pages'],
|
||||
['/', 'Kids'],
|
||||
[
|
||||
'[',
|
||||
[
|
||||
['objref', '3 0 R'],
|
||||
],
|
||||
],
|
||||
]),
|
||||
'3_0' => $this->dictObject([
|
||||
['/', 'Type'],
|
||||
['/', 'Page'],
|
||||
]),
|
||||
]);
|
||||
|
||||
$this->expectException(ImportCorruptedSourceException::class);
|
||||
$this->expectExceptionMessageMatches('/' . preg_quote('maximum node budget', '/') . '/');
|
||||
$resolver->countPages($doc, 1);
|
||||
}
|
||||
|
||||
/** @throws \Throwable */
|
||||
public function testCountPagesThrowsForUnexpectedNodeType(): void
|
||||
{
|
||||
$resolver = new PageResolver();
|
||||
$doc = $this->mockDoc([
|
||||
'1_0' => $this->dictObject([
|
||||
['/', 'Pages'],
|
||||
['objref', '2 0 R'],
|
||||
]),
|
||||
'2_0' => $this->dictObject([
|
||||
['/', 'Type'],
|
||||
['/', 'Catalog'],
|
||||
]),
|
||||
]);
|
||||
|
||||
$this->expectException(ImportCorruptedSourceException::class);
|
||||
$this->expectExceptionMessageMatches('/' . preg_quote('Unexpected page tree node type', '/') . '/');
|
||||
$resolver->countPages($doc);
|
||||
}
|
||||
|
||||
/** @throws \Throwable */
|
||||
public function testCountPagesThrowsForPagesNodeWithoutKids(): void
|
||||
{
|
||||
$resolver = new PageResolver();
|
||||
$doc = $this->mockDoc([
|
||||
'1_0' => $this->dictObject([
|
||||
['/', 'Pages'],
|
||||
['objref', '2 0 R'],
|
||||
]),
|
||||
'2_0' => $this->dictObject([
|
||||
['/', 'Type'],
|
||||
['/', 'Pages'],
|
||||
]),
|
||||
]);
|
||||
|
||||
$this->expectException(ImportCorruptedSourceException::class);
|
||||
$this->expectExceptionMessageMatches('/' . preg_quote('missing /Kids', '/') . '/');
|
||||
$resolver->countPages($doc);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// buildPageIndex / resolveFromIndex
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/** @throws \Throwable */
|
||||
public function testBuildPageIndexReturnsPagesInDocumentOrderWithInheritedAttributes(): void
|
||||
{
|
||||
$resolver = new PageResolver();
|
||||
$doc = $this->mockDoc([
|
||||
'1_0' => $this->dictObject([
|
||||
['/', 'Pages'],
|
||||
['objref', '2 0 R'],
|
||||
]),
|
||||
'2_0' => $this->dictObject([
|
||||
['/', 'Type'],
|
||||
['/', 'Pages'],
|
||||
['/', 'Kids'],
|
||||
[
|
||||
'[',
|
||||
[
|
||||
['objref', '3 0 R'],
|
||||
['objref', '6 0 R'],
|
||||
],
|
||||
],
|
||||
['/', 'MediaBox'],
|
||||
[
|
||||
'[',
|
||||
[
|
||||
['numeric', 0],
|
||||
['numeric', 0],
|
||||
['numeric', 300],
|
||||
['numeric', 500],
|
||||
],
|
||||
],
|
||||
]),
|
||||
'3_0' => $this->dictObject([
|
||||
['/', 'Type'],
|
||||
['/', 'Pages'],
|
||||
['/', 'Kids'],
|
||||
[
|
||||
'[',
|
||||
[
|
||||
['objref', '4 0 R'],
|
||||
['objref', '5 0 R'],
|
||||
],
|
||||
],
|
||||
]),
|
||||
'4_0' => $this->dictObject([
|
||||
['/', 'Type'],
|
||||
['/', 'Page'],
|
||||
['/', 'Rotate'],
|
||||
['numeric', 90],
|
||||
]),
|
||||
'5_0' => $this->dictObject([
|
||||
['/', 'Type'],
|
||||
['/', 'Page'],
|
||||
['/', 'Rotate'],
|
||||
['numeric', 180],
|
||||
]),
|
||||
'6_0' => $this->dictObject([
|
||||
['/', 'Type'],
|
||||
['/', 'Page'],
|
||||
['/', 'Rotate'],
|
||||
['numeric', 270],
|
||||
]),
|
||||
]);
|
||||
|
||||
$index = $resolver->buildPageIndex($doc);
|
||||
|
||||
$this->assertCount(3, $index);
|
||||
// Leaves of the nested /Pages node come before the root's second kid.
|
||||
$this->assertSame([90, 180, 270], \array_column($index, 'Rotate'));
|
||||
// The root /Pages MediaBox is inherited by every leaf.
|
||||
foreach ($index as $pageDict) {
|
||||
$this->assertSame([0, 0, 300, 500], $pageDict['MediaBox'] ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
/** @throws \Throwable */
|
||||
public function testResolveFromIndexThrowsForPageZero(): void
|
||||
{
|
||||
$resolver = new PageResolver();
|
||||
$doc = $this->loadDoc();
|
||||
$index = $resolver->buildPageIndex($doc);
|
||||
$this->expectException(ImportPageOutOfRangeException::class);
|
||||
$resolver->resolveFromIndex($doc, $index, 0);
|
||||
}
|
||||
|
||||
/** @throws \Throwable */
|
||||
public function testResolveFromIndexThrowsForPageBeyondIndex(): void
|
||||
{
|
||||
$resolver = new PageResolver();
|
||||
$doc = $this->loadDoc();
|
||||
$index = $resolver->buildPageIndex($doc);
|
||||
$this->expectException(ImportPageOutOfRangeException::class);
|
||||
$this->expectExceptionMessageMatches('/' . preg_quote('document has fewer pages', '/') . '/');
|
||||
$resolver->resolveFromIndex($doc, $index, 2);
|
||||
}
|
||||
|
||||
/** @throws \Throwable */
|
||||
public function testResolveFromIndexDoesNotWalkTheTreeAgain(): void
|
||||
{
|
||||
$resolver = new PageResolver();
|
||||
$calls = 0;
|
||||
$objects = [
|
||||
'1_0' => $this->dictObject([
|
||||
['/', 'Pages'],
|
||||
['objref', '2 0 R'],
|
||||
]),
|
||||
'2_0' => $this->dictObject([
|
||||
['/', 'Type'],
|
||||
['/', 'Pages'],
|
||||
['/', 'Kids'],
|
||||
[
|
||||
'[',
|
||||
[
|
||||
['objref', '3 0 R'],
|
||||
],
|
||||
],
|
||||
['/', 'MediaBox'],
|
||||
[
|
||||
'[',
|
||||
[
|
||||
['numeric', 0],
|
||||
['numeric', 0],
|
||||
['numeric', 200],
|
||||
['numeric', 200],
|
||||
],
|
||||
],
|
||||
]),
|
||||
'3_0' => $this->dictObject([
|
||||
['/', 'Type'],
|
||||
['/', 'Page'],
|
||||
]),
|
||||
];
|
||||
$doc = $this->createStub(SourceDocument::class);
|
||||
$doc->method('getTrailer')->willReturn(['root' => '1 0 R']);
|
||||
$doc->method('getObject')->willReturnCallback(static function (string $ref) use ($objects, &$calls): array {
|
||||
++$calls;
|
||||
return $objects[$ref] ?? [];
|
||||
});
|
||||
|
||||
$index = $resolver->buildPageIndex($doc);
|
||||
$callsAfterBuild = $calls;
|
||||
$this->assertGreaterThan(0, $callsAfterBuild);
|
||||
|
||||
$resolved = $resolver->resolveFromIndex($doc, $index, 1);
|
||||
|
||||
$this->assertSame([0.0, 0.0, 200.0, 200.0], $resolved['mediaBox']);
|
||||
$this->assertSame($callsAfterBuild, $calls);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user