generated from jric11/baseProject
Initial commit
This commit is contained in:
@@ -0,0 +1,413 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* ParserFixesTest.php
|
||||
*
|
||||
* @since 2011-05-23
|
||||
* @category Library
|
||||
* @package Pdfparser
|
||||
* @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-parser
|
||||
*
|
||||
* This file is part of tc-lib-pdf-parser software library.
|
||||
*/
|
||||
|
||||
namespace Test;
|
||||
|
||||
use Com\Tecnick\Pdf\Parser\Parser;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Regression tests for parser hardening and stream-decoding fixes.
|
||||
*
|
||||
* @phpstan-import-type RawObjectArray from \Com\Tecnick\Pdf\Parser\Process\RawObject
|
||||
*/
|
||||
class ParserFixesTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* F1: the same parser instance must be reusable for multiple documents.
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testParserInstanceIsReusableAcrossDocuments(): void
|
||||
{
|
||||
$pdf = (string) \file_get_contents('resources/test/example_005.pdf');
|
||||
$parser = new Parser(['ignore_filter_errors' => true]);
|
||||
|
||||
$first = $parser->parse($pdf);
|
||||
$second = $parser->parse($pdf);
|
||||
|
||||
$this->assertSame(\md5(\serialize($first)), \md5(\serialize($second)));
|
||||
}
|
||||
|
||||
/**
|
||||
* F2: a truncated object body running to EOF must not emit PHP warnings.
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testTruncatedObjectBodyDoesNotEmitWarnings(): void
|
||||
{
|
||||
$header = "%PDF-1.4\n";
|
||||
$objBody = "1 0 obj\n(unterminated literal string with no closing parenthesis\n";
|
||||
$objOffset = \strlen($header);
|
||||
$document = $header . $objBody;
|
||||
$xrefOffset = \strlen($document);
|
||||
$document .=
|
||||
"xref\n0 2\n0000000000 65535 f \n"
|
||||
. $this->xrefInUseEntry($objOffset)
|
||||
. "trailer\n<< /Size 2 /Root 1 0 R >>\nstartxref\n"
|
||||
. $xrefOffset
|
||||
. "\n%%EOF";
|
||||
|
||||
$warnings = [];
|
||||
\set_error_handler(static function (int $_errno, string $errstr) use (&$warnings): bool {
|
||||
$warnings[] = $errstr;
|
||||
return true;
|
||||
});
|
||||
|
||||
try {
|
||||
$parser = new Parser(['ignore_filter_errors' => true]);
|
||||
$parser->parse($document);
|
||||
} finally {
|
||||
\restore_error_handler();
|
||||
}
|
||||
|
||||
$this->assertSame([], $warnings);
|
||||
}
|
||||
|
||||
/**
|
||||
* F3: a false "endstream" marker inside the payload must not truncate the stream
|
||||
* when a direct /Length declares the real length.
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testStreamIsNotTruncatedAtFalseEndstreamWithDirectLength(): void
|
||||
{
|
||||
$payload = "ABC endstream FAKE\nXYZ-real-tail";
|
||||
$this->assertSame(
|
||||
$payload,
|
||||
$this->extractStreamPayload($this->buildFalseEndstreamPdf((string) \strlen($payload), $payload)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* F3: the same protection must work when /Length is an indirect reference.
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testStreamIsNotTruncatedAtFalseEndstreamWithIndirectLength(): void
|
||||
{
|
||||
$payload = "ABC endstream FAKE\nXYZ-real-tail";
|
||||
$this->assertSame($payload, $this->extractStreamPayload($this->buildFalseEndstreamPdf('5 0 R', $payload)));
|
||||
}
|
||||
|
||||
/**
|
||||
* F4: a PNG predictor declared in DecodeParms of a regular FlateDecode stream
|
||||
* must be reversed (Colors/BitsPerComponent honoured).
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testPngPredictorIsReversedForRegularFlateStream(): void
|
||||
{
|
||||
$colors = 3;
|
||||
$columns = 2;
|
||||
$rawRows = [
|
||||
[10, 20, 30, 40, 50, 60],
|
||||
[11, 22, 33, 44, 55, 66],
|
||||
[200, 100, 50, 7, 8, 9],
|
||||
];
|
||||
|
||||
$rawFlat = '';
|
||||
foreach ($rawRows as $row) {
|
||||
$rawFlat .= \pack('C*', ...$row);
|
||||
}
|
||||
|
||||
$predicted = '';
|
||||
foreach ($rawRows as $row) {
|
||||
// PNG Sub filter (type 1): encoded = raw - left
|
||||
$predicted .= \chr(1);
|
||||
$count = \count($row);
|
||||
for ($i = 0; $i < $count; ++$i) {
|
||||
$left = $i >= $colors ? (int) ($row[$i - $colors] ?? 0) : 0;
|
||||
$predicted .= \chr(((int) ($row[$i] ?? 0) - $left) & 0xff);
|
||||
}
|
||||
}
|
||||
|
||||
$compressed = (string) \gzcompress($predicted);
|
||||
$dict =
|
||||
'<< /Length '
|
||||
. \strlen($compressed)
|
||||
. ' /Filter /FlateDecode'
|
||||
. ' /DecodeParms << /Predictor 12 /Colors '
|
||||
. $colors
|
||||
. ' /BitsPerComponent 8 /Columns '
|
||||
. $columns
|
||||
. ' >> >>';
|
||||
|
||||
$header = "%PDF-1.4\n";
|
||||
$obj1 = "1 0 obj\n" . $dict . "\nstream\n" . $compressed . "\nendstream\nendobj\n";
|
||||
$obj1Offset = \strlen($header);
|
||||
$document = $header . $obj1;
|
||||
$obj2Offset = \strlen($document);
|
||||
$document .= "2 0 obj\n<< /Type /Catalog >>\nendobj\n";
|
||||
$xrefOffset = \strlen($document);
|
||||
$document .=
|
||||
"xref\n0 3\n0000000000 65535 f \n"
|
||||
. $this->xrefInUseEntry($obj1Offset)
|
||||
. $this->xrefInUseEntry($obj2Offset)
|
||||
. "trailer\n<< /Size 3 /Root 2 0 R >>\nstartxref\n"
|
||||
. $xrefOffset
|
||||
. "\n%%EOF";
|
||||
|
||||
$parser = new Parser();
|
||||
[, $objects] = $parser->parse($document);
|
||||
|
||||
$decoded = null;
|
||||
foreach ($objects['1_0'] ?? [] as $element) {
|
||||
if ($element[0] !== 'stream' || !\array_key_exists(3, $element)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$decoded = $element[3][0];
|
||||
}
|
||||
|
||||
$this->assertSame($rawFlat, $decoded);
|
||||
}
|
||||
|
||||
/**
|
||||
* F4: a TIFF Predictor 2 declared in DecodeParms of a regular FlateDecode stream
|
||||
* must be reversed.
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testTiffPredictorIsReversedForRegularFlateStream(): void
|
||||
{
|
||||
$colors = 1;
|
||||
$columns = 4;
|
||||
$rawRows = [
|
||||
[5, 10, 3, 250],
|
||||
[1, 2, 3, 4],
|
||||
];
|
||||
|
||||
$rawFlat = '';
|
||||
foreach ($rawRows as $row) {
|
||||
$rawFlat .= \pack('C*', ...$row);
|
||||
}
|
||||
|
||||
$predicted = '';
|
||||
foreach ($rawRows as $row) {
|
||||
// TIFF horizontal differencing: encoded = sample - sample-to-the-left
|
||||
for ($i = 0; $i < $columns; ++$i) {
|
||||
$left = $i >= $colors ? (int) ($row[$i - $colors] ?? 0) : 0;
|
||||
$predicted .= \chr(((int) ($row[$i] ?? 0) - $left) & 0xff);
|
||||
}
|
||||
}
|
||||
|
||||
$compressed = (string) \gzcompress($predicted);
|
||||
$dict =
|
||||
'<< /Length '
|
||||
. \strlen($compressed)
|
||||
. ' /Filter /FlateDecode'
|
||||
. ' /DecodeParms << /Predictor 2 /Colors '
|
||||
. $colors
|
||||
. ' /BitsPerComponent 8 /Columns '
|
||||
. $columns
|
||||
. ' >> >>';
|
||||
|
||||
$header = "%PDF-1.4\n";
|
||||
$obj1 = "1 0 obj\n" . $dict . "\nstream\n" . $compressed . "\nendstream\nendobj\n";
|
||||
$obj1Offset = \strlen($header);
|
||||
$document = $header . $obj1;
|
||||
$obj2Offset = \strlen($document);
|
||||
$document .= "2 0 obj\n<< /Type /Catalog >>\nendobj\n";
|
||||
$xrefOffset = \strlen($document);
|
||||
$document .=
|
||||
"xref\n0 3\n0000000000 65535 f \n"
|
||||
. $this->xrefInUseEntry($obj1Offset)
|
||||
. $this->xrefInUseEntry($obj2Offset)
|
||||
. "trailer\n<< /Size 3 /Root 2 0 R >>\nstartxref\n"
|
||||
. $xrefOffset
|
||||
. "\n%%EOF";
|
||||
|
||||
$parser = new Parser();
|
||||
[, $objects] = $parser->parse($document);
|
||||
|
||||
$decoded = null;
|
||||
foreach ($objects['1_0'] ?? [] as $element) {
|
||||
if ($element[0] !== 'stream' || !\array_key_exists(3, $element)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$decoded = $element[3][0];
|
||||
}
|
||||
|
||||
$this->assertSame($rawFlat, $decoded);
|
||||
}
|
||||
|
||||
/**
|
||||
* F4: a cross-reference stream that uses a predictor must still be decoded
|
||||
* correctly (the predictor must not be applied twice).
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testCrossReferenceStreamWithPredictorIsNotDoubleDecoded(): void
|
||||
{
|
||||
$width = [1, 2, 1];
|
||||
$rowlen = \max(0, (int) \array_sum($width));
|
||||
|
||||
$header = "%PDF-1.4\n";
|
||||
$obj1Offset = \strlen($header);
|
||||
$document = $header . "1 0 obj\n<< /Type /Catalog >>\nendobj\n";
|
||||
$xrefObjOffset = \strlen($document);
|
||||
|
||||
$entries = [
|
||||
[0, 0, 0],
|
||||
[1, $obj1Offset, 0],
|
||||
[1, $xrefObjOffset, 0],
|
||||
];
|
||||
|
||||
$predicted = '';
|
||||
$prev = \array_fill(0, $rowlen, 0);
|
||||
foreach ($entries as $entry) {
|
||||
$row = $this->encodeXrefEntry($entry, $width);
|
||||
$predicted .= \chr(2); // PNG Up filter
|
||||
for ($i = 0; $i < $rowlen; ++$i) {
|
||||
$predicted .= \chr(((int) ($row[$i] ?? 0) - (int) ($prev[$i] ?? 0)) & 0xff);
|
||||
}
|
||||
|
||||
$prev = $row;
|
||||
}
|
||||
|
||||
$compressed = (string) \gzcompress($predicted);
|
||||
$dict =
|
||||
'<< /Type /XRef /Size 3 /Root 1 0 R /W [1 2 1] /Filter /FlateDecode'
|
||||
. ' /DecodeParms << /Predictor 12 /Columns '
|
||||
. $rowlen
|
||||
. ' >> /Length '
|
||||
. \strlen($compressed)
|
||||
. ' >>';
|
||||
$document .= "2 0 obj\n" . $dict . "\nstream\n" . $compressed . "\nendstream\nendobj\n";
|
||||
$document .= "startxref\n" . $xrefObjOffset . "\n%%EOF";
|
||||
|
||||
$parser = new Parser();
|
||||
[$xref, $objects] = $parser->parse($document);
|
||||
|
||||
$this->assertSame($obj1Offset, $xref['xref']['1_0'] ?? null);
|
||||
$this->assertSame($xrefObjOffset, $xref['xref']['2_0'] ?? null);
|
||||
$this->assertSame('1_0', $xref['trailer']['root']);
|
||||
$this->assertSame('<<', $objects['1_0'][0][0] ?? null);
|
||||
}
|
||||
|
||||
/**
|
||||
* F7: a trailer dictionary that contains a nested dictionary must be parsed
|
||||
* without being truncated at the first ">>".
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testTrailerWithNestedDictionaryIsParsed(): void
|
||||
{
|
||||
$header = "%PDF-1.4\n";
|
||||
$objOffset = \strlen($header);
|
||||
$document = $header . "1 0 obj\n<< /Type /Catalog >>\nendobj\n";
|
||||
$xrefOffset = \strlen($document);
|
||||
$document .=
|
||||
"xref\n0 2\n0000000000 65535 f \n"
|
||||
. $this->xrefInUseEntry($objOffset)
|
||||
. "trailer\n<< /Custom << /Nested 1 >> /Size 2 /Root 1 0 R >>\nstartxref\n"
|
||||
. $xrefOffset
|
||||
. "\n%%EOF";
|
||||
|
||||
$parser = new Parser();
|
||||
[$xref] = $parser->parse($document);
|
||||
|
||||
$this->assertSame('1_0', $xref['trailer']['root']);
|
||||
$this->assertSame(2, $xref['trailer']['size']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a PDF whose object 1 stream contains a false "endstream" marker.
|
||||
*
|
||||
* @param string $lengthEntry The /Length entry value (a number or an "N 0 R" reference).
|
||||
* @param string $payload The real stream payload.
|
||||
*/
|
||||
private function buildFalseEndstreamPdf(string $lengthEntry, string $payload): string
|
||||
{
|
||||
$length = \strlen($payload);
|
||||
$header = "%PDF-1.4\n";
|
||||
|
||||
$obj1 = "1 0 obj\n<< /Length " . $lengthEntry . " >>\nstream\n" . $payload . "\nendstream\nendobj\n";
|
||||
$obj1Offset = \strlen($header);
|
||||
$document = $header . $obj1;
|
||||
|
||||
$obj2Offset = \strlen($document);
|
||||
$document .= "2 0 obj\n<< /Type /Catalog >>\nendobj\n";
|
||||
|
||||
$obj5Offset = \strlen($document);
|
||||
$document .= "5 0 obj\n" . $length . "\nendobj\n";
|
||||
|
||||
$xrefOffset = \strlen($document);
|
||||
$document .=
|
||||
"xref\n0 6\n0000000000 65535 f \n"
|
||||
. $this->xrefInUseEntry($obj1Offset)
|
||||
. $this->xrefInUseEntry($obj2Offset)
|
||||
. "0000000000 00000 f \n"
|
||||
. "0000000000 00000 f \n"
|
||||
. $this->xrefInUseEntry($obj5Offset)
|
||||
. "trailer\n<< /Size 6 /Root 2 0 R >>\nstartxref\n"
|
||||
. $xrefOffset
|
||||
. "\n%%EOF";
|
||||
|
||||
return $document;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a PDF and return the raw payload of the stream in object "1_0".
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
private function extractStreamPayload(string $document): string
|
||||
{
|
||||
$parser = new Parser(['decode_streams' => false]);
|
||||
[, $objects] = $parser->parse($document);
|
||||
|
||||
foreach ($objects['1_0'] ?? [] as $element) {
|
||||
if ($element[0] === 'stream' && \is_string($element[1])) {
|
||||
return $element[1];
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a single xref-stream entry into its byte values using the given field widths.
|
||||
*
|
||||
* @param array{0: int, 1: int, 2: int} $entry Entry values (type, field2, field3).
|
||||
* @param array{0: int, 1: int, 2: int} $width Field widths in bytes.
|
||||
*
|
||||
* @return array<int, int> Byte values for the encoded row.
|
||||
*/
|
||||
private function encodeXrefEntry(array $entry, array $width): array
|
||||
{
|
||||
$bytes = [];
|
||||
foreach ([0, 1, 2] as $field) {
|
||||
$value = $entry[$field] ?? 0;
|
||||
$fieldWidth = $width[$field] ?? 0;
|
||||
for ($byte = $fieldWidth - 1; $byte >= 0; --$byte) {
|
||||
$bytes[] = ($value >> ($byte * 8)) & 0xff;
|
||||
}
|
||||
}
|
||||
|
||||
return $bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a classic in-use xref entry line for the given object offset.
|
||||
*/
|
||||
private function xrefInUseEntry(int $offset): string
|
||||
{
|
||||
return \sprintf("%010d 00000 n \n", $offset);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* ParserHarness.php
|
||||
*
|
||||
* @since 2011-05-23
|
||||
* @category Library
|
||||
* @package Pdfparser
|
||||
* @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-parser
|
||||
*
|
||||
* This file is part of tc-lib-pdf-parser software library.
|
||||
*/
|
||||
|
||||
namespace Test;
|
||||
|
||||
use Com\Tecnick\Pdf\Parser\Parser;
|
||||
|
||||
/**
|
||||
* @phpstan-import-type RawObjectArray from \Com\Tecnick\Pdf\Parser\Process\RawObject
|
||||
*/
|
||||
class ParserHarness extends Parser
|
||||
{
|
||||
/**
|
||||
* @var array{
|
||||
* 'trailer': array{
|
||||
* 'encrypt'?: string,
|
||||
* 'id': array<int, string>,
|
||||
* 'info': string,
|
||||
* 'root': string,
|
||||
* 'size': int,
|
||||
* },
|
||||
* 'xref': array<string, int|string>,
|
||||
* }
|
||||
*/
|
||||
private array $stubXrefData = [
|
||||
'trailer' => [
|
||||
'id' => [],
|
||||
'info' => '',
|
||||
'root' => '',
|
||||
'size' => 0,
|
||||
],
|
||||
'xref' => [],
|
||||
];
|
||||
|
||||
/** @var array<int, array{0:string,1:int,2:bool}> */
|
||||
private array $indirectCalls = [];
|
||||
|
||||
/** @var array<int, RawObjectArray> */
|
||||
private array $stubIndirectReturn = [['null', 'null', 0]];
|
||||
|
||||
/** @var array<int, RawObjectArray> */
|
||||
private array $rawObjectQueue = [];
|
||||
|
||||
private bool $useParentIndirect = false;
|
||||
|
||||
private bool $useParentRawObject = false;
|
||||
|
||||
/**
|
||||
* @param array{
|
||||
* 'trailer': array{
|
||||
* 'encrypt'?: string,
|
||||
* 'id': array<int, string>,
|
||||
* 'info': string,
|
||||
* 'root': string,
|
||||
* 'size': int,
|
||||
* },
|
||||
* 'xref': array<string, int|string>,
|
||||
* } $xref
|
||||
*/
|
||||
public function setStubXrefData(array $xref): void
|
||||
{
|
||||
$this->stubXrefData = $xref;
|
||||
}
|
||||
|
||||
/** @return array<int, array{0:string,1:int,2:bool}> */
|
||||
public function getIndirectCalls(): array
|
||||
{
|
||||
return $this->indirectCalls;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, RawObjectArray> $obj
|
||||
*/
|
||||
public function setStubIndirectReturn(array $obj): void
|
||||
{
|
||||
$this->stubIndirectReturn = $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, RawObjectArray> $queue
|
||||
*/
|
||||
public function setRawObjectQueue(array $queue): void
|
||||
{
|
||||
$this->rawObjectQueue = $queue;
|
||||
}
|
||||
|
||||
public function setPdfDataPublic(string $data): void
|
||||
{
|
||||
$this->pdfdata = $data;
|
||||
}
|
||||
|
||||
public function getPdfDataPublic(): string
|
||||
{
|
||||
return $this->pdfdata;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array<int, RawObjectArray>> $objects
|
||||
*/
|
||||
public function setObjectsPublic(array $objects): void
|
||||
{
|
||||
$this->objects = $objects;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, int|string> $xref
|
||||
*/
|
||||
public function setXrefMapPublic(array $xref): void
|
||||
{
|
||||
$this->xref = [
|
||||
'trailer' => [
|
||||
'id' => [],
|
||||
'info' => '',
|
||||
'root' => '',
|
||||
'size' => 0,
|
||||
],
|
||||
'xref' => $xref,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param RawObjectArray $obj
|
||||
*
|
||||
* @return RawObjectArray
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function getObjectValPublic(array $obj): array
|
||||
{
|
||||
return $this->getObjectVal($obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string> $filters
|
||||
* @param array<int, RawObjectArray> $sdic
|
||||
*
|
||||
* @return array<string>
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function getFiltersPublic(array $filters, array $sdic, int $key): array
|
||||
{
|
||||
return $this->getFilters($filters, $sdic, $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, RawObjectArray> $sdic
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function getDecodeParmsPublic(array $sdic, int $key): array
|
||||
{
|
||||
return $this->getDecodeParms($sdic, $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string> $filters
|
||||
* @param array<string, mixed> $params
|
||||
*
|
||||
* @return array{0:string,1:array<string>}
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function getDecodedStreamPublic(array $filters, string $stream, array $params = []): array
|
||||
{
|
||||
return $this->getDecodedStream($filters, $stream, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, RawObjectArray> $sdic
|
||||
*
|
||||
* @return array{0:string,1:array<string>}
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function decodeStreamPublic(array $sdic, string $stream): array
|
||||
{
|
||||
return $this->decodeStream($sdic, $stream);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, RawObjectArray>
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function getRawIndirectObjectPublic(int $offset, bool $decoding): array
|
||||
{
|
||||
return $this->getRawIndirectObject($offset, $decoding);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, RawObjectArray>
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function callParentGetIndirectObject(string $obj_ref, int $offset = 0, bool $decoding = true): array
|
||||
{
|
||||
$this->useParentIndirect = true;
|
||||
try {
|
||||
return $this->getIndirectObject($obj_ref, $offset, $decoding);
|
||||
} finally {
|
||||
$this->useParentIndirect = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test-only: bypass the queue override below and run the real inherited
|
||||
* getRawObject against `$this->pdfdata`. Lets tests exercise the real
|
||||
* processAngular / processBracket loops with arbitrary byte input.
|
||||
*
|
||||
* @return RawObjectArray
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function callParentGetRawObject(int $offset = 0): array
|
||||
{
|
||||
$this->useParentRawObject = true;
|
||||
try {
|
||||
return $this->getRawObject($offset);
|
||||
} finally {
|
||||
$this->useParentRawObject = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{
|
||||
* 'trailer'?: array{
|
||||
* 'encrypt'?: string,
|
||||
* 'id': array<int, string>,
|
||||
* 'info': string,
|
||||
* 'root': string,
|
||||
* 'size': int,
|
||||
* },
|
||||
* 'xref'?: array<string, int|string>,
|
||||
* } $xref
|
||||
*
|
||||
* @return array{
|
||||
* 'trailer': array{
|
||||
* 'encrypt'?: string,
|
||||
* 'id': array<int, string>,
|
||||
* 'info': string,
|
||||
* 'root': string,
|
||||
* 'size': int,
|
||||
* },
|
||||
* 'xref': array<string, int|string>,
|
||||
* }
|
||||
*/
|
||||
protected function getXrefData(int $offset = 0, array $xref = []): array
|
||||
{
|
||||
$unused = [$offset, $xref];
|
||||
unset($unused);
|
||||
|
||||
return $this->stubXrefData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, RawObjectArray>
|
||||
*/
|
||||
protected function getIndirectObject(string $obj_ref, int $offset = 0, bool $decoding = true): array
|
||||
{
|
||||
if ($this->useParentIndirect) {
|
||||
return parent::getIndirectObject($obj_ref, $offset, $decoding);
|
||||
}
|
||||
|
||||
$this->indirectCalls[] = [$obj_ref, $offset, $decoding];
|
||||
|
||||
return $this->stubIndirectReturn;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return RawObjectArray
|
||||
*/
|
||||
protected function getRawObject(int $offset = 0): array
|
||||
{
|
||||
if ($this->useParentRawObject) {
|
||||
return parent::getRawObject($offset);
|
||||
}
|
||||
|
||||
if (empty($this->rawObjectQueue)) {
|
||||
return ['endobj', 'endobj', $offset];
|
||||
}
|
||||
|
||||
return \array_shift($this->rawObjectQueue);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,920 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* ParserProcessingTest.php
|
||||
*
|
||||
* @since 2011-05-23
|
||||
* @category Library
|
||||
* @package Pdfparser
|
||||
* @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-parser
|
||||
*
|
||||
* This file is part of tc-lib-pdf-parser software library.
|
||||
*/
|
||||
|
||||
namespace Test;
|
||||
|
||||
use Com\Tecnick\Pdf\Parser\Exception as PPException;
|
||||
|
||||
/**
|
||||
* @phpstan-import-type RawObjectArray from \Com\Tecnick\Pdf\Parser\Process\RawObject
|
||||
*/
|
||||
class ParserProcessingTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testParseRejectsEmptyAndInvalidData(): void
|
||||
{
|
||||
$parser = new ParserHarness();
|
||||
|
||||
$this->expectException(PPException::class);
|
||||
$this->expectExceptionMessageContains('Empty PDF data.');
|
||||
$parser->parse('');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testParseRejectsDataWithoutPdfHeader(): void
|
||||
{
|
||||
$parser = new ParserHarness();
|
||||
|
||||
$this->expectException(PPException::class);
|
||||
$this->expectExceptionMessageContains('Invalid PDF data: missing %PDF header.');
|
||||
$parser->parse('not a pdf');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testParseLoadsPositiveOffsetsOnlyAndClearsPdfData(): void
|
||||
{
|
||||
$parser = new ParserHarness();
|
||||
$parser->setStubXrefData([
|
||||
'trailer' => [
|
||||
'id' => [],
|
||||
'info' => '',
|
||||
'root' => '1_0',
|
||||
'size' => 3,
|
||||
],
|
||||
'xref' => [
|
||||
'1_0' => 10,
|
||||
'2_0' => 0,
|
||||
'3_0' => -1,
|
||||
],
|
||||
]);
|
||||
$parser->setStubIndirectReturn([['numeric', '42', 0]]);
|
||||
|
||||
$parsed = $parser->parse("junk%PDF-1.7\n");
|
||||
|
||||
$calls = $parser->getIndirectCalls();
|
||||
$firstCall = $calls[0] ?? null;
|
||||
$this->assertCount(1, $calls);
|
||||
$this->assertSame(['1_0', 10, true], $firstCall);
|
||||
$this->assertSame('', $parser->getPdfDataPublic());
|
||||
$this->assertArrayHasKey('1_0', $parsed[1]);
|
||||
$this->assertArrayNotHasKey('2_0', $parsed[1]);
|
||||
$this->assertArrayNotHasKey('3_0', $parsed[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testParseUsesDecodeStreamsConfigForDirectObjects(): void
|
||||
{
|
||||
$parser = new ParserHarness();
|
||||
$parser->setStubXrefData([
|
||||
'trailer' => [
|
||||
'id' => [],
|
||||
'info' => '',
|
||||
'root' => '1_0',
|
||||
'size' => 2,
|
||||
],
|
||||
'xref' => [
|
||||
'1_0' => 10,
|
||||
],
|
||||
]);
|
||||
$parser->setStubIndirectReturn([['numeric', '42', 0]]);
|
||||
|
||||
$parser->parse("%PDF-1.7\n");
|
||||
$calls = $parser->getIndirectCalls();
|
||||
$this->assertSame(['1_0', 10, true], $calls[0] ?? null);
|
||||
|
||||
$lazy = new ParserHarness(['decode_streams' => false]);
|
||||
$lazy->setStubXrefData([
|
||||
'trailer' => [
|
||||
'id' => [],
|
||||
'info' => '',
|
||||
'root' => '1_0',
|
||||
'size' => 2,
|
||||
],
|
||||
'xref' => [
|
||||
'1_0' => 10,
|
||||
],
|
||||
]);
|
||||
$lazy->setStubIndirectReturn([['numeric', '42', 0]]);
|
||||
|
||||
$lazy->parse("%PDF-1.7\n");
|
||||
$lazyCalls = $lazy->getIndirectCalls();
|
||||
$this->assertSame(['1_0', 10, false], $lazyCalls[0] ?? null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testParentIndirectObjectRejectsInvalidReference(): void
|
||||
{
|
||||
$parser = new ParserHarness();
|
||||
$parser->setPdfDataPublic("%PDF-1.7\n1 0 obj\nendobj\n");
|
||||
|
||||
$this->expectException(PPException::class);
|
||||
$this->expectExceptionMessageContains('Invalid object reference:');
|
||||
$parser->callParentGetIndirectObject('invalid', 0, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testParentIndirectObjectReturnsEmptyResultWhenTargetIsMissing(): void
|
||||
{
|
||||
$parser = new ParserHarness();
|
||||
$parser->setPdfDataPublic("%PDF-1.7\n");
|
||||
|
||||
$obj = $parser->callParentGetIndirectObject('1_0', 0, true);
|
||||
|
||||
$this->assertSame([], $obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testRawIndirectObjectDecodesStreamWhenDictionaryIsPresent(): void
|
||||
{
|
||||
$parser = new ParserHarness();
|
||||
$parser->setRawObjectQueue([
|
||||
['<<', [['/', 'Length', 0], ['numeric', '3', 0]], 5],
|
||||
['stream', 'abcdef', 12],
|
||||
['endobj', 'endobj', 18],
|
||||
]);
|
||||
|
||||
$objdata = $parser->getRawIndirectObjectPublic(0, true);
|
||||
|
||||
$this->assertCount(2, $objdata);
|
||||
$entry = $objdata[1] ?? null;
|
||||
if (!\is_array($entry)) {
|
||||
$this->fail('Missing decoded stream entry at index 1.');
|
||||
}
|
||||
|
||||
$this->assertSame('stream', $entry[0]);
|
||||
$this->assertSame('abcdef', $entry[1]);
|
||||
|
||||
$decoded = $entry[3] ?? null;
|
||||
if (!\is_array($decoded)) {
|
||||
$this->fail('Decoded stream payload not available.');
|
||||
}
|
||||
|
||||
$this->assertSame('abc', $decoded[0]);
|
||||
$this->assertSame([], $decoded[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testGetFiltersParsesSingleAndArraySyntax(): void
|
||||
{
|
||||
$parser = new ParserHarness();
|
||||
|
||||
$single = [
|
||||
['/', 'Filter', 0],
|
||||
['/', 'FlateDecode', 0],
|
||||
];
|
||||
$filters = $parser->getFiltersPublic([], $single, 0);
|
||||
$this->assertSame(['FlateDecode'], $filters);
|
||||
|
||||
$list = [
|
||||
['/', 'Filter', 0],
|
||||
[
|
||||
'[',
|
||||
[
|
||||
['/', 'FlateDecode', 0],
|
||||
['numeric', '1', 0],
|
||||
['/', 'ASCIIHexDecode', 0],
|
||||
],
|
||||
0,
|
||||
],
|
||||
];
|
||||
$filters = $parser->getFiltersPublic([], $list, 0);
|
||||
$this->assertSame(['FlateDecode', 'ASCIIHexDecode'], $filters);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testGetObjectValResolvesCachedAndMappedObjectReferences(): void
|
||||
{
|
||||
$parser = new ParserHarness();
|
||||
$parser->setObjectsPublic([
|
||||
'3_0' => [['string', 'cached', 0]],
|
||||
]);
|
||||
$cached = $parser->getObjectValPublic(['objref', '3_0', 0]);
|
||||
$this->assertSame(['string', 'cached', 0], $cached);
|
||||
|
||||
$parser = new ParserHarness();
|
||||
$parser->setXrefMapPublic(['4_0' => 99]);
|
||||
$parser->setStubIndirectReturn([['string', 'loaded', 0]]);
|
||||
$loaded = $parser->getObjectValPublic(['objref', '4_0', 0]);
|
||||
$this->assertSame(['string', 'loaded', 0], $loaded);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testGetDecodedStreamTracksErrorsWhenConfiguredToIgnore(): void
|
||||
{
|
||||
$parser = new ParserHarness(['ignore_filter_errors' => true]);
|
||||
|
||||
$result = $parser->getDecodedStreamPublic(['UnknownFilter'], 'sample-data');
|
||||
|
||||
$this->assertSame('sample-data', $result[0]);
|
||||
$this->assertSame(['UnknownFilter'], $result[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testGetDecodedStreamThrowsWhenFilterErrorsAreNotIgnored(): void
|
||||
{
|
||||
$parser = new ParserHarness();
|
||||
|
||||
$this->expectException(PPException::class);
|
||||
$parser->getDecodedStreamPublic(['UnknownFilter'], 'sample-data');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testParentIndirectObjectFindsObjectAfterOffsetShift(): void
|
||||
{
|
||||
$parser = new ParserHarness();
|
||||
$parser->setPdfDataPublic("%PDF-1.7\nX1 0 obj\nendobj\n");
|
||||
$parser->setRawObjectQueue([
|
||||
['numeric', '7', 16],
|
||||
['endobj', 'endobj', 22],
|
||||
]);
|
||||
|
||||
$obj = $parser->callParentGetIndirectObject('1_0', 9, true);
|
||||
|
||||
$this->assertSame([['numeric', '7', 16]], $obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testGetFiltersHandlesMissingAndInvalidArrayPayload(): void
|
||||
{
|
||||
$parser = new ParserHarness();
|
||||
|
||||
$filters = ['FlateDecode'];
|
||||
$this->assertSame($filters, $parser->getFiltersPublic($filters, [['/', 'Filter', 0]], 0));
|
||||
|
||||
$invalid = [
|
||||
['/', 'Filter', 0],
|
||||
['[', 'invalid', 0],
|
||||
];
|
||||
$this->assertSame([], $parser->getFiltersPublic([], $invalid, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testGetDecodeParmsHandlesDictionaryArrayAndMissingValues(): void
|
||||
{
|
||||
$parser = new ParserHarness();
|
||||
|
||||
$this->assertSame([], $parser->getDecodeParmsPublic([['/', 'DecodeParms', 0]], 0));
|
||||
|
||||
$dict = [
|
||||
['/', 'DecodeParms', 0],
|
||||
[
|
||||
'<<',
|
||||
[
|
||||
['/', 'Columns', 0],
|
||||
['numeric', '5', 0],
|
||||
['/', 'EarlyChange', 0],
|
||||
['true', 'true', 0],
|
||||
['/', 'FilterName', 0],
|
||||
['/', 'FlateDecode', 0],
|
||||
['/', 'Text', 0],
|
||||
['string', 'abc', 0],
|
||||
['/', 'Ignored', 0],
|
||||
['[', [], 0],
|
||||
],
|
||||
0,
|
||||
],
|
||||
];
|
||||
|
||||
$this->assertSame(
|
||||
[
|
||||
'Columns' => 5,
|
||||
'EarlyChange' => true,
|
||||
'FilterName' => 'FlateDecode',
|
||||
'Text' => 'abc',
|
||||
],
|
||||
$parser->getDecodeParmsPublic($dict, 0),
|
||||
);
|
||||
|
||||
$array = [
|
||||
['/', 'DecodeParms', 0],
|
||||
[
|
||||
'[',
|
||||
[
|
||||
['null', 'null', 0],
|
||||
[
|
||||
'<<',
|
||||
[
|
||||
['/', 'Rows', 0],
|
||||
['numeric', '2', 0],
|
||||
['/', 'Enabled', 0],
|
||||
['false', 'false', 0],
|
||||
],
|
||||
0,
|
||||
],
|
||||
],
|
||||
0,
|
||||
],
|
||||
];
|
||||
|
||||
$this->assertSame(
|
||||
[
|
||||
'Rows' => 2,
|
||||
'Enabled' => false,
|
||||
],
|
||||
$parser->getDecodeParmsPublic($array, 0),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testDecodeStreamHandlesEmptyStreamAndDecodeParmsExtraction(): void
|
||||
{
|
||||
$parser = new ParserHarness(['ignore_filter_errors' => true]);
|
||||
|
||||
$this->assertSame(['', []], $parser->decodeStreamPublic([], ''));
|
||||
|
||||
$sdic = [
|
||||
['/', 'Filter', 0],
|
||||
['/', 'UnknownFilter', 0],
|
||||
['/', 'DecodeParms', 0],
|
||||
[
|
||||
'<<',
|
||||
[
|
||||
['/', 'Columns', 0],
|
||||
['numeric', '3', 0],
|
||||
['/', 'Predictor', 0],
|
||||
['numeric', '12', 0],
|
||||
],
|
||||
0,
|
||||
],
|
||||
];
|
||||
|
||||
$result = $parser->decodeStreamPublic($sdic, 'abc');
|
||||
$this->assertSame('abc', $result[0]);
|
||||
$this->assertSame(['UnknownFilter'], $result[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testParentIndirectObjectReturnsNullObjectWhenSearchMissesTwice(): void
|
||||
{
|
||||
$parser = new ParserHarness();
|
||||
$parser->setPdfDataPublic("%PDF-1.7\nno objects\n");
|
||||
|
||||
$obj = $parser->callParentGetIndirectObject('1_0', 2, true);
|
||||
|
||||
$this->assertSame([['null', 'null', 3]], $obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testGetFiltersSkipsSlashEntriesWithNonStringNames(): void
|
||||
{
|
||||
$parser = new ParserHarness();
|
||||
$sdic = [
|
||||
['/', 'Filter', 0],
|
||||
[
|
||||
'[',
|
||||
[
|
||||
['/', [['numeric', '1', 0]], 0],
|
||||
['/', 'FlateDecode', 0],
|
||||
],
|
||||
0,
|
||||
],
|
||||
];
|
||||
|
||||
$filters = $parser->getFiltersPublic([], $sdic, 0);
|
||||
$this->assertSame(['FlateDecode'], $filters);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testGetDecodeParmsSkipsInvalidPairsAcrossLoopChecks(): void
|
||||
{
|
||||
$parser = new ParserHarness();
|
||||
$sdic = [
|
||||
['/', 'DecodeParms', 0],
|
||||
[
|
||||
'<<',
|
||||
[
|
||||
['numeric', '0', 0],
|
||||
['numeric', '1', 0],
|
||||
['/', 'MissingValue', 0],
|
||||
['null', 'null', 0],
|
||||
['/', 'Valid', 0],
|
||||
['numeric', '7', 0],
|
||||
['/', 'DanglingKey', 0],
|
||||
],
|
||||
0,
|
||||
],
|
||||
];
|
||||
|
||||
$params = $parser->getDecodeParmsPublic($sdic, 0);
|
||||
$this->assertSame(['Valid' => 7], $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testParseResolvesCompressedObjectFromObjectStream(): void
|
||||
{
|
||||
$parser = new ParserHarness();
|
||||
$parser->setStubXrefData([
|
||||
'trailer' => [
|
||||
'id' => [],
|
||||
'info' => '',
|
||||
'root' => '1_0',
|
||||
'size' => 3,
|
||||
],
|
||||
'xref' => [
|
||||
'1_0' => 10,
|
||||
'2_0' => '1_0_0',
|
||||
],
|
||||
]);
|
||||
$parser->setStubIndirectReturn($this->buildObjectStreamObject(1, 4, '2 0 (A)'));
|
||||
|
||||
$parsed = $parser->parse("%PDF-1.7\n");
|
||||
|
||||
$this->assertArrayHasKey('2_0', $parsed[1]);
|
||||
$obj = $parsed[1]['2_0'] ?? null;
|
||||
$this->assertIsArray($obj);
|
||||
$this->assertNotEmpty($obj);
|
||||
$this->assertCount(1, $parser->getIndirectCalls());
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testParseWithLazyStreamsReparsesObjectStreamForDecodedPayload(): void
|
||||
{
|
||||
$parser = new class(['decode_streams' => false]) extends ParserHarness {
|
||||
/** @var array<int, array{0:string,1:int,2:bool}> */
|
||||
private array $calls = [];
|
||||
|
||||
/**
|
||||
* @return array<int, RawObjectArray>
|
||||
*/
|
||||
protected function getIndirectObject(string $obj_ref, int $offset = 0, bool $decoding = true): array
|
||||
{
|
||||
$this->calls[] = [$obj_ref, $offset, $decoding];
|
||||
|
||||
if ($obj_ref !== '1_0') {
|
||||
return [['null', 'null', 0]];
|
||||
}
|
||||
|
||||
if ($decoding) {
|
||||
return [
|
||||
[
|
||||
'<<',
|
||||
[
|
||||
['/', 'Type', 0],
|
||||
['/', 'ObjStm', 0],
|
||||
['/', 'N', 0],
|
||||
['numeric', '1', 0],
|
||||
['/', 'First', 0],
|
||||
['numeric', '4', 0],
|
||||
],
|
||||
0,
|
||||
],
|
||||
['stream', 'raw', 0, ['2 0 (A)', []]],
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
[
|
||||
'<<',
|
||||
[
|
||||
['/', 'Type', 0],
|
||||
['/', 'ObjStm', 0],
|
||||
['/', 'N', 0],
|
||||
['numeric', '1', 0],
|
||||
['/', 'First', 0],
|
||||
['numeric', '4', 0],
|
||||
],
|
||||
0,
|
||||
],
|
||||
['stream', 'raw', 0],
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<int, array{0:string,1:int,2:bool}> */
|
||||
public function getCalls(): array
|
||||
{
|
||||
return $this->calls;
|
||||
}
|
||||
};
|
||||
|
||||
$parser->setStubXrefData([
|
||||
'trailer' => [
|
||||
'id' => [],
|
||||
'info' => '',
|
||||
'root' => '1_0',
|
||||
'size' => 3,
|
||||
],
|
||||
'xref' => [
|
||||
'1_0' => 10,
|
||||
'2_0' => '1_0_0',
|
||||
],
|
||||
]);
|
||||
|
||||
$parsed = $parser->parse("%PDF-1.7\n");
|
||||
|
||||
$this->assertArrayHasKey('2_0', $parsed[1]);
|
||||
$calls = $parser->getCalls();
|
||||
$this->assertSame(['1_0', 10, false], $calls[0] ?? null);
|
||||
$this->assertSame(['1_0', 10, true], $calls[1] ?? null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testParseSkipsObjectAlreadyInjectedDuringIteration(): void
|
||||
{
|
||||
$parser = new class() extends ParserHarness {
|
||||
protected function getIndirectObject(string $obj_ref, int $offset = 0, bool $decoding = true): array
|
||||
{
|
||||
$obj = parent::getIndirectObject($obj_ref, $offset, $decoding);
|
||||
if ($obj_ref === '1_0') {
|
||||
$this->objects['2_0'] = [['string', 'prefilled', 0]];
|
||||
}
|
||||
|
||||
return $obj;
|
||||
}
|
||||
};
|
||||
|
||||
$parser->setStubXrefData([
|
||||
'trailer' => [
|
||||
'id' => [],
|
||||
'info' => '',
|
||||
'root' => '1_0',
|
||||
'size' => 3,
|
||||
],
|
||||
'xref' => [
|
||||
'1_0' => 10,
|
||||
'2_0' => 20,
|
||||
],
|
||||
]);
|
||||
$parser->setStubIndirectReturn([['numeric', '9', 0]]);
|
||||
|
||||
$parsed = $parser->parse("%PDF-1.7\n");
|
||||
|
||||
$calls = $parser->getIndirectCalls();
|
||||
$this->assertCount(1, $calls);
|
||||
$first = $calls[0] ?? null;
|
||||
$this->assertIsArray($first);
|
||||
$this->assertSame('1_0', $first[0]);
|
||||
$this->assertSame('prefilled', $parsed[1]['2_0'][0][1] ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testParseSkipsCompressedObjectWhenStreamEnvelopeIsInvalid(): void
|
||||
{
|
||||
$parser = new ParserHarness();
|
||||
$parser->setStubXrefData([
|
||||
'trailer' => [
|
||||
'id' => [],
|
||||
'info' => '',
|
||||
'root' => '1_0',
|
||||
'size' => 3,
|
||||
],
|
||||
'xref' => [
|
||||
'1_0' => 10,
|
||||
'2_0' => '1_0_0',
|
||||
],
|
||||
]);
|
||||
$parser->setStubIndirectReturn([
|
||||
['<<', [['/', 'N', 0], ['numeric', '1', 0], ['/', 'First', 0], ['numeric', '4', 0]], 0],
|
||||
['stream', 'raw', 0],
|
||||
]);
|
||||
|
||||
$parsed = $parser->parse("%PDF-1.7\n");
|
||||
|
||||
$this->assertArrayNotHasKey('2_0', $parsed[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testGetObjectValReturnsOriginalWhenCompressedLookupFails(): void
|
||||
{
|
||||
$parser = new ParserHarness();
|
||||
$parser->setXrefMapPublic([
|
||||
'2_0' => '1_0_0',
|
||||
'1_0' => 0,
|
||||
]);
|
||||
|
||||
$obj = ['objref', '2_0', 0];
|
||||
$result = $parser->getObjectValPublic($obj);
|
||||
|
||||
$this->assertSame($obj, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testGetObjectValReturnsOriginalForNonPositiveOffsetAndInvalidLocator(): void
|
||||
{
|
||||
$parser = new ParserHarness();
|
||||
$parser->setXrefMapPublic(['2_0' => 0]);
|
||||
$obj = ['objref', '2_0', 0];
|
||||
$this->assertSame($obj, $parser->getObjectValPublic($obj));
|
||||
|
||||
$parser = new ParserHarness();
|
||||
$parser->setXrefMapPublic(['2_0' => 'bad_locator']);
|
||||
$this->assertSame($obj, $parser->getObjectValPublic($obj));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testGetObjectValReturnsOriginalWhenCachedObjectStreamNeedsMissingReparseOffset(): void
|
||||
{
|
||||
$parser = new ParserHarness();
|
||||
$parser->setObjectsPublic([
|
||||
'1_0' => [
|
||||
[
|
||||
'<<',
|
||||
[
|
||||
['/', 'Type', 0],
|
||||
['/', 'ObjStm', 0],
|
||||
['/', 'N', 0],
|
||||
['numeric', '1', 0],
|
||||
['/', 'First', 0],
|
||||
['numeric', '4', 0],
|
||||
],
|
||||
0,
|
||||
],
|
||||
['stream', 'raw', 0],
|
||||
],
|
||||
]);
|
||||
$parser->setXrefMapPublic([
|
||||
'2_0' => '1_0_0',
|
||||
]);
|
||||
|
||||
$obj = ['objref', '2_0', 0];
|
||||
$this->assertSame($obj, $parser->getObjectValPublic($obj));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testGetObjectValReturnsOriginalWhenIndirectObjectParsesEmpty(): void
|
||||
{
|
||||
$parser = new ParserHarness();
|
||||
$parser->setXrefMapPublic(['2_0' => 10]);
|
||||
$parser->setStubIndirectReturn([]);
|
||||
|
||||
$obj = ['objref', '2_0', 0];
|
||||
$this->assertSame($obj, $parser->getObjectValPublic($obj));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testGetObjectValReturnsFirstElementForResolvedCompressedObject(): void
|
||||
{
|
||||
$parser = new ParserHarness();
|
||||
$parser->setXrefMapPublic([
|
||||
'2_0' => '1_0_0',
|
||||
'1_0' => 12,
|
||||
]);
|
||||
$parser->setStubIndirectReturn($this->buildObjectStreamObject(1, 4, '2 0 (A)'));
|
||||
|
||||
$resolved = $parser->getObjectValPublic(['objref', '2_0', 0]);
|
||||
|
||||
$this->assertNotSame('objref', $resolved[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testGetObjectValHandlesObjectStreamIndexAndBodyValidation(): void
|
||||
{
|
||||
$parser = new ParserHarness();
|
||||
$parser->setXrefMapPublic([
|
||||
'2_0' => '1_0_0',
|
||||
'1_0' => 12,
|
||||
]);
|
||||
$parser->setStubIndirectReturn($this->buildObjectStreamObject(1, 4, 'x y (A)'));
|
||||
|
||||
$obj = ['objref', '2_0', 0];
|
||||
$this->assertSame($obj, $parser->getObjectValPublic($obj));
|
||||
|
||||
$parser = new ParserHarness();
|
||||
$parser->setXrefMapPublic([
|
||||
'2_0' => '1_0_0',
|
||||
'1_0' => 12,
|
||||
]);
|
||||
$parser->setStubIndirectReturn($this->buildObjectStreamObject(1, 4, '0 0 ()'));
|
||||
$this->assertSame($obj, $parser->getObjectValPublic($obj));
|
||||
|
||||
$parser = new ParserHarness();
|
||||
$parser->setXrefMapPublic([
|
||||
'2_0' => '1_0_0',
|
||||
'1_0' => 12,
|
||||
]);
|
||||
$parser->setStubIndirectReturn($this->buildObjectStreamObject(0, 4, '2 0 (A)'));
|
||||
$this->assertSame($obj, $parser->getObjectValPublic($obj));
|
||||
|
||||
$parser = new ParserHarness();
|
||||
$parser->setXrefMapPublic([
|
||||
'2_0' => '1_0_0',
|
||||
'1_0' => 12,
|
||||
]);
|
||||
$parser->setStubIndirectReturn($this->buildObjectStreamObject(1, 0, ''));
|
||||
$this->assertSame($obj, $parser->getObjectValPublic($obj));
|
||||
|
||||
$parser = new ParserHarness();
|
||||
$parser->setXrefMapPublic([
|
||||
'2_0' => '1_0_0',
|
||||
'1_0' => 12,
|
||||
]);
|
||||
$parser->setStubIndirectReturn($this->buildObjectStreamObject(1, 6, '2 -10 '));
|
||||
$this->assertSame($obj, $parser->getObjectValPublic($obj));
|
||||
|
||||
$parser = new ParserHarness();
|
||||
$parser->setXrefMapPublic([
|
||||
'2_0' => '1_0_0',
|
||||
'1_0' => 12,
|
||||
]);
|
||||
$parser->setStubIndirectReturn($this->buildObjectStreamObject(1, 4, '2 0 '));
|
||||
$this->assertSame($obj, $parser->getObjectValPublic($obj));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testGetDecodeParmsHandlesSparseDictionaryIndexes(): void
|
||||
{
|
||||
$parser = new ParserHarness();
|
||||
$sdic = [
|
||||
['/', 'DecodeParms', 0],
|
||||
[
|
||||
'<<',
|
||||
[
|
||||
2 => ['/', 'Columns', 0],
|
||||
3 => ['numeric', '5', 0],
|
||||
],
|
||||
0,
|
||||
],
|
||||
];
|
||||
|
||||
$this->assertSame([], $parser->getDecodeParmsPublic($sdic, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, RawObjectArray>
|
||||
*/
|
||||
private function buildObjectStreamObject(int $n, int $first, string $decodedData): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'<<',
|
||||
[
|
||||
['/', 'Type', 0],
|
||||
['/', 'ObjStm', 0],
|
||||
['/', 'N', 0],
|
||||
['numeric', (string) $n, 0],
|
||||
['/', 'First', 0],
|
||||
['numeric', (string) $first, 0],
|
||||
],
|
||||
0,
|
||||
],
|
||||
['stream', 'raw', 0, [$decodedData, []]],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Regression: processAngular() must bail out when getRawObject() fails
|
||||
* to advance $offset, rather than spinning forever and exhausting PHP
|
||||
* memory. Without the guard, the inner do-while loop accumulates
|
||||
* identical zero-length tokens at the same offset until OOM.
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testProcessAngularBailsOnNonAdvancingByte(): void
|
||||
{
|
||||
// `<<` then `~` — a byte that processDefault() cannot consume — and
|
||||
// no `>>` terminator. Before the fix this hangs / OOMs in
|
||||
// RawObject::processAngular() at the inner do-while loop.
|
||||
$parser = new ParserHarness();
|
||||
$parser->setPdfDataPublic('<<~');
|
||||
|
||||
$element = $parser->callParentGetRawObject(0);
|
||||
|
||||
$this->assertIsArray($element);
|
||||
$this->assertSame('<<', $element[0]);
|
||||
$this->assertIsArray($element[1]);
|
||||
// The non-advancing guard must short-circuit within a single
|
||||
// iteration; the trailing array_pop then leaves $objval empty.
|
||||
$this->assertLessThan(5, \count($element[1]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Regression: processBracket() must bail out on a non-advancing parse,
|
||||
* for the same reasons as the dictionary loop above.
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testProcessBracketBailsOnNonAdvancingByte(): void
|
||||
{
|
||||
$parser = new ParserHarness();
|
||||
$parser->setPdfDataPublic('[~');
|
||||
|
||||
$element = $parser->callParentGetRawObject(0);
|
||||
|
||||
$this->assertIsArray($element);
|
||||
$this->assertSame('[', $element[0]);
|
||||
$this->assertIsArray($element[1]);
|
||||
$this->assertLessThan(5, \count($element[1]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testParentRawObjectParsesBooleanKeywords(): void
|
||||
{
|
||||
$parser = new ParserHarness();
|
||||
$parser->setPdfDataPublic('true false');
|
||||
|
||||
$first = $parser->callParentGetRawObject(0);
|
||||
$second = $parser->callParentGetRawObject(5);
|
||||
|
||||
$this->assertSame('boolean', $first[0]);
|
||||
$this->assertSame('true', $first[1]);
|
||||
$this->assertSame('boolean', $second[0]);
|
||||
$this->assertSame('false', $second[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testParentRawObjectParsesNestedParentheses(): void
|
||||
{
|
||||
$parser = new ParserHarness();
|
||||
$parser->setPdfDataPublic('((ab))');
|
||||
|
||||
$element = $parser->callParentGetRawObject(0);
|
||||
|
||||
$this->assertSame('(', $element[0]);
|
||||
$this->assertSame('(ab)', $element[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testParentRawObjectParsesHexStringAndMalformedHexFallback(): void
|
||||
{
|
||||
$parser = new ParserHarness();
|
||||
$parser->setPdfDataPublic('< 4A 4B >');
|
||||
|
||||
$hex = $parser->callParentGetRawObject(0);
|
||||
$this->assertSame('<', $hex[0]);
|
||||
$this->assertSame(' 4A 4B ', $hex[1]);
|
||||
|
||||
$parser = new ParserHarness();
|
||||
$parser->setPdfDataPublic('<GG>');
|
||||
$fallback = $parser->callParentGetRawObject(0);
|
||||
|
||||
$this->assertSame('<', $fallback[0]);
|
||||
$this->assertSame('', $fallback[1]);
|
||||
$this->assertSame(4, $fallback[2]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* ParserTest.php
|
||||
*
|
||||
* @since 2011-05-23
|
||||
* @category Library
|
||||
* @package Pdfparser
|
||||
* @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-parser
|
||||
*
|
||||
* This file is part of tc-lib-pdf-parser software library.
|
||||
*/
|
||||
|
||||
namespace Test;
|
||||
|
||||
use Com\Tecnick\Pdf\Parser\Parser;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Filter Test
|
||||
*
|
||||
* @since 2011-05-23
|
||||
* @category Library
|
||||
* @package PdfParser
|
||||
* @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-parser
|
||||
*/
|
||||
class ParserTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
#[DataProvider('getParseProvider')]
|
||||
public function testParse(string $filename, string $hash): void
|
||||
{
|
||||
$cfg = [
|
||||
'ignore_filter_errors' => true,
|
||||
];
|
||||
$rawdata = \file_get_contents($filename);
|
||||
$this->assertNotFalse($rawdata);
|
||||
$parser = new Parser($cfg);
|
||||
$data = $parser->parse($rawdata);
|
||||
$this->assertEquals($hash, \md5(\serialize($data)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{0:string, 1:string}>
|
||||
*/
|
||||
public static function getParseProvider(): array
|
||||
{
|
||||
return [
|
||||
['resources/test/example_005.pdf', '510a5ea860470dae0781f4bd8d5eb250'],
|
||||
['resources/test/example_036.pdf', '2869501cf41a4c4a0402c00832329e25'],
|
||||
['resources/test/example_046.pdf', 'cfaee514b9c09aa282b4e2a8f0061a3d'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testParseHandlesMultiRangeXrefIndexRegression(): void
|
||||
{
|
||||
$parser = new Parser(['ignore_filter_errors' => true]);
|
||||
$data = $parser->parse($this->buildMultiRangeXrefIndexPdf());
|
||||
|
||||
$xref = $data[0];
|
||||
$objects = $data[1];
|
||||
|
||||
$this->assertSame('41_0', $xref['trailer']['root']);
|
||||
$this->assertArrayNotHasKey('encrypt', $xref['trailer']);
|
||||
|
||||
$this->assertNotNull($objects['41_0'][0] ?? null);
|
||||
$this->assertSame('<<', $objects['41_0'][0][0]);
|
||||
$this->assertNotNull($objects['42_0'][0] ?? null);
|
||||
$this->assertSame('<<', $objects['42_0'][0][0]);
|
||||
$this->assertNotNull($objects['43_0'][0] ?? null);
|
||||
$this->assertSame('<<', $objects['43_0'][0][0]);
|
||||
|
||||
$this->assertSame('7aa83a972ca79bafe26be3620b7512e2', \md5(\serialize($data)));
|
||||
}
|
||||
|
||||
private function buildMultiRangeXrefIndexPdf(): string
|
||||
{
|
||||
$pdf = "%PDF-1.7\n";
|
||||
$offsets = [];
|
||||
$addObject = static function (string &$pdfData, array &$objOffsets, int $objNum, string $body): void {
|
||||
$objOffsets[$objNum] = \strlen($pdfData);
|
||||
$pdfData .= $objNum . " 0 obj\n" . $body . "\nendobj\n";
|
||||
};
|
||||
|
||||
$addObject($pdf, $offsets, 3, '<< /Dummy 3 >>');
|
||||
$addObject($pdf, $offsets, 15, '<< /Dummy 15 >>');
|
||||
$addObject($pdf, $offsets, 17, '<< /Dummy 17 >>');
|
||||
$addObject($pdf, $offsets, 18, '<< /Dummy 18 >>');
|
||||
$addObject($pdf, $offsets, 41, '<< /Type /Catalog /Pages 42 0 R >>');
|
||||
$addObject($pdf, $offsets, 42, '<< /Type /Pages /Count 1 /Kids [43 0 R] >>');
|
||||
$addObject($pdf, $offsets, 43, '<< /Type /Page /Parent 42 0 R /MediaBox [0 0 10 10] >>');
|
||||
|
||||
$indexObjects = [3, 15, 17, 18, 41, 42, 43];
|
||||
$bytes = [];
|
||||
foreach ($indexObjects as $objNum) {
|
||||
$offset = (int) ($offsets[$objNum] ?? 0);
|
||||
$bytes[] = 0;
|
||||
$bytes[] = 1;
|
||||
$bytes[] = ($offset >> 16) & 0xff;
|
||||
$bytes[] = ($offset >> 8) & 0xff;
|
||||
$bytes[] = $offset & 0xff;
|
||||
}
|
||||
|
||||
$stream = \pack('C*', ...$bytes);
|
||||
$streamLen = \strlen($stream);
|
||||
$xrefBody =
|
||||
'<< /Type /XRef'
|
||||
. ' /Size 60'
|
||||
. ' /Root 41 0 R'
|
||||
. ' /Index [3 1 15 1 17 2 41 3]'
|
||||
. ' /W [1 3 0]'
|
||||
. ' /Length '
|
||||
. $streamLen
|
||||
. ' /DecodeParms << /Columns 4 /Predictor 12 >>'
|
||||
. " >>\nstream\n"
|
||||
. $stream
|
||||
. "\nendstream";
|
||||
|
||||
$addObject($pdf, $offsets, 50, $xrefBody);
|
||||
|
||||
$startxref = (int) ($offsets[50] ?? 0);
|
||||
$pdf .= "startxref\n" . $startxref . "\n%%EOF";
|
||||
|
||||
return $pdf;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* TestCase.php
|
||||
*
|
||||
* @since 2011-05-23
|
||||
* @category Library
|
||||
* @package Pdfparser
|
||||
* @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-parser
|
||||
*
|
||||
* This file is part of tc-lib-pdf-parser software library.
|
||||
*/
|
||||
|
||||
namespace Test;
|
||||
|
||||
use PHPUnit\Framework\TestCase as FrameworkTestCase;
|
||||
|
||||
/**
|
||||
* Base test case with cross-version helpers.
|
||||
*/
|
||||
abstract class TestCase extends FrameworkTestCase
|
||||
{
|
||||
/**
|
||||
* Assert that the expected exception message contains the given substring.
|
||||
*
|
||||
* Uses expectExceptionMessageMatches(), the only message assertion that is
|
||||
* available and not deprecated across PHPUnit 11.5, 12.5 and 13.2 (PHP 8.2+).
|
||||
* The deprecated expectExceptionMessage() and the 13.2-only
|
||||
* expectExceptionMessageIsOrContains() are intentionally avoided.
|
||||
*
|
||||
* @param string $message Substring expected within the exception message.
|
||||
*/
|
||||
protected function expectExceptionMessageContains(string $message): void
|
||||
{
|
||||
$this->expectExceptionMessageMatches('/' . preg_quote($message, '/') . '/');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* XrefHarness.php
|
||||
*
|
||||
* @since 2011-05-23
|
||||
* @category Library
|
||||
* @package Pdfparser
|
||||
* @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-parser
|
||||
*
|
||||
* This file is part of tc-lib-pdf-parser software library.
|
||||
*/
|
||||
|
||||
namespace Test;
|
||||
|
||||
use Com\Tecnick\Pdf\Parser\Process\Xref;
|
||||
|
||||
/**
|
||||
* @phpstan-import-type RawObjectArray from \Com\Tecnick\Pdf\Parser\Process\RawObject
|
||||
*/
|
||||
class XrefHarness extends Xref
|
||||
{
|
||||
/** @var array<int, RawObjectArray> */
|
||||
private array $stubIndirectObject = [];
|
||||
|
||||
/** @var RawObjectArray|null */
|
||||
private ?array $stubRawObject = null;
|
||||
|
||||
public function setPdfDataPublic(string $pdfdata): void
|
||||
{
|
||||
$this->pdfdata = $pdfdata;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, RawObjectArray> $indirectObject
|
||||
*/
|
||||
public function setStubIndirectObject(array $indirectObject): void
|
||||
{
|
||||
$this->stubIndirectObject = $indirectObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param RawObjectArray $rawObject
|
||||
*/
|
||||
public function setStubRawObject(array $rawObject): void
|
||||
{
|
||||
$this->stubRawObject = $rawObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{
|
||||
* 'trailer'?: array{
|
||||
* 'encrypt'?: string,
|
||||
* 'id': array<int, string>,
|
||||
* 'info': string,
|
||||
* 'root': string,
|
||||
* 'size': int,
|
||||
* },
|
||||
* 'xref'?: array<string, int|string>,
|
||||
* } $xref
|
||||
*
|
||||
* @return array{
|
||||
* 'trailer': array{
|
||||
* 'encrypt'?: string,
|
||||
* 'id': array<int, string>,
|
||||
* 'info': string,
|
||||
* 'root': string,
|
||||
* 'size': int,
|
||||
* },
|
||||
* 'xref': array<string, int|string>,
|
||||
* }
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function getXrefDataPublic(int $offset = 0, array $xref = []): array
|
||||
{
|
||||
return $this->getXrefData($offset, $xref);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{
|
||||
* 'trailer'?: array{
|
||||
* 'encrypt'?: string,
|
||||
* 'id': array<int, string>,
|
||||
* 'info': string,
|
||||
* 'root': string,
|
||||
* 'size': int,
|
||||
* },
|
||||
* 'xref': array<string, int|string>,
|
||||
* } $xref
|
||||
*
|
||||
* @return array{
|
||||
* 'trailer': array{
|
||||
* 'encrypt'?: string,
|
||||
* 'id': array<int, string>,
|
||||
* 'info': string,
|
||||
* 'root': string,
|
||||
* 'size': int,
|
||||
* },
|
||||
* 'xref': array<string, int|string>,
|
||||
* }
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function decodeXrefPublic(int $startxref, array $xref): array
|
||||
{
|
||||
return $this->decodeXref($startxref, $xref);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{
|
||||
* 'trailer'?: array{
|
||||
* 'encrypt'?: string,
|
||||
* 'id': array<int, string>,
|
||||
* 'info': string,
|
||||
* 'root': string,
|
||||
* 'size': int,
|
||||
* },
|
||||
* 'xref': array<string, int|string>,
|
||||
* } $xref
|
||||
*
|
||||
* @return array{
|
||||
* 'trailer': array{
|
||||
* 'encrypt'?: string,
|
||||
* 'id': array<int, string>,
|
||||
* 'info': string,
|
||||
* 'root': string,
|
||||
* 'size': int,
|
||||
* },
|
||||
* 'xref': array<string, int|string>,
|
||||
* }
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function decodeXrefStreamPublic(int $startxref, array $xref): array
|
||||
{
|
||||
return $this->decodeXrefStream($startxref, $xref);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<int, int>> $sdata
|
||||
* @param array<int, array<int, int>> $ddata
|
||||
* @param array<int, int> $wbt
|
||||
*/
|
||||
public function processDdataPublic(array &$sdata, array $ddata, array $wbt): void
|
||||
{
|
||||
$this->processDdata($sdata, $ddata, $wbt);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, RawObjectArray>
|
||||
*/
|
||||
protected function getIndirectObject(string $obj_ref, int $offset = 0, bool $decoding = true): array
|
||||
{
|
||||
$unused = [$obj_ref, $offset, $decoding];
|
||||
unset($unused);
|
||||
|
||||
return $this->stubIndirectObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return RawObjectArray
|
||||
*/
|
||||
protected function getRawObject(int $offset = 0): array
|
||||
{
|
||||
if ($this->stubRawObject !== null) {
|
||||
return $this->stubRawObject;
|
||||
}
|
||||
|
||||
return parent::getRawObject($offset);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* XrefStreamHarness.php
|
||||
*
|
||||
* @since 2011-05-23
|
||||
* @category Library
|
||||
* @package Pdfparser
|
||||
* @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-parser
|
||||
*
|
||||
* This file is part of tc-lib-pdf-parser software library.
|
||||
*/
|
||||
|
||||
namespace Test;
|
||||
|
||||
use Com\Tecnick\Pdf\Parser\Process\XrefStream;
|
||||
|
||||
/**
|
||||
* @phpstan-import-type RawObjectArray from \Com\Tecnick\Pdf\Parser\Process\RawObject
|
||||
*/
|
||||
class XrefStreamHarness extends XrefStream
|
||||
{
|
||||
/**
|
||||
* @param array{
|
||||
* 'trailer': array{
|
||||
* 'encrypt'?: string,
|
||||
* 'id': array<int, string>,
|
||||
* 'info': string,
|
||||
* 'root': string,
|
||||
* 'size': int,
|
||||
* },
|
||||
* 'xref': array<string, int|string>,
|
||||
* } $xref
|
||||
* @param array<int, array<int, int>> $sdata
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function processObjIndexesPublic(array &$xref, int &$obj_num, array $sdata): void
|
||||
{
|
||||
$this->processObjIndexes($xref, $obj_num, $sdata);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{
|
||||
* 'trailer': array{
|
||||
* 'encrypt'?: string,
|
||||
* 'id': array<int, string>,
|
||||
* 'info': string,
|
||||
* 'root': string,
|
||||
* 'size': int,
|
||||
* },
|
||||
* 'xref': array<string, int|string>,
|
||||
* } $xref
|
||||
* @param array<int, int> $objNumbers
|
||||
* @param array<int, array<int, int>> $sdata
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function processObjIndexesMapPublic(array &$xref, array $objNumbers, array $sdata): void
|
||||
{
|
||||
$this->processObjIndexesMap($xref, $objNumbers, $sdata);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{
|
||||
* 'trailer': array{
|
||||
* 'encrypt'?: string,
|
||||
* 'id': array<int, string>,
|
||||
* 'info': string,
|
||||
* 'root': string,
|
||||
* 'size': int,
|
||||
* },
|
||||
* 'xref': array<string, int|string>,
|
||||
* } $xref
|
||||
* @param array<int, int> $sdatum
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function processSingleObjIndexPublic(array &$xref, int $objNum, array $sdatum): void
|
||||
{
|
||||
$this->processSingleObjIndex($xref, $objNum, $sdatum);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param RawObjectArray|null $indexObj
|
||||
*
|
||||
* @return array<int, array{0:int, 1:int}>|null
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function parseXrefIndexSectionsPublic(?array $indexObj): ?array
|
||||
{
|
||||
return $this->parseXrefIndexSections($indexObj);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array{0:int, 1:int}> $indexSections
|
||||
*
|
||||
* @return array<int, int>
|
||||
*/
|
||||
public function buildXrefObjectNumbersPublic(array $indexSections): array
|
||||
{
|
||||
return $this->buildXrefObjectNumbers($indexSections);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<int, int>> $sdata
|
||||
* @param array<int, array<int, int>> $ddata
|
||||
* @param array<int, int> $prev_row
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function pngUnpredictorPublic(array $sdata, array &$ddata, int $columns, array $prev_row): void
|
||||
{
|
||||
$this->pngUnpredictor($sdata, $ddata, $columns, $prev_row);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<int, int>> $ddata
|
||||
* @param array{0:int, 1:int, 2:int} $rows
|
||||
*/
|
||||
public function minDistancePublic(array &$ddata, int $key, int $row_value, int $jdx, array $rows): void
|
||||
{
|
||||
$this->minDistance($ddata, $key, $row_value, $jdx, $rows);
|
||||
}
|
||||
|
||||
/** @param RawObjectArray|null $next */
|
||||
public function processXrefPrevPublic(?array $next, ?int &$prevxref): void
|
||||
{
|
||||
$this->processXrefPrev($next, $prevxref);
|
||||
}
|
||||
|
||||
/** @param RawObjectArray|null $next */
|
||||
public function processXrefDecodeParmsPublic(?array $next, int &$columns, int &$predictor): void
|
||||
{
|
||||
$this->processXrefDecodeParms($next, $columns, $predictor);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, RawObjectArray> $sarr
|
||||
* @param array{
|
||||
* trailer: array{encrypt?: string, id: array<int, string>, info: string, root: string, size: int},
|
||||
* xref: array<string, int|string>,
|
||||
* } $xref
|
||||
*/
|
||||
public function processXrefTypeFtPublic(string $type, array $sarr, int $key, array &$xref, bool $filltrailer): void
|
||||
{
|
||||
$this->processXrefTypeFt($type, $sarr, $key, $xref, $filltrailer);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, RawObjectArray> $sarr
|
||||
* @param array{
|
||||
* trailer: array{encrypt?: string, id: array<int, string>, info: string, root: string, size: int},
|
||||
* xref: array<string, int|string>,
|
||||
* } $xref
|
||||
*
|
||||
* @return array{
|
||||
* trailer: array{encrypt?: string, id: array<int, string>, info: string, root: string, size: int},
|
||||
* xref: array<string, int|string>,
|
||||
* }
|
||||
*/
|
||||
public function processXrefObjrefPublic(string $type, array $sarr, int $key, array $xref): array
|
||||
{
|
||||
return $this->processXrefObjref($type, $sarr, $key, $xref);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, RawObjectArray> $sarr
|
||||
* @param array{
|
||||
* trailer: array{encrypt?: string, id: array<int, string>, info: string, root: string, size: int},
|
||||
* xref: array<string, int|string>,
|
||||
* } $xref
|
||||
* @param array<int, int> $wbt
|
||||
* @param array{
|
||||
* index_sections: array<int, array{0:int, 1:int}>|null,
|
||||
* prevxref: int|null,
|
||||
* predictor: int,
|
||||
* columns: int,
|
||||
* size: int|null,
|
||||
* valid_crs: bool
|
||||
* } $state
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function processXrefTypePublic(
|
||||
array $sarr,
|
||||
array &$xref,
|
||||
array &$wbt,
|
||||
array &$state,
|
||||
bool $filltrailer,
|
||||
): void {
|
||||
$this->processXrefType($sarr, $xref, $wbt, $state, $filltrailer);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,971 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* XrefTest.php
|
||||
*
|
||||
* @since 2011-05-23
|
||||
* @category Library
|
||||
* @package Pdfparser
|
||||
* @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-parser
|
||||
*
|
||||
* This file is part of tc-lib-pdf-parser software library.
|
||||
*/
|
||||
|
||||
namespace Test;
|
||||
|
||||
use Com\Tecnick\Pdf\Parser\Exception as PPException;
|
||||
|
||||
class XrefTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testProcessObjIndexesHandlesInUseAndCompressedObjects(): void
|
||||
{
|
||||
$xref = [
|
||||
'trailer' => [
|
||||
'id' => [],
|
||||
'info' => '',
|
||||
'root' => '',
|
||||
'size' => 0,
|
||||
],
|
||||
'xref' => [],
|
||||
];
|
||||
$obj_num = 3;
|
||||
$sdata = [
|
||||
[1, 42, 0],
|
||||
[2, 7, 4],
|
||||
[0, 0, 0],
|
||||
];
|
||||
|
||||
$parser = new XrefStreamHarness();
|
||||
$parser->processObjIndexesPublic($xref, $obj_num, $sdata);
|
||||
|
||||
$this->assertSame(6, $obj_num);
|
||||
$this->assertSame(42, $xref['xref']['3_0'] ?? null);
|
||||
$this->assertSame('7_0_4', $xref['xref']['4_0'] ?? null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testPngUnpredictorDecodesAndRejectsUnknownPredictor(): void
|
||||
{
|
||||
$parser = new XrefStreamHarness();
|
||||
|
||||
$decoded = [];
|
||||
$parser->pngUnpredictorPublic([[0, 10]], $decoded, 1, [0]);
|
||||
if (!isset($decoded[0][0])) {
|
||||
$this->fail('Decoded first row value is missing.');
|
||||
}
|
||||
$this->assertSame(10, $decoded[0][0]);
|
||||
|
||||
$decoded = [];
|
||||
$parser->pngUnpredictorPublic([[1, 5]], $decoded, 1, [0]);
|
||||
if (!isset($decoded[0][0])) {
|
||||
$this->fail('Decoded first row value is missing.');
|
||||
}
|
||||
$this->assertSame(5, $decoded[0][0]);
|
||||
|
||||
$decoded = [];
|
||||
$parser->pngUnpredictorPublic([[2, 3]], $decoded, 1, [4]);
|
||||
if (!isset($decoded[0][0])) {
|
||||
$this->fail('Decoded first row value is missing.');
|
||||
}
|
||||
$this->assertSame(7, $decoded[0][0]);
|
||||
|
||||
$decoded = [];
|
||||
$parser->pngUnpredictorPublic([[4, 8]], $decoded, 1, [1]);
|
||||
if (!isset($decoded[0][0])) {
|
||||
$this->fail('Decoded first row value is missing.');
|
||||
}
|
||||
$this->assertSame(9, $decoded[0][0]);
|
||||
|
||||
$this->expectException(PPException::class);
|
||||
$this->expectExceptionMessageContains('Unknown PNG predictor');
|
||||
$decoded = [];
|
||||
$parser->pngUnpredictorPublic([[9, 1]], $decoded, 1, [0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testDecodeXrefParsesEntriesAndTrailer(): void
|
||||
{
|
||||
$pdf =
|
||||
"xref\r\n"
|
||||
. "0 2\r\n"
|
||||
. "0000000000 65535 f\r\n"
|
||||
. "0000000017 00000 n\r\n"
|
||||
. "trailer << /Size 2 /Root 1 0 R /Info 2 0 R /Encrypt 3 0 R /ID [<AA><BB>] >>\r\n";
|
||||
|
||||
$parser = new XrefHarness();
|
||||
$parser->setPdfDataPublic($pdf);
|
||||
$xref = $parser->decodeXrefPublic(0, ['xref' => []]);
|
||||
|
||||
$this->assertSame(17, $xref['xref']['1_0'] ?? null);
|
||||
$this->assertSame(2, $xref['trailer']['size']);
|
||||
$this->assertSame('1_0', $xref['trailer']['root']);
|
||||
$this->assertSame('2_0', $xref['trailer']['info']);
|
||||
$this->assertSame('3_0', $xref['trailer']['encrypt'] ?? null);
|
||||
$this->assertSame(['AA', 'BB'], $xref['trailer']['id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testDecodeXrefLeavesEncryptUnsetWhenMissing(): void
|
||||
{
|
||||
$pdf =
|
||||
"xref\r\n"
|
||||
. "0 1\r\n"
|
||||
. "0000000017 00000 n\r\n"
|
||||
. "trailer << /Size 1 /Root 1 0 R /Info 2 0 R /ID [<AA><BB>] >>\r\n";
|
||||
|
||||
$parser = new XrefHarness();
|
||||
$parser->setPdfDataPublic($pdf);
|
||||
$xref = $parser->decodeXrefPublic(0, ['xref' => []]);
|
||||
|
||||
$this->assertArrayNotHasKey('encrypt', $xref['trailer']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testDecodeXrefStreamParsesRowsAndTrailer(): void
|
||||
{
|
||||
$stream_data = \pack('C*', 0, 1, 10, 0, 0, 2, 5, 1);
|
||||
|
||||
$parser = new XrefHarness();
|
||||
$parser->setStubRawObject(['objref', '5_0', 0]);
|
||||
$parser->setStubIndirectObject([
|
||||
[
|
||||
'<<',
|
||||
[
|
||||
['/', 'Type', 0],
|
||||
['/', 'XRef', 0],
|
||||
['/', 'W', 0],
|
||||
['[', [['numeric', '1', 0], ['numeric', '1', 0], ['numeric', '1', 0]], 0],
|
||||
['/', 'Index', 0],
|
||||
['[', [['numeric', '0', 0], ['numeric', '2', 0]], 0],
|
||||
['/', 'Size', 0],
|
||||
['numeric', '2', 0],
|
||||
['/', 'Root', 0],
|
||||
['objref', '1_0', 0],
|
||||
['/', 'ID', 0],
|
||||
['[', [['hex', 'AA', 0], ['hex', 'BB', 0]], 0],
|
||||
['/', 'DecodeParms', 0],
|
||||
['<<', [['/', 'Columns', 0], ['numeric', '3', 0], ['/', 'Predictor', 0], ['numeric', '10', 0]], 0],
|
||||
],
|
||||
0,
|
||||
],
|
||||
['stream', $stream_data, 0, [$stream_data, []]],
|
||||
]);
|
||||
|
||||
$xref = $parser->decodeXrefStreamPublic(100, ['xref' => []]);
|
||||
|
||||
$this->assertSame(10, $xref['xref']['0_0'] ?? null);
|
||||
$this->assertSame('5_0_1', $xref['xref']['1_0'] ?? null);
|
||||
$this->assertSame('1_0', $xref['trailer']['root']);
|
||||
$this->assertSame(2, $xref['trailer']['size']);
|
||||
$this->assertSame(['AA', 'BB'], $xref['trailer']['id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testDecodeXrefStreamMapsMultiRangeIndexSections(): void
|
||||
{
|
||||
$stream_data = \pack(
|
||||
'C*',
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
30,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
31,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
32,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
33,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
34,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
35,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
36,
|
||||
);
|
||||
|
||||
$parser = new XrefHarness();
|
||||
$parser->setStubRawObject(['objref', '5_0', 0]);
|
||||
$parser->setStubIndirectObject([
|
||||
[
|
||||
'<<',
|
||||
[
|
||||
['/', 'Type', 0],
|
||||
['/', 'XRef', 0],
|
||||
['/', 'W', 0],
|
||||
['[', [['numeric', '1', 0], ['numeric', '3', 0], ['numeric', '0', 0]], 0],
|
||||
['/', 'Index', 0],
|
||||
[
|
||||
'[',
|
||||
[
|
||||
['numeric', '3', 0],
|
||||
['numeric', '1', 0],
|
||||
['numeric', '15', 0],
|
||||
['numeric', '1', 0],
|
||||
['numeric', '17', 0],
|
||||
['numeric', '2', 0],
|
||||
['numeric', '41', 0],
|
||||
['numeric', '3', 0],
|
||||
],
|
||||
0,
|
||||
],
|
||||
['/', 'Size', 0],
|
||||
['numeric', '44', 0],
|
||||
['/', 'DecodeParms', 0],
|
||||
['<<', [['/', 'Columns', 0], ['numeric', '4', 0], ['/', 'Predictor', 0], ['numeric', '10', 0]], 0],
|
||||
],
|
||||
0,
|
||||
],
|
||||
['stream', $stream_data, 0, [$stream_data, []]],
|
||||
]);
|
||||
|
||||
$xref = $parser->decodeXrefStreamPublic(100, ['xref' => []]);
|
||||
|
||||
$this->assertSame(30, $xref['xref']['3_0'] ?? null);
|
||||
$this->assertSame(31, $xref['xref']['15_0'] ?? null);
|
||||
$this->assertSame(32, $xref['xref']['17_0'] ?? null);
|
||||
$this->assertSame(33, $xref['xref']['18_0'] ?? null);
|
||||
$this->assertSame(34, $xref['xref']['41_0'] ?? null);
|
||||
$this->assertSame(35, $xref['xref']['42_0'] ?? null);
|
||||
$this->assertSame(36, $xref['xref']['43_0'] ?? null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testDecodeXrefStreamFallsBackToSizeWhenIndexIsMissing(): void
|
||||
{
|
||||
$stream_data = \pack('C*', 0, 1, 0, 0, 20, 0, 1, 0, 0, 21);
|
||||
|
||||
$parser = new XrefHarness();
|
||||
$parser->setStubRawObject(['objref', '5_0', 0]);
|
||||
$parser->setStubIndirectObject([
|
||||
[
|
||||
'<<',
|
||||
[
|
||||
['/', 'Type', 0],
|
||||
['/', 'XRef', 0],
|
||||
['/', 'W', 0],
|
||||
['[', [['numeric', '1', 0], ['numeric', '3', 0], ['numeric', '0', 0]], 0],
|
||||
['/', 'Size', 0],
|
||||
['numeric', '2', 0],
|
||||
['/', 'DecodeParms', 0],
|
||||
['<<', [['/', 'Columns', 0], ['numeric', '4', 0], ['/', 'Predictor', 0], ['numeric', '10', 0]], 0],
|
||||
],
|
||||
0,
|
||||
],
|
||||
['stream', $stream_data, 0, [$stream_data, []]],
|
||||
]);
|
||||
|
||||
$xref = $parser->decodeXrefStreamPublic(100, ['xref' => []]);
|
||||
|
||||
$this->assertSame(20, $xref['xref']['0_0'] ?? null);
|
||||
$this->assertSame(21, $xref['xref']['1_0'] ?? null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testDecodeXrefStreamRejectsOddIndexArrayLength(): void
|
||||
{
|
||||
$stream_data = \pack('C*', 0, 1, 0, 0, 20);
|
||||
|
||||
$parser = new XrefHarness();
|
||||
$parser->setStubRawObject(['objref', '5_0', 0]);
|
||||
$parser->setStubIndirectObject([
|
||||
[
|
||||
'<<',
|
||||
[
|
||||
['/', 'Type', 0],
|
||||
['/', 'XRef', 0],
|
||||
['/', 'W', 0],
|
||||
['[', [['numeric', '1', 0], ['numeric', '3', 0], ['numeric', '0', 0]], 0],
|
||||
['/', 'Index', 0],
|
||||
['[', [['numeric', '3', 0], ['numeric', '1', 0], ['numeric', '15', 0]], 0],
|
||||
['/', 'Size', 0],
|
||||
['numeric', '16', 0],
|
||||
['/', 'DecodeParms', 0],
|
||||
['<<', [['/', 'Columns', 0], ['numeric', '4', 0], ['/', 'Predictor', 0], ['numeric', '10', 0]], 0],
|
||||
],
|
||||
0,
|
||||
],
|
||||
['stream', $stream_data, 0, [$stream_data, []]],
|
||||
]);
|
||||
|
||||
$this->expectException(PPException::class);
|
||||
$this->expectExceptionMessageContains('Invalid xref stream Index array: expected even number of values');
|
||||
$parser->decodeXrefStreamPublic(100, ['xref' => []]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testDecodeXrefStreamRejectsRowCountMismatchAgainstIndexCoverage(): void
|
||||
{
|
||||
$stream_data = \pack('C*', 0, 1, 0, 0, 20, 0, 1, 0, 0, 21);
|
||||
|
||||
$parser = new XrefHarness();
|
||||
$parser->setStubRawObject(['objref', '5_0', 0]);
|
||||
$parser->setStubIndirectObject([
|
||||
[
|
||||
'<<',
|
||||
[
|
||||
['/', 'Type', 0],
|
||||
['/', 'XRef', 0],
|
||||
['/', 'W', 0],
|
||||
['[', [['numeric', '1', 0], ['numeric', '3', 0], ['numeric', '0', 0]], 0],
|
||||
['/', 'Index', 0],
|
||||
['[', [['numeric', '3', 0], ['numeric', '3', 0]], 0],
|
||||
['/', 'Size', 0],
|
||||
['numeric', '20', 0],
|
||||
['/', 'DecodeParms', 0],
|
||||
['<<', [['/', 'Columns', 0], ['numeric', '4', 0], ['/', 'Predictor', 0], ['numeric', '10', 0]], 0],
|
||||
],
|
||||
0,
|
||||
],
|
||||
['stream', $stream_data, 0, [$stream_data, []]],
|
||||
]);
|
||||
|
||||
$this->expectException(PPException::class);
|
||||
$this->expectExceptionMessageContains('Invalid xref stream row count: expected 3 rows from Index, got 2');
|
||||
$parser->decodeXrefStreamPublic(100, ['xref' => []]);
|
||||
}
|
||||
|
||||
public function testProcessDdataUsesDefaultTypeWhenFirstFieldWidthIsZero(): void
|
||||
{
|
||||
$parser = new XrefHarness();
|
||||
$sdata = [];
|
||||
$parser->processDdataPublic($sdata, [[9, 3]], [0, 1, 1]);
|
||||
|
||||
$this->assertSame([1, 9, 3], $sdata[0] ?? null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testGetXrefDataThrowsWhenStartxrefIsMissing(): void
|
||||
{
|
||||
$parser = new XrefHarness();
|
||||
$parser->setPdfDataPublic("%PDF-1.7\nNo xref markers here");
|
||||
|
||||
$this->expectException(PPException::class);
|
||||
$this->expectExceptionMessageContains('Unable to find startxref (1)');
|
||||
$parser->getXrefDataPublic();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testProcessObjIndexesIgnoresUnknownEntryTypes(): void
|
||||
{
|
||||
$xref = [
|
||||
'trailer' => [
|
||||
'id' => [],
|
||||
'info' => '',
|
||||
'root' => '',
|
||||
'size' => 0,
|
||||
],
|
||||
'xref' => [],
|
||||
];
|
||||
$obj_num = 9;
|
||||
|
||||
$parser = new XrefStreamHarness();
|
||||
$parser->processObjIndexesPublic($xref, $obj_num, [[9, 1, 2]]);
|
||||
|
||||
$this->assertSame(10, $obj_num);
|
||||
$this->assertSame([], $xref['xref']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testPngUnpredictorCoversAveragePredictor(): void
|
||||
{
|
||||
$parser = new XrefStreamHarness();
|
||||
$decoded = [];
|
||||
|
||||
$parser->pngUnpredictorPublic([[3, 4]], $decoded, 1, [2]);
|
||||
|
||||
if (!isset($decoded[0][0])) {
|
||||
$this->fail('Decoded first row value is missing.');
|
||||
}
|
||||
|
||||
$this->assertSame(5, $decoded[0][0]);
|
||||
}
|
||||
|
||||
public function testMinDistanceCoversAllOutcomeBranches(): void
|
||||
{
|
||||
$parser = new XrefStreamHarness();
|
||||
$ddata = [[0]];
|
||||
|
||||
$parser->minDistancePublic($ddata, 0, 5, 0, [7, 2, 0]);
|
||||
$this->assertSame(12, $ddata[0][0] ?? null);
|
||||
|
||||
$parser->minDistancePublic($ddata, 0, 5, 0, [10, 3, 9]);
|
||||
$this->assertSame(8, $ddata[0][0] ?? null);
|
||||
|
||||
$parser->minDistancePublic($ddata, 0, 5, 0, [0, 10, 4]);
|
||||
$this->assertSame(9, $ddata[0][0] ?? null);
|
||||
}
|
||||
|
||||
public function testProcessXrefPrevAndDecodeParmsHandleInvalidInput(): void
|
||||
{
|
||||
$parser = new XrefStreamHarness();
|
||||
|
||||
$prevxref = null;
|
||||
$parser->processXrefPrevPublic(['name', 'x', 0], $prevxref);
|
||||
$this->assertNull($prevxref);
|
||||
|
||||
$parser->processXrefPrevPublic(['numeric', '17', 0], $prevxref);
|
||||
$this->assertSame(17, $prevxref);
|
||||
|
||||
$columns = 9;
|
||||
$predictor = 3;
|
||||
$parser->processXrefDecodeParmsPublic(['name', 'x', 0], $columns, $predictor);
|
||||
$this->assertSame(9, $columns);
|
||||
$this->assertSame(3, $predictor);
|
||||
|
||||
$parser->processXrefDecodeParmsPublic(
|
||||
['<<', [['/', 'Columns', 0], ['numeric', '-5', 0], ['/', 'Predictor', 0], ['numeric', '12', 0]], 0],
|
||||
$columns,
|
||||
$predictor,
|
||||
);
|
||||
$this->assertSame(0, $columns);
|
||||
$this->assertSame(12, $predictor);
|
||||
}
|
||||
|
||||
public function testProcessXrefTypeFtAndObjrefCoverTrailerBranches(): void
|
||||
{
|
||||
$parser = new XrefStreamHarness();
|
||||
$xref = [
|
||||
'trailer' => [
|
||||
'id' => [],
|
||||
'info' => '',
|
||||
'root' => '',
|
||||
'size' => 0,
|
||||
],
|
||||
'xref' => [],
|
||||
];
|
||||
|
||||
$sarr = [
|
||||
['/', 'Root', 0],
|
||||
['objref', '1_0', 0],
|
||||
['/', 'Info', 0],
|
||||
['objref', '2_0', 0],
|
||||
['/', 'Encrypt', 0],
|
||||
['objref', '3_0', 0],
|
||||
['/', 'Size', 0],
|
||||
['numeric', '4', 0],
|
||||
['/', 'ID', 0],
|
||||
['[', [['hex', 'AA', 0], ['hex', 'BB', 0]], 0],
|
||||
['/', 'ID', 0],
|
||||
['[', [['hex', '', 0], ['hex', 'BB', 0]], 0],
|
||||
];
|
||||
|
||||
$parser->processXrefTypeFtPublic('Root', $sarr, 0, $xref, true);
|
||||
$parser->processXrefTypeFtPublic('Info', $sarr, 2, $xref, true);
|
||||
$parser->processXrefTypeFtPublic('Encrypt', $sarr, 4, $xref, true);
|
||||
$parser->processXrefTypeFtPublic('Size', $sarr, 6, $xref, true);
|
||||
$parser->processXrefTypeFtPublic('ID', $sarr, 8, $xref, true);
|
||||
$parser->processXrefTypeFtPublic('ID', $sarr, 10, $xref, true);
|
||||
|
||||
$this->assertSame('1_0', $xref['trailer']['root']);
|
||||
$this->assertSame('2_0', $xref['trailer']['info']);
|
||||
$this->assertSame('3_0', $xref['trailer']['encrypt'] ?? null);
|
||||
$this->assertSame(4, $xref['trailer']['size']);
|
||||
$this->assertSame(['AA', 'BB'], $xref['trailer']['id']);
|
||||
|
||||
$before = $xref;
|
||||
$parser->processXrefTypeFtPublic('Size', $sarr, 6, $xref, false);
|
||||
$this->assertSame($before, $xref);
|
||||
|
||||
$objref = $parser->processXrefObjrefPublic('Root', [['/', 'Root', 0], ['name', 'x', 0]], 0, $xref);
|
||||
$this->assertSame($xref, $objref);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testDecodeXrefThrowsWhenTrailerIsMissing(): void
|
||||
{
|
||||
$parser = new XrefHarness();
|
||||
$parser->setPdfDataPublic("xref\r\n0 1\r\n0000000000 65535 f\r\n");
|
||||
|
||||
$this->expectException(PPException::class);
|
||||
$this->expectExceptionMessageContains('Unable to find trailer');
|
||||
$parser->decodeXrefPublic(0, ['xref' => []]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testGetXrefDataDetectsRepeatedOffsetLoop(): void
|
||||
{
|
||||
$parser = new XrefHarness();
|
||||
$parser->setPdfDataPublic("%PDF-1.7\ninvalid\n");
|
||||
|
||||
try {
|
||||
$parser->getXrefDataPublic(5, ['xref' => []]);
|
||||
} catch (PPException $exception) {
|
||||
$this->assertContains($exception->getMessage(), [
|
||||
'Unable to find startxref (3)',
|
||||
'Unable to find xref (4)',
|
||||
]);
|
||||
}
|
||||
|
||||
$this->expectException(PPException::class);
|
||||
$this->expectExceptionMessageContains('LOOP: this XRef offset has been already processed');
|
||||
$parser->getXrefDataPublic(5, ['xref' => []]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testGetXrefDataFindsObjectStreamStartxrefFromOffset(): void
|
||||
{
|
||||
$stream_data = \pack('C*', 0, 1, 9, 0);
|
||||
|
||||
$parser = new XrefHarness();
|
||||
$parser->setPdfDataPublic('AAAAA12 0 obj .... xref');
|
||||
$parser->setStubRawObject(['objref', '12_0', 0]);
|
||||
$parser->setStubIndirectObject([
|
||||
[
|
||||
'<<',
|
||||
[
|
||||
['/', 'Type', 0],
|
||||
['/', 'XRef', 0],
|
||||
['/', 'W', 0],
|
||||
['[', [['numeric', '1', 0], ['numeric', '1', 0], ['numeric', '1', 0]], 0],
|
||||
['/', 'Index', 0],
|
||||
['[', [['numeric', '0', 0], ['numeric', '1', 0]], 0],
|
||||
['/', 'Size', 0],
|
||||
['numeric', '1', 0],
|
||||
['/', 'DecodeParms', 0],
|
||||
['<<', [['/', 'Columns', 0], ['numeric', '3', 0], ['/', 'Predictor', 0], ['numeric', '10', 0]], 0],
|
||||
],
|
||||
0,
|
||||
],
|
||||
['stream', $stream_data, 0, [$stream_data, []]],
|
||||
]);
|
||||
|
||||
$xref = $parser->getXrefDataPublic(5, ['xref' => []]);
|
||||
|
||||
$this->assertSame(9, $xref['xref']['0_0'] ?? null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testGetXrefDataFindsStartxrefMarkerWhenOffsetIsNonZero(): void
|
||||
{
|
||||
$body = 'xxxxxxxxxxxxxxxxxxxx';
|
||||
$pdf =
|
||||
$body
|
||||
. "xref\r\n"
|
||||
. "0 1\r\n"
|
||||
. "0000000001 00000 n\r\n"
|
||||
. "trailer << /Size 1 >>\r\n"
|
||||
. "startxref\n20\n%%EOF";
|
||||
|
||||
$parser = new XrefHarness();
|
||||
$parser->setPdfDataPublic($pdf);
|
||||
|
||||
$start = (int) \strpos($pdf, 'startxref');
|
||||
$xref = $parser->getXrefDataPublic($start - 1, ['xref' => []]);
|
||||
|
||||
$this->assertSame(1, $xref['xref']['0_0'] ?? null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testGetXrefDataThrowsStartxref3ForNonZeroOffsetWithoutMarkers(): void
|
||||
{
|
||||
$parser = new XrefHarness();
|
||||
$parser->setPdfDataPublic('%PDF-1.7\nbody without xref hints');
|
||||
|
||||
$this->expectException(PPException::class);
|
||||
$this->expectExceptionMessageContains('Unable to find startxref (3)');
|
||||
$parser->getXrefDataPublic(5, ['xref' => []]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testDecodeXrefCallsGetXrefDataWhenTrailerContainsPrev(): void
|
||||
{
|
||||
$parser = new class() extends XrefHarness {
|
||||
public int $capturedOffset = -1;
|
||||
|
||||
protected function getXrefData(int $offset = 0, array $xref = []): array
|
||||
{
|
||||
$this->capturedOffset = $offset;
|
||||
$xref['xref']['prev_0'] = $offset;
|
||||
|
||||
return [
|
||||
'trailer' => $xref['trailer'] ?? ['id' => [], 'info' => '', 'root' => '', 'size' => 0],
|
||||
'xref' => $xref['xref'],
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
$parser->setPdfDataPublic("xref\r\n0 1\r\n0000000001 00000 n\r\ntrailer << /Size 1 /Prev 11 >>\r\n");
|
||||
|
||||
$xref = $parser->decodeXrefPublic(0, ['xref' => []]);
|
||||
|
||||
$this->assertSame(11, $parser->capturedOffset);
|
||||
$this->assertSame(11, $xref['xref']['prev_0'] ?? null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testDecodeXrefStreamThrowsWhenRawObjectValueIsNotString(): void
|
||||
{
|
||||
$parser = new XrefHarness();
|
||||
$parser->setStubRawObject(['numeric', [], 0]);
|
||||
|
||||
$this->expectException(PPException::class);
|
||||
$this->expectExceptionMessageContains('Unable to find xref stream');
|
||||
$parser->decodeXrefStreamPublic(0, ['xref' => []]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testDecodeXrefStreamHandlesNonArrayDictionaryPayload(): void
|
||||
{
|
||||
$stream_data = \pack('C*', 0, 1, 9, 0);
|
||||
|
||||
$parser = new XrefHarness();
|
||||
$parser->setStubRawObject(['objref', '1_0', 0]);
|
||||
$parser->setStubIndirectObject([
|
||||
['<<', 'not-array', 0],
|
||||
['stream', $stream_data, 0, [$stream_data, []]],
|
||||
]);
|
||||
|
||||
$xref = $parser->decodeXrefStreamPublic(0, ['xref' => ['9_0' => 99]]);
|
||||
$this->assertSame(99, $xref['xref']['9_0'] ?? null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testDecodeXrefStreamCallsGetXrefDataWhenPrevIsPresent(): void
|
||||
{
|
||||
$stream_data = \pack('C*', 0, 1, 9, 0);
|
||||
|
||||
$parser = new class() extends XrefHarness {
|
||||
public int $capturedOffset = -1;
|
||||
|
||||
protected function getXrefData(int $offset = 0, array $xref = []): array
|
||||
{
|
||||
$this->capturedOffset = $offset;
|
||||
$xref['xref']['prev_0'] = $offset;
|
||||
|
||||
return [
|
||||
'trailer' => $xref['trailer'] ?? ['id' => [], 'info' => '', 'root' => '', 'size' => 0],
|
||||
'xref' => $xref['xref'],
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
$parser->setStubRawObject(['objref', '1_0', 0]);
|
||||
$parser->setStubIndirectObject([
|
||||
[
|
||||
'<<',
|
||||
[
|
||||
['/', 'Type', 0],
|
||||
['/', 'XRef', 0],
|
||||
['/', 'W', 0],
|
||||
['[', [['numeric', '1', 0], ['numeric', '1', 0], ['numeric', '1', 0]], 0],
|
||||
['/', 'Index', 0],
|
||||
['[', [['numeric', '0', 0], ['numeric', '1', 0]], 0],
|
||||
['/', 'Size', 0],
|
||||
['numeric', '1', 0],
|
||||
['/', 'Prev', 0],
|
||||
['numeric', '77', 0],
|
||||
['/', 'DecodeParms', 0],
|
||||
['<<', [['/', 'Columns', 0], ['numeric', '3', 0], ['/', 'Predictor', 0], ['numeric', '10', 0]], 0],
|
||||
],
|
||||
0,
|
||||
],
|
||||
['stream', $stream_data, 0, [$stream_data, []]],
|
||||
]);
|
||||
|
||||
$xref = $parser->decodeXrefStreamPublic(0, ['xref' => []]);
|
||||
|
||||
$this->assertSame(77, $parser->capturedOffset);
|
||||
$this->assertSame(77, $xref['xref']['prev_0'] ?? null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testProcessObjIndexesMapRejectsSparseRowLookup(): void
|
||||
{
|
||||
$parser = new XrefStreamHarness();
|
||||
$xref = [
|
||||
'trailer' => [
|
||||
'id' => [],
|
||||
'info' => '',
|
||||
'root' => '',
|
||||
'size' => 0,
|
||||
],
|
||||
'xref' => [],
|
||||
];
|
||||
|
||||
$this->expectException(PPException::class);
|
||||
$this->expectExceptionMessageContains('Invalid xref stream row at index 2');
|
||||
$parser->processObjIndexesMapPublic($xref, [2 => 10], [[1, 20, 0]]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testProcessSingleObjIndexRejectsMissingMandatoryFields(): void
|
||||
{
|
||||
$parser = new XrefStreamHarness();
|
||||
$xref = [
|
||||
'trailer' => [
|
||||
'id' => [],
|
||||
'info' => '',
|
||||
'root' => '',
|
||||
'size' => 0,
|
||||
],
|
||||
'xref' => [],
|
||||
];
|
||||
|
||||
try {
|
||||
$parser->processSingleObjIndexPublic($xref, 8, [1]);
|
||||
$this->fail('Expected missing offset exception for in-use entry.');
|
||||
} catch (PPException $exception) {
|
||||
$this->assertSame(
|
||||
'Invalid xref stream entry for object 8: missing offset for in-use entry',
|
||||
$exception->getMessage(),
|
||||
);
|
||||
}
|
||||
|
||||
$this->expectException(PPException::class);
|
||||
$this->expectExceptionMessageContains(
|
||||
'Invalid xref stream entry for object 9: missing object stream reference for compressed entry',
|
||||
);
|
||||
$parser->processSingleObjIndexPublic($xref, 9, [2, 55]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testParseXrefIndexSectionsCoversNullAndValidationErrors(): void
|
||||
{
|
||||
$parser = new XrefStreamHarness();
|
||||
|
||||
$this->assertNull($parser->parseXrefIndexSectionsPublic(null));
|
||||
$this->assertNull($parser->parseXrefIndexSectionsPublic(['[', 'oops', 0]));
|
||||
|
||||
try {
|
||||
$parser->parseXrefIndexSectionsPublic(['[', [['name', 'x', 0], ['numeric', '1', 0]], 0]);
|
||||
$this->fail('Expected invalid numeric token exception.');
|
||||
} catch (PPException $exception) {
|
||||
$this->assertSame('Invalid xref stream Index array: expected numeric values', $exception->getMessage());
|
||||
}
|
||||
|
||||
$this->expectException(PPException::class);
|
||||
$this->expectExceptionMessageContains('Invalid xref stream Index array: values must be non-negative');
|
||||
$parser->parseXrefIndexSectionsPublic(['[', [['numeric', '-1', 0], ['numeric', '1', 0]], 0]);
|
||||
}
|
||||
|
||||
public function testBuildXrefObjectNumbersExpandsSections(): void
|
||||
{
|
||||
$parser = new XrefStreamHarness();
|
||||
|
||||
$this->assertSame([3, 4, 5, 10], $parser->buildXrefObjectNumbersPublic([[3, 3], [10, 1]]));
|
||||
$this->assertSame([], $parser->buildXrefObjectNumbersPublic([]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testDecodeXrefStreamThrowsWhenIndexAndSizeAreMissing(): void
|
||||
{
|
||||
$stream_data = \pack('C*', 1, 9, 0);
|
||||
|
||||
$parser = new XrefHarness();
|
||||
$parser->setStubRawObject(['objref', '5_0', 0]);
|
||||
$parser->setStubIndirectObject([
|
||||
[
|
||||
'<<',
|
||||
[
|
||||
['/', 'Type', 0],
|
||||
['/', 'XRef', 0],
|
||||
['/', 'W', 0],
|
||||
['[', [['numeric', '1', 0], ['numeric', '1', 0], ['numeric', '1', 0]], 0],
|
||||
],
|
||||
0,
|
||||
],
|
||||
['stream', $stream_data, 0, [$stream_data, []]],
|
||||
]);
|
||||
|
||||
$this->expectException(PPException::class);
|
||||
$this->expectExceptionMessageContains('Unable to determine xref stream Index coverage: missing Index and Size');
|
||||
$parser->decodeXrefStreamPublic(100, ['xref' => []]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testDecodeXrefStreamWithoutPredictorUsesRawRows(): void
|
||||
{
|
||||
$stream_data = \pack('C*', 1, 9, 0);
|
||||
|
||||
$parser = new XrefHarness();
|
||||
$parser->setStubRawObject(['objref', '5_0', 0]);
|
||||
$parser->setStubIndirectObject([
|
||||
[
|
||||
'<<',
|
||||
[
|
||||
['/', 'Type', 0],
|
||||
['/', 'XRef', 0],
|
||||
['/', 'W', 0],
|
||||
['[', [['numeric', '1', 0], ['numeric', '1', 0], ['numeric', '1', 0]], 0],
|
||||
['/', 'Index', 0],
|
||||
['[', [['numeric', '0', 0], ['numeric', '1', 0]], 0],
|
||||
['/', 'Size', 0],
|
||||
['numeric', '1', 0],
|
||||
],
|
||||
0,
|
||||
],
|
||||
['stream', $stream_data, 0, [$stream_data, []]],
|
||||
]);
|
||||
|
||||
$xref = $parser->decodeXrefStreamPublic(100, ['xref' => []]);
|
||||
$this->assertSame(9, $xref['xref']['0_0'] ?? null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testDecodeXrefBreaksWhenEntryPatternStartsAtDifferentOffset(): void
|
||||
{
|
||||
$pdf = "xref\r\nabc\r\n0 1 n\r\ntrailer << /Size 1 >>\r\n";
|
||||
|
||||
$parser = new XrefHarness();
|
||||
$parser->setPdfDataPublic($pdf);
|
||||
$xref = $parser->decodeXrefPublic(0, ['xref' => ['9_0' => 99]]);
|
||||
|
||||
$this->assertSame(99, $xref['xref']['9_0'] ?? null);
|
||||
$this->assertSame(1, $xref['trailer']['size']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testParseXrefIndexSectionsRejectsNonArrayTokens(): void
|
||||
{
|
||||
$parser = new XrefStreamHarness();
|
||||
|
||||
$this->expectException(PPException::class);
|
||||
$this->expectExceptionMessageContains('Invalid xref stream Index array: expected numeric values');
|
||||
$parser->parseXrefIndexSectionsPublic(['[', [1 => ['numeric', '1', 0], 2 => ['numeric', '2', 0]], 0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
public function testProcessXrefTypeSkipsSlashTokenWithNonStringName(): void
|
||||
{
|
||||
$parser = new XrefStreamHarness();
|
||||
$xref = [
|
||||
'trailer' => [
|
||||
'id' => [],
|
||||
'info' => '',
|
||||
'root' => '',
|
||||
'size' => 0,
|
||||
],
|
||||
'xref' => [],
|
||||
];
|
||||
$wbt = [0, 0, 0];
|
||||
$state = [
|
||||
'index_sections' => null,
|
||||
'prevxref' => null,
|
||||
'predictor' => 0,
|
||||
'columns' => 0,
|
||||
'size' => null,
|
||||
'valid_crs' => false,
|
||||
];
|
||||
|
||||
$parser->processXrefTypePublic(
|
||||
[
|
||||
['/', [['numeric', '1', 0]], 0],
|
||||
['numeric', '7', 0],
|
||||
],
|
||||
$xref,
|
||||
$wbt,
|
||||
$state,
|
||||
true,
|
||||
);
|
||||
|
||||
$this->assertSame([], $xref['xref']);
|
||||
$this->assertFalse($state['valid_crs']);
|
||||
}
|
||||
|
||||
public function testProcessXrefObjrefReturnsWhenObjrefValueIsNotString(): void
|
||||
{
|
||||
$parser = new XrefStreamHarness();
|
||||
$xref = [
|
||||
'trailer' => [
|
||||
'id' => [],
|
||||
'info' => '',
|
||||
'root' => '',
|
||||
'size' => 0,
|
||||
],
|
||||
'xref' => [],
|
||||
];
|
||||
|
||||
$result = $parser->processXrefObjrefPublic(
|
||||
'Root',
|
||||
[
|
||||
['/', 'Root', 0],
|
||||
['objref', [['numeric', '1', 0]], 0],
|
||||
],
|
||||
0,
|
||||
$xref,
|
||||
);
|
||||
|
||||
$this->assertSame($xref, $result);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user