generated from jric11/baseProject
983 lines
31 KiB
PHP
983 lines
31 KiB
PHP
<?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);
|
|
}
|
|
}
|