generated from jric11/baseProject
Initial commit
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Exception.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 Com\Tecnick\Pdf\Parser;
|
||||
|
||||
/**
|
||||
* Com\Tecnick\Pdf\Parser\Exception
|
||||
*
|
||||
* Custom Exception class
|
||||
*
|
||||
* @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 Exception extends \Exception {}
|
||||
+1039
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,434 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* RawObject.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 Com\Tecnick\Pdf\Parser\Process;
|
||||
|
||||
/**
|
||||
* Com\Tecnick\Pdf\Parser\Process\RawObject
|
||||
*
|
||||
* Process Raw Objects
|
||||
*
|
||||
* @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
|
||||
*
|
||||
* @phpstan-type RawObjectArray array{
|
||||
* 0: string,
|
||||
* 1: string|array<int, array{
|
||||
* 0: string,
|
||||
* 1: string|array<int, array{
|
||||
* 0: string,
|
||||
* 1: string|array<int, array{
|
||||
* 0: string,
|
||||
* 1: string|array<int, array{
|
||||
* 0: string,
|
||||
* 1: string|array<int, array{
|
||||
* 0: string,
|
||||
* 1: string,
|
||||
* 2: int,
|
||||
* 3?: array{string, array<string>},
|
||||
* }>,
|
||||
* 2: int,
|
||||
* 3?: array{string, array<string>},
|
||||
* }>,
|
||||
* 2: int,
|
||||
* 3?: array{string, array<string>},
|
||||
* }>,
|
||||
* 2: int,
|
||||
* 3?: array{string, array<string>},
|
||||
* }>,
|
||||
* 2: int,
|
||||
* 3?: array{string, array<string>},
|
||||
* }>,
|
||||
* 2: int,
|
||||
* 3?: array{string, array<string>},
|
||||
* }
|
||||
*
|
||||
* @phpstan-type RawObjectValue string|array<int, array{
|
||||
* 0: string,
|
||||
* 1: string|array<int, array{
|
||||
* 0: string,
|
||||
* 1: string,
|
||||
* 2: int,
|
||||
* }>,
|
||||
* 2: int,
|
||||
* }>
|
||||
*/
|
||||
abstract class RawObject
|
||||
{
|
||||
/**
|
||||
* Raw content of the PDF document.
|
||||
*/
|
||||
protected string $pdfdata = '';
|
||||
|
||||
/**
|
||||
* Offset where the data of the most recently tokenized stream begins
|
||||
* (i.e. just after the "stream" keyword and its end-of-line marker).
|
||||
*/
|
||||
protected int $streamDataStart = 0;
|
||||
|
||||
/**
|
||||
* Whether to apply the PNG/TIFF predictor from DecodeParms when decoding a stream.
|
||||
*
|
||||
* This is temporarily disabled while the xref machinery decodes a cross-reference
|
||||
* stream, because that path un-predicts the rows itself.
|
||||
*/
|
||||
protected bool $applyStreamPredictor = true;
|
||||
|
||||
/**
|
||||
* Array of PDF objects.
|
||||
*
|
||||
* @var array<string, array<int, RawObjectArray>>
|
||||
*/
|
||||
protected array $objects = [];
|
||||
|
||||
/**
|
||||
* Map symbols with corresponding processing methods.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected const SYMBOLMETHOD = [
|
||||
// \x2F SOLIDUS
|
||||
'/' => 'Solidus',
|
||||
// \x28 LEFT PARENTHESIS
|
||||
'(' => 'Parenthesis',
|
||||
// \x29 RIGHT PARENTHESIS
|
||||
')' => 'Parenthesis',
|
||||
// \x5B LEFT SQUARE BRACKET
|
||||
'[' => 'Bracket',
|
||||
// \x5D RIGHT SQUARE BRACKET
|
||||
']' => 'Bracket',
|
||||
// \x3C LESS-THAN SIGN
|
||||
'<' => 'Angular',
|
||||
// \x3E GREATER-THAN SIGN
|
||||
'>' => 'Angular',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get object type, raw value and offset to next object
|
||||
*
|
||||
* @param int $offset Object offset.
|
||||
*
|
||||
* @return RawObjectArray Array containing: object type, raw value and offset to next object
|
||||
*/
|
||||
protected function getRawObject(int $offset = 0): array
|
||||
{
|
||||
// skip initial white space chars:
|
||||
// \x00 null (NUL)
|
||||
// \x09 horizontal tab (HT)
|
||||
// \x0A line feed (LF)
|
||||
// \x0C form feed (FF)
|
||||
// \x0D carriage return (CR)
|
||||
// \x20 space (SP)
|
||||
$offset += \strspn($this->pdfdata, "\x00\x09\x0a\x0c\x0d\x20", $offset);
|
||||
// stop if we reached the end of the (possibly truncated/malformed) data
|
||||
if ($offset >= \strlen($this->pdfdata)) {
|
||||
return ['', '', $offset];
|
||||
}
|
||||
|
||||
// get first char
|
||||
$char = $this->pdfdata[$offset];
|
||||
if ($char === '%') { // \x25 PERCENT SIGN
|
||||
// skip comment and search for next token
|
||||
$next = \strcspn($this->pdfdata, "\r\n", $offset);
|
||||
if ($next > 0) {
|
||||
$offset += $next;
|
||||
return $this->getRawObject($offset);
|
||||
}
|
||||
}
|
||||
|
||||
$objtype = '';
|
||||
$objval = '';
|
||||
// map symbols with corresponding processing methods
|
||||
$methodSuffix = self::SYMBOLMETHOD[$char] ?? null;
|
||||
if (\is_string($methodSuffix)) {
|
||||
$method = 'process' . $methodSuffix;
|
||||
$this->$method($char, $offset, $objtype, $objval);
|
||||
} elseif (!$this->processDefaultName($offset, $objtype, $objval)) {
|
||||
$this->processDefault($offset, $objtype, $objval);
|
||||
}
|
||||
|
||||
return [$objtype, $objval, $offset];
|
||||
}
|
||||
|
||||
/**
|
||||
* Process name object
|
||||
* \x2F SOLIDUS
|
||||
*
|
||||
* @param string $char Symbol to process
|
||||
* @param-out int $offset Offset after processing
|
||||
* @param-out string $objtype Object type after processing
|
||||
* @param-out string $objval Object content after processing
|
||||
*/
|
||||
protected function processSolidus(string $char, int &$offset, string &$objtype, string|array &$objval): void
|
||||
{
|
||||
$objtype = $char;
|
||||
++$offset;
|
||||
$matches = [];
|
||||
if (
|
||||
\preg_match(
|
||||
'/^([^\x00\x09\x0a\x0c\x0d\x20\s\x28\x29\x3c\x3e\x5b\x5d\x7b\x7d\x2f\x25]+)/',
|
||||
\substr($this->pdfdata, $offset, 256),
|
||||
$matches,
|
||||
) === 1
|
||||
&& ($matches[1] ?? null) !== null
|
||||
) {
|
||||
$objval = $matches[1]; // unescaped value
|
||||
$offset += \strlen($objval);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process literal string object
|
||||
* \x28 LEFT PARENTHESIS and \x29 RIGHT PARENTHESIS
|
||||
*
|
||||
* @param string $char Symbol to process
|
||||
* @param-out int $offset Offset after processing
|
||||
* @param-out string $objtype Object type after processing
|
||||
* @param-out string $objval Object content after processing
|
||||
*/
|
||||
protected function processParenthesis(string $char, int &$offset, string &$objtype, string|array &$objval): void
|
||||
{
|
||||
$objtype = $char;
|
||||
++$offset;
|
||||
$strpos = $offset;
|
||||
if ($char === '(') {
|
||||
$open_bracket = 1;
|
||||
$pdflen = \strlen($this->pdfdata);
|
||||
while ($open_bracket > 0 && $strpos < $pdflen) {
|
||||
$chr = $this->pdfdata[$strpos];
|
||||
switch ($chr) {
|
||||
case '\\':
|
||||
// REVERSE SOLIDUS (5Ch) (Backslash)
|
||||
// skip next character
|
||||
++$strpos;
|
||||
break;
|
||||
case '(':
|
||||
// LEFT PARENTHESIS (28h)
|
||||
++$open_bracket;
|
||||
break;
|
||||
case ')':
|
||||
// RIGHT PARENTHESIS (29h)
|
||||
--$open_bracket;
|
||||
break;
|
||||
}
|
||||
|
||||
++$strpos;
|
||||
}
|
||||
|
||||
$objval = \substr($this->pdfdata, $offset, $strpos - $offset - 1);
|
||||
$offset = $strpos;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process array content
|
||||
* \x5B LEFT SQUARE BRACKET and \x5D RIGHT SQUARE BRACKET
|
||||
*
|
||||
* @param string $char Symbol to process
|
||||
* @param-out int $offset Offset after processing
|
||||
* @param-out string $objtype Object type after processing
|
||||
* @param-out string|array $objval Object content after processing
|
||||
*/
|
||||
protected function processBracket(string $char, int &$offset, string &$objtype, string|array &$objval): void
|
||||
{
|
||||
// array object
|
||||
$objtype = $char;
|
||||
++$offset;
|
||||
if ($char === '[') {
|
||||
// get array content
|
||||
$objval = [];
|
||||
do {
|
||||
$oldoffset = $offset;
|
||||
$element = $this->getRawObject($offset);
|
||||
$offset = $element[2];
|
||||
$objval[] = $element;
|
||||
} while ($element[0] !== ']' && (int) $offset !== (int) $oldoffset);
|
||||
|
||||
if (\count($objval) > 0) {
|
||||
// remove closing delimiter
|
||||
\array_pop($objval);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process \x3C LESS-THAN SIGN and \x3E GREATER-THAN SIGN
|
||||
*
|
||||
* @param string $char Symbol to process
|
||||
* @param-out int $offset Offset after processing
|
||||
* @param-out string $objtype Object type after processing
|
||||
* @param-out string|array $objval Object content after processing
|
||||
*/
|
||||
protected function processAngular(string $char, int &$offset, string &$objtype, string|array &$objval): void
|
||||
{
|
||||
if (($this->pdfdata[$offset + 1] ?? null) === $char) {
|
||||
// dictionary object
|
||||
$objtype = $char . $char;
|
||||
$offset += 2;
|
||||
if ($char === '<') {
|
||||
// get array content
|
||||
$objval = [];
|
||||
do {
|
||||
$oldoffset = $offset;
|
||||
$element = $this->getRawObject($offset);
|
||||
$offset = $element[2];
|
||||
$objval[] = $element;
|
||||
} while ($element[0] !== '>>' && (int) $offset !== (int) $oldoffset);
|
||||
|
||||
if (\count($objval) > 0) {
|
||||
// remove closing delimiter
|
||||
\array_pop($objval);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// hexadecimal string object
|
||||
$objtype = $char;
|
||||
++$offset;
|
||||
$matches = [];
|
||||
if (
|
||||
$char === '<'
|
||||
&& \preg_match('/^([0-9A-Fa-f\x09\x0a\x0c\x0d\x20]+)>/iU', \substr($this->pdfdata, $offset), $matches)
|
||||
=== 1
|
||||
&& ($matches[0] ?? null) !== null
|
||||
&& ($matches[1] ?? null) !== null
|
||||
) {
|
||||
// remove white space characters
|
||||
$objval = \strtr($matches[1], "\x09\x0a\x0c\x0d\x20", '');
|
||||
$offset += \strlen($matches[0]);
|
||||
} elseif (($endpos = \strpos($this->pdfdata, '>', $offset)) !== false) {
|
||||
$offset = $endpos + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process default
|
||||
*
|
||||
* @param-out int $offset Offset after processing
|
||||
* @param-out string $objtype Object type after processing
|
||||
* @param-out RawObjectValue $objval Object content after processing
|
||||
*
|
||||
* @return bool True in case of match, false otherwise
|
||||
*/
|
||||
protected function processDefaultName(int &$offset, string &$objtype, string|array &$objval): bool
|
||||
{
|
||||
$status = false;
|
||||
if (\substr($this->pdfdata, $offset, 6) === 'endobj') {
|
||||
// indirect object
|
||||
$objtype = 'endobj';
|
||||
$offset += 6;
|
||||
$status = true;
|
||||
} elseif (\substr($this->pdfdata, $offset, 4) === 'null') {
|
||||
// null object
|
||||
$objtype = 'null';
|
||||
$offset += 4;
|
||||
$objval = 'null';
|
||||
$status = true;
|
||||
} elseif (\substr($this->pdfdata, $offset, 4) === 'true') {
|
||||
// boolean true object
|
||||
$objtype = 'boolean';
|
||||
$offset += 4;
|
||||
$objval = 'true';
|
||||
$status = true;
|
||||
} elseif (\substr($this->pdfdata, $offset, 5) === 'false') {
|
||||
// boolean false object
|
||||
$objtype = 'boolean';
|
||||
$offset += 5;
|
||||
$objval = 'false';
|
||||
$status = true;
|
||||
} elseif (\substr($this->pdfdata, $offset, 6) === 'stream') {
|
||||
// start stream object
|
||||
$objtype = 'stream';
|
||||
$offset += 6;
|
||||
$matches = [];
|
||||
if (
|
||||
\preg_match('/^([\r]?[\n])/isU', \substr($this->pdfdata, $offset), $matches) === 1
|
||||
&& ($matches[0] ?? null) !== null
|
||||
) {
|
||||
$offset += \strlen($matches[0]);
|
||||
// record where the stream payload starts (used for length-aware extraction)
|
||||
$this->streamDataStart = $offset;
|
||||
if (
|
||||
\preg_match(
|
||||
'/(endstream)[\x09\x0a\x0c\x0d\x20]/isU',
|
||||
\substr($this->pdfdata, $offset),
|
||||
$matches,
|
||||
PREG_OFFSET_CAPTURE,
|
||||
) === 1
|
||||
&& ($matches[0] ?? null) !== null
|
||||
&& ($matches[1] ?? null) !== null
|
||||
) {
|
||||
$objval = \substr($this->pdfdata, $offset, (int) $matches[0][1]);
|
||||
$offset += (int) $matches[1][1];
|
||||
}
|
||||
}
|
||||
|
||||
$status = true;
|
||||
} elseif (\substr($this->pdfdata, $offset, 9) === 'endstream') {
|
||||
// end stream object
|
||||
$objtype = 'endstream';
|
||||
$offset += 9;
|
||||
$status = true;
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process default
|
||||
*
|
||||
* @param-out int $offset Offset after processing
|
||||
* @param-out string $objtype Object type after processing
|
||||
* @param-out RawObjectValue $objval Object content after processing
|
||||
*/
|
||||
protected function processDefault(int &$offset, string &$objtype, string|array &$objval): void
|
||||
{
|
||||
$matches = [];
|
||||
if (
|
||||
\preg_match('/^([0-9]+)[\s]+([0-9]+)[\s]+R/iU', \substr($this->pdfdata, $offset, 33), $matches) === 1
|
||||
&& ($matches[0] ?? null) !== null
|
||||
&& ($matches[1] ?? null) !== null
|
||||
&& ($matches[2] ?? null) !== null
|
||||
) {
|
||||
// indirect object reference
|
||||
$objtype = 'objref';
|
||||
$offset += \strlen($matches[0]);
|
||||
$objval = (int) $matches[1] . '_' . (int) $matches[2];
|
||||
} elseif (
|
||||
\preg_match('/^([0-9]+)[\s]+([0-9]+)[\s]+obj/iU', \substr($this->pdfdata, $offset, 33), $matches) === 1
|
||||
&& ($matches[0] ?? null) !== null
|
||||
&& ($matches[1] ?? null) !== null
|
||||
&& ($matches[2] ?? null) !== null
|
||||
) {
|
||||
// object start
|
||||
$objtype = 'obj';
|
||||
$objval = (int) $matches[1] . '_' . (int) $matches[2];
|
||||
$offset += \strlen($matches[0]);
|
||||
} elseif (($numlen = \strspn($this->pdfdata, '+-.0123456789', $offset)) > 0) {
|
||||
// numeric object
|
||||
$objtype = 'numeric';
|
||||
$objval = \substr($this->pdfdata, $offset, $numlen);
|
||||
$offset += $numlen;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,546 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Xref.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 Com\Tecnick\Pdf\Parser\Process;
|
||||
|
||||
use Com\Tecnick\Pdf\Parser\Exception as PPException;
|
||||
|
||||
/**
|
||||
* Com\Tecnick\Pdf\Parser\Process\Xref
|
||||
*
|
||||
* Process XREF
|
||||
*
|
||||
* @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
|
||||
*
|
||||
* @phpstan-import-type RawObjectArray from \Com\Tecnick\Pdf\Parser\Process\RawObject
|
||||
* @phpstan-import-type XrefData from \Com\Tecnick\Pdf\Parser\Process\XrefStream
|
||||
*
|
||||
* @phpstan-type XrefDataPartial array{
|
||||
* 'trailer'?: array{
|
||||
* 'encrypt'?: string,
|
||||
* 'id': array<int, string>,
|
||||
* 'info': string,
|
||||
* 'root': string,
|
||||
* 'size': int,
|
||||
* },
|
||||
* 'xref': array<string, int|string>,
|
||||
* }
|
||||
*
|
||||
* @phpstan-type XrefDataInput array{
|
||||
* 'trailer'?: array{
|
||||
* 'encrypt'?: string,
|
||||
* 'id': array<int, string>,
|
||||
* 'info': string,
|
||||
* 'root': string,
|
||||
* 'size': int,
|
||||
* },
|
||||
* 'xref'?: array<string, int|string>,
|
||||
* }
|
||||
*/
|
||||
abstract class Xref extends \Com\Tecnick\Pdf\Parser\Process\XrefStream
|
||||
{
|
||||
/**
|
||||
* Default empty XREF data.
|
||||
*
|
||||
* @var XrefData
|
||||
*/
|
||||
protected const XREF_EMPTY = [
|
||||
'trailer' => [
|
||||
'id' => [],
|
||||
'info' => '',
|
||||
'root' => '',
|
||||
'size' => 0,
|
||||
],
|
||||
'xref' => [],
|
||||
];
|
||||
|
||||
/**
|
||||
* XREF data.
|
||||
*
|
||||
* @var XrefData
|
||||
*/
|
||||
protected array $xref = self::XREF_EMPTY;
|
||||
|
||||
/**
|
||||
* Store the processed offsets
|
||||
*
|
||||
* @var array<int, int>
|
||||
*/
|
||||
protected array $mrkoff = [];
|
||||
|
||||
/**
|
||||
* Get content of indirect object.
|
||||
*
|
||||
* @param string $obj_ref Object number and generation number separated by underscore character.
|
||||
* @param int $offset Object offset.
|
||||
* @param bool $decoding If true decode streams.
|
||||
*
|
||||
* @return array<int, RawObjectArray> Object data.
|
||||
*/
|
||||
abstract protected function getIndirectObject(string $obj_ref, int $offset = 0, bool $decoding = true): array;
|
||||
|
||||
/**
|
||||
* Get Cross-Reference (xref) table and trailer data from PDF document data.
|
||||
*
|
||||
* @param int $offset Xref offset (if known).
|
||||
* @param XrefDataInput $xref Previous xref array (if any).
|
||||
*
|
||||
* @return XrefData Xref and trailer data.
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
* @SuppressWarnings("PHPMD.CyclomaticComplexity")
|
||||
*/
|
||||
protected function getXrefData(int $offset = 0, array $xref = []): array
|
||||
{
|
||||
if (\in_array($offset, $this->mrkoff, true)) {
|
||||
throw new PPException('LOOP: this XRef offset has been already processed');
|
||||
}
|
||||
|
||||
$this->mrkoff[] = $offset;
|
||||
$matches = [];
|
||||
if ($offset === 0) {
|
||||
// find last startxref
|
||||
$matchCount = \preg_match_all(
|
||||
'/[\r\n]startxref[\s]*[\r\n]+([0-9]+)[\s]*[\r\n]+%%EOF/i',
|
||||
$this->pdfdata,
|
||||
$matches,
|
||||
PREG_SET_ORDER,
|
||||
$offset,
|
||||
);
|
||||
if ($matchCount === false || $matchCount === 0) {
|
||||
throw new PPException('Unable to find startxref (1)');
|
||||
}
|
||||
|
||||
$matches = \array_pop($matches);
|
||||
$startxref = (int) ($matches[1] ?? 0);
|
||||
} elseif (($pos = \strpos($this->pdfdata, 'xref', $offset)) !== false && $pos <= ($offset + 4)) {
|
||||
// Already pointing at the xref table
|
||||
$startxref = $pos;
|
||||
} elseif (\preg_match('/([0-9]+[\s][0-9]+[\s]obj)/i', $this->pdfdata, $matches, PREG_OFFSET_CAPTURE, $offset)) {
|
||||
// Cross-Reference Stream object
|
||||
$startxref = (int) $offset;
|
||||
} elseif (\preg_match(
|
||||
'/[\r\n]startxref[\s]*[\r\n]+([0-9]+)[\s]*[\r\n]+%%EOF/i',
|
||||
$this->pdfdata,
|
||||
$matches,
|
||||
PREG_OFFSET_CAPTURE,
|
||||
$offset,
|
||||
)) {
|
||||
// startxref found
|
||||
$startxref = (int) ($matches[1][0] ?? 0);
|
||||
} else {
|
||||
throw new PPException('Unable to find startxref (3)');
|
||||
}
|
||||
|
||||
$xref['xref'] ??= [];
|
||||
|
||||
// check xref position (allow leading whitespace before the xref keyword)
|
||||
$xrefPos = \strpos($this->pdfdata, 'xref', $startxref);
|
||||
$hasPlainXref = false;
|
||||
if ($xrefPos !== false && $xrefPos >= $startxref) {
|
||||
$prefixLen = $xrefPos - $startxref;
|
||||
$prefix = $prefixLen > 0 ? \substr($this->pdfdata, $startxref, $prefixLen) : '';
|
||||
if (\trim($prefix, "\x00\x09\x0a\x0c\x0d\x20") === '') {
|
||||
$hasPlainXref = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($hasPlainXref) {
|
||||
// Cross-Reference
|
||||
$xrefStart = \is_int($xrefPos) ? $xrefPos : $startxref;
|
||||
$xref = $this->decodeXref($xrefStart, $xref);
|
||||
} else {
|
||||
// Cross-Reference Stream
|
||||
$xref = $this->decodeXrefStream($startxref, $xref);
|
||||
}
|
||||
|
||||
if ($xref['xref'] === []) {
|
||||
throw new PPException('Unable to find xref (4)');
|
||||
}
|
||||
|
||||
return $xref;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the Cross-Reference section
|
||||
*
|
||||
* @param int $startxref Offset at which the xref section starts (position of the 'xref' keyword).
|
||||
* @param XrefDataPartial $xref Previous xref array (if any).
|
||||
*
|
||||
* @return XrefData Xref and trailer data.
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
protected function decodeXref(int $startxref, array $xref): array
|
||||
{
|
||||
$startxref += 4; // 4 is the length of the word 'xref'
|
||||
// skip initial white space chars:
|
||||
// \x00 null (NUL)
|
||||
// \x09 horizontal tab (HT)
|
||||
// \x0A line feed (LF)
|
||||
// \x0C form feed (FF)
|
||||
// \x0D carriage return (CR)
|
||||
// \x20 space (SP)
|
||||
$offset = $startxref + \strspn($this->pdfdata, "\x00\x09\x0a\x0c\x0d\x20", $startxref);
|
||||
// initialize object number
|
||||
$obj_num = 0;
|
||||
$matches = [];
|
||||
// search for cross-reference entries or subsection
|
||||
while (
|
||||
\preg_match(
|
||||
'/(\d+)[\x20](\d+)[\x20]?([nf]?)(\r\n|[\x20]?[\r\n])/',
|
||||
$this->pdfdata,
|
||||
$matches,
|
||||
PREG_OFFSET_CAPTURE,
|
||||
$offset,
|
||||
) === 1
|
||||
) {
|
||||
$matchOffset = (int) ($matches[0][1] ?? -1);
|
||||
if ($matchOffset !== $offset) {
|
||||
// we are on another section
|
||||
break;
|
||||
}
|
||||
|
||||
$offset += \strlen($matches[0][0] ?? '');
|
||||
$flag = $matches[3][0] ?? '';
|
||||
if ($flag === 'n') {
|
||||
// create unique object index: [object number]_[generation number]
|
||||
$index = $obj_num . '_' . (int) ($matches[2][0] ?? 0);
|
||||
// check if object already exist
|
||||
if (!\array_key_exists($index, $xref['xref'])) {
|
||||
// store object offset position
|
||||
$xref['xref'][$index] = (int) ($matches[1][0] ?? 0);
|
||||
}
|
||||
|
||||
++$obj_num;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($flag === 'f') {
|
||||
++$obj_num;
|
||||
continue;
|
||||
}
|
||||
|
||||
// object number (index) - handle any other character (free object)
|
||||
$obj_num = (int) ($matches[1][0] ?? 0);
|
||||
}
|
||||
|
||||
// get trailer data (using a depth-aware scan so nested dictionaries are not truncated)
|
||||
$trailerData = $this->extractTrailerDict($offset);
|
||||
if ($trailerData === null) {
|
||||
throw new PPException('Unable to find trailer');
|
||||
}
|
||||
|
||||
return $this->getTrailerData($xref, $trailerData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the content of the trailer dictionary, honouring nested "<<...>>" pairs.
|
||||
*
|
||||
* @param int $offset Offset from which to look for the 'trailer' keyword.
|
||||
*
|
||||
* @return string|null The dictionary content (without the outer delimiters), or null if not found.
|
||||
*/
|
||||
protected function extractTrailerDict(int $offset): ?string
|
||||
{
|
||||
$search = $offset;
|
||||
while (($trpos = \strpos($this->pdfdata, 'trailer', $search)) !== false) {
|
||||
$search = $trpos + 7; // 7 is the length of the word 'trailer'
|
||||
// only whitespace is allowed between the 'trailer' keyword and the '<<' delimiter
|
||||
$open = $search + \strspn($this->pdfdata, "\x00\x09\x0a\x0c\x0d\x20", $search);
|
||||
if (\substr($this->pdfdata, $open, 2) !== '<<') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$content = $this->extractBalancedDict($open);
|
||||
if ($content !== null) {
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the content between the outermost balanced "<<" and ">>" starting at $open.
|
||||
*
|
||||
* @param int $open Offset of the opening '<<'.
|
||||
*
|
||||
* @return string|null Inner content, or null if the dictionary is not balanced.
|
||||
*/
|
||||
protected function extractBalancedDict(int $open): ?string
|
||||
{
|
||||
$len = \strlen($this->pdfdata);
|
||||
$depth = 0;
|
||||
$contentStart = $open + 2;
|
||||
for ($pos = $open; $pos < ($len - 1); ++$pos) {
|
||||
$pair = $this->pdfdata[$pos] . $this->pdfdata[$pos + 1];
|
||||
if ($pair === '<<') {
|
||||
++$depth;
|
||||
++$pos;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($pair === '>>') {
|
||||
--$depth;
|
||||
if ($depth === 0) {
|
||||
return \substr($this->pdfdata, $contentStart, $pos - $contentStart);
|
||||
}
|
||||
|
||||
++$pos;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the raw xref stream rows into xref entry values.
|
||||
*
|
||||
* @param string $streamData Raw stream payload.
|
||||
* @param int $entryWidth Width of a decoded row without PNG predictor metadata.
|
||||
* @param int $pngColumns Column count to use when a PNG predictor is active.
|
||||
* @param bool $usePngPredictor Whether to apply PNG unprediction.
|
||||
* @param array<int, int> $wbt WBT data.
|
||||
*
|
||||
* @return array<int, array<int, int>>
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
protected function decodeXrefStreamRows(
|
||||
string $streamData,
|
||||
int $entryWidth,
|
||||
int $pngColumns,
|
||||
bool $usePngPredictor,
|
||||
array $wbt,
|
||||
): array {
|
||||
$rowlen = $usePngPredictor ? $pngColumns + 1 : $entryWidth;
|
||||
|
||||
// convert the stream into an array of integers
|
||||
$sdata = \unpack('C*', $streamData);
|
||||
|
||||
// split the rows
|
||||
$sdata = \array_chunk($sdata, \max(1, $rowlen), false);
|
||||
|
||||
if ($usePngPredictor) {
|
||||
// initialize decoded array
|
||||
$ddata = [];
|
||||
// initialize first row with zeros
|
||||
$filllen = \max(0, $pngColumns);
|
||||
$prev_row = \array_fill(0, $filllen, 0);
|
||||
$this->pngUnpredictor($sdata, $ddata, $pngColumns, $prev_row);
|
||||
} else {
|
||||
$ddata = $sdata;
|
||||
}
|
||||
|
||||
// complete decoding
|
||||
$sdata = [];
|
||||
$this->processDdata($sdata, $ddata, $wbt);
|
||||
|
||||
return $sdata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the PDF trailer dictionary data
|
||||
*
|
||||
* @param XrefDataPartial $xref Previous xref array (if any).
|
||||
* @param string $trailer_data Trailer content string.
|
||||
*
|
||||
* @return XrefData Xref and trailer data.
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
protected function getTrailerData(array $xref, string $trailer_data): array
|
||||
{
|
||||
$matches = [];
|
||||
if (($xref['trailer'] ?? []) === []) {
|
||||
// get only the last updated version
|
||||
$xref['trailer'] = [
|
||||
'id' => [],
|
||||
'info' => '',
|
||||
'root' => '',
|
||||
'size' => 0,
|
||||
];
|
||||
|
||||
// parse trailer_data
|
||||
if (\preg_match('/Size[\s]+([0-9]+)/i', $trailer_data, $matches) === 1) {
|
||||
$xref['trailer']['size'] = (int) ($matches[1] ?? 0);
|
||||
}
|
||||
|
||||
if (\preg_match('/Root[\s]+([0-9]+)[\s]+([0-9]+)[\s]+R/i', $trailer_data, $matches) === 1) {
|
||||
$xref['trailer']['root'] = (int) ($matches[1] ?? 0) . '_' . (int) ($matches[2] ?? 0);
|
||||
}
|
||||
|
||||
if (\preg_match('/Encrypt[\s]+([0-9]+)[\s]+([0-9]+)[\s]+R/i', $trailer_data, $matches) === 1) {
|
||||
$xref['trailer']['encrypt'] = (int) ($matches[1] ?? 0) . '_' . (int) ($matches[2] ?? 0);
|
||||
}
|
||||
|
||||
if (\preg_match('/Info[\s]+([0-9]+)[\s]+([0-9]+)[\s]+R/i', $trailer_data, $matches) === 1) {
|
||||
$xref['trailer']['info'] = (int) ($matches[1] ?? 0) . '_' . (int) ($matches[2] ?? 0);
|
||||
}
|
||||
|
||||
if (\preg_match('/ID[\s]*+[\[][\s]*+[<]([^>]*+)[>][\s]*+[<]([^>]*+)[>]/i', $trailer_data, $matches) === 1) {
|
||||
/** @var array<int, string> $id_array */
|
||||
$id_array = [$matches[1] ?? '', $matches[2] ?? ''];
|
||||
$xref['trailer']['id'] = $id_array;
|
||||
}
|
||||
}
|
||||
|
||||
if (\preg_match('/Prev[\s]+([0-9]+)/i', $trailer_data, $matches) === 1) {
|
||||
// get previous xref
|
||||
return $this->getXrefData((int) ($matches[1] ?? 0), $xref);
|
||||
}
|
||||
|
||||
$xref['trailer'] ??= self::XREF_EMPTY['trailer'];
|
||||
|
||||
return $xref;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the Cross-Reference Stream section
|
||||
*
|
||||
* @param int $startxref Offset at which the xref section starts.
|
||||
* @param XrefDataPartial $xref Previous xref array (if any).
|
||||
*
|
||||
* @return XrefData Xref and trailer data.
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
* @SuppressWarnings("PHPMD.CyclomaticComplexity")
|
||||
*/
|
||||
protected function decodeXrefStream(int $startxref, array $xref): array
|
||||
{
|
||||
// try to read Cross-Reference Stream
|
||||
$xrefobj = $this->getRawObject($startxref);
|
||||
if (!\is_string($xrefobj[1])) {
|
||||
throw new PPException('Unable to find xref stream');
|
||||
}
|
||||
|
||||
// the xref rows are un-predicted by this class itself, so suppress the generic
|
||||
// predictor handling while decoding the cross-reference stream payload
|
||||
$this->applyStreamPredictor = false;
|
||||
try {
|
||||
$xrefcrs = $this->getIndirectObject($xrefobj[1], $startxref, true);
|
||||
} finally {
|
||||
$this->applyStreamPredictor = true;
|
||||
}
|
||||
|
||||
$filltrailer = ($xref['trailer'] ?? []) === [];
|
||||
if ($filltrailer) {
|
||||
$xref['trailer'] = self::XREF_EMPTY['trailer'];
|
||||
}
|
||||
|
||||
$valid_crs = false;
|
||||
$columns = 0;
|
||||
$predictor = 0;
|
||||
$sarr = $xrefcrs[0][1] ?? [];
|
||||
/** @var array<int, array>|string $sarr */
|
||||
if (!\is_array($sarr)) {
|
||||
$sarr = [];
|
||||
}
|
||||
|
||||
/** @var array<int, RawObjectArray> $sarr */
|
||||
/** @var XrefData $xref */
|
||||
|
||||
$wbt = [0, 0, 0];
|
||||
$state = [
|
||||
'index_sections' => null,
|
||||
'prevxref' => null,
|
||||
'predictor' => $predictor,
|
||||
'columns' => $columns,
|
||||
'size' => null,
|
||||
'valid_crs' => $valid_crs,
|
||||
];
|
||||
$this->processXrefType($sarr, $xref, $wbt, $state, $filltrailer);
|
||||
$index_sections = $state['index_sections'];
|
||||
$size = $state['size'];
|
||||
$columns = $state['columns'];
|
||||
$predictor = (int) $state['predictor'];
|
||||
$valid_crs = $state['valid_crs'];
|
||||
// decode data
|
||||
$streamData = $xrefcrs[1][3][0] ?? null;
|
||||
if ($valid_crs && \is_string($streamData)) {
|
||||
$entryWidth = \max(1, (int) (($wbt[0] ?? 0) + ($wbt[1] ?? 0) + ($wbt[2] ?? 0)));
|
||||
$usePngPredictor = $predictor >= 10;
|
||||
$pngColumns = $columns > 0 ? $columns : $entryWidth;
|
||||
$sdata = $this->decodeXrefStreamRows($streamData, $entryWidth, $pngColumns, $usePngPredictor, $wbt);
|
||||
$ddata = [];
|
||||
if ($index_sections === null) {
|
||||
if ($size === null) {
|
||||
throw new PPException('Unable to determine xref stream Index coverage: missing Index and Size');
|
||||
}
|
||||
|
||||
$index_sections = [[0, $size]];
|
||||
}
|
||||
|
||||
$objNumbers = $this->buildXrefObjectNumbers($index_sections);
|
||||
$this->processObjIndexesMap($xref, $objNumbers, $sdata);
|
||||
}
|
||||
|
||||
// end decoding data
|
||||
$prevxref = $state['prevxref'];
|
||||
if ($prevxref === null) {
|
||||
return $xref;
|
||||
}
|
||||
|
||||
// get previous xref
|
||||
return $this->getXrefData($prevxref, $xref);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process ddata
|
||||
*
|
||||
* @param array<int, array<int, int>> $sdata
|
||||
* @param array<int, array<int, int>> $ddata
|
||||
* @param array<int, int> $wbt
|
||||
*/
|
||||
protected function processDdata(array &$sdata, array $ddata, array $wbt): void
|
||||
{
|
||||
// for every row
|
||||
foreach ($ddata as $key => $row) {
|
||||
// initialize new row
|
||||
$sdata[$key] = [0, 0, 0];
|
||||
if (($wbt[0] ?? 0) === 0) {
|
||||
// default type field
|
||||
$sdata[$key][0] = 1;
|
||||
}
|
||||
|
||||
$idx = 0; // count bytes in the row
|
||||
// for every column
|
||||
for ($col = 0; $col < 3; ++$col) {
|
||||
// for every byte on the column
|
||||
$colWidth = (int) ($wbt[$col] ?? 0);
|
||||
for ($byte = 0; $byte < $colWidth; ++$byte) {
|
||||
if (\array_key_exists($idx, $row)) {
|
||||
$rowValue = (int) $row[$idx];
|
||||
$currentValue = (int) ($sdata[$key][$col] ?? 0);
|
||||
$shift = ($colWidth - 1 - $byte) * 8;
|
||||
$sdata[$key][$col] = $currentValue + ($rowValue << $shift);
|
||||
}
|
||||
|
||||
++$idx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,512 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* XrefStream.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 Com\Tecnick\Pdf\Parser\Process;
|
||||
|
||||
use Com\Tecnick\Pdf\Parser\Exception as PPException;
|
||||
|
||||
/**
|
||||
* Com\Tecnick\Pdf\Parser\Process\XrefStream
|
||||
*
|
||||
* Process XREF
|
||||
*
|
||||
* @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
|
||||
*
|
||||
* @phpstan-import-type RawObjectArray from \Com\Tecnick\Pdf\Parser\Process\RawObject
|
||||
*
|
||||
* @phpstan-type XrefData array{
|
||||
* 'trailer': array{
|
||||
* 'encrypt'?: string,
|
||||
* 'id': array<int, string>,
|
||||
* 'info': string,
|
||||
* 'root': string,
|
||||
* 'size': int,
|
||||
* },
|
||||
* 'xref': array<string, int|string>,
|
||||
* }
|
||||
*
|
||||
* @SuppressWarnings("PHPMD.ExcessiveClassComplexity")
|
||||
*/
|
||||
abstract class XrefStream extends \Com\Tecnick\Pdf\Parser\Process\RawObject
|
||||
{
|
||||
/**
|
||||
* Process object indexes
|
||||
*
|
||||
* @param XrefData $xref XREF data
|
||||
* @param int $obj_num Object number
|
||||
* @param array<int, array<int, int>> $sdata Stream data
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
protected function processObjIndexes(array &$xref, int &$obj_num, array $sdata): void
|
||||
{
|
||||
foreach ($sdata as $sdatum) {
|
||||
$this->processSingleObjIndex($xref, $obj_num, $sdatum);
|
||||
++$obj_num;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process object indexes using explicit object numbers.
|
||||
*
|
||||
* @param XrefData $xref XREF data
|
||||
* @param array<int, int> $objNumbers Object numbers for each stream row
|
||||
* @param array<int, array<int, int>> $sdata Stream data
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
protected function processObjIndexesMap(array &$xref, array $objNumbers, array $sdata): void
|
||||
{
|
||||
$objCount = \count($objNumbers);
|
||||
$rowCount = \count($sdata);
|
||||
if ($rowCount !== $objCount) {
|
||||
throw new PPException(
|
||||
'Invalid xref stream row count: expected ' . $objCount . ' rows from Index, got ' . $rowCount,
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($objNumbers as $idx => $objNum) {
|
||||
$row = $sdata[$idx] ?? null;
|
||||
if (!\is_array($row)) {
|
||||
throw new PPException('Invalid xref stream row at index ' . $idx);
|
||||
}
|
||||
|
||||
$this->processSingleObjIndex($xref, $objNum, $row);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a single xref stream row.
|
||||
*
|
||||
* @param XrefData $xref XREF data
|
||||
* @param int $objNum Object number for the row
|
||||
* @param array<int, int> $sdatum Stream row data
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
protected function processSingleObjIndex(array &$xref, int $objNum, array $sdatum): void
|
||||
{
|
||||
$entryType = (int) ($sdatum[0] ?? 0);
|
||||
switch ($entryType) {
|
||||
case 0:
|
||||
// (f) linked list of free objects
|
||||
break;
|
||||
case 1:
|
||||
if (!\array_key_exists(1, $sdatum)) {
|
||||
throw new PPException(
|
||||
'Invalid xref stream entry for object ' . $objNum . ': missing offset for in-use entry',
|
||||
);
|
||||
}
|
||||
|
||||
// (n) objects that are in use but are not compressed
|
||||
// create unique object index: [object number]_[generation number]
|
||||
$index = $objNum . '_' . (int) ($sdatum[2] ?? 0);
|
||||
// check if object already exist
|
||||
if (!\array_key_exists($index, $xref['xref'])) {
|
||||
// store object offset position
|
||||
$xref['xref'][$index] = (int) $sdatum[1];
|
||||
}
|
||||
|
||||
break;
|
||||
case 2:
|
||||
if (!\array_key_exists(1, $sdatum) || !\array_key_exists(2, $sdatum)) {
|
||||
throw new PPException(
|
||||
'Invalid xref stream entry for object '
|
||||
. $objNum
|
||||
. ': missing object stream reference for compressed entry',
|
||||
);
|
||||
}
|
||||
|
||||
// compressed objects
|
||||
// $row[1] = object number of the object stream in which this object is stored
|
||||
// $row[2] = index of this object within the object stream
|
||||
$index = $objNum . '_0';
|
||||
if (!\array_key_exists($index, $xref['xref'])) {
|
||||
$xref['xref'][$index] = (int) $sdatum[1] . '_0_' . (int) $sdatum[2];
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// null objects
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PNG Unpredictor
|
||||
*
|
||||
* @param array<int, array<int, int>> $sdata Stream data
|
||||
* @param array<int, array<int, int>> $ddata Decoded data
|
||||
* @param int $columns Number of columns
|
||||
* @param array<int, int> $prev_row Previous row
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
protected function pngUnpredictor(array $sdata, array &$ddata, int $columns, array $prev_row): void
|
||||
{
|
||||
// for each row apply PNG unpredictor
|
||||
foreach ($sdata as $key => $row) {
|
||||
// initialize new row
|
||||
$filllen = \max(0, $columns);
|
||||
$ddata[$key] = \array_fill(0, $filllen, 0);
|
||||
// get PNG predictor value
|
||||
$predictor = 10 + (int) ($row[0] ?? 0);
|
||||
// for each byte on the row
|
||||
for ($idx = 1; $idx <= $columns; ++$idx) {
|
||||
// new index
|
||||
$jdx = $idx - 1;
|
||||
$row_up = (int) ($prev_row[$jdx] ?? 0);
|
||||
if ($idx === 1) {
|
||||
$row_left = 0;
|
||||
$row_upleft = 0;
|
||||
} else {
|
||||
$row_left = (int) ($row[$idx - 1] ?? 0);
|
||||
$row_upleft = (int) ($prev_row[$jdx - 1] ?? 0);
|
||||
}
|
||||
|
||||
$row_value = (int) ($row[$idx] ?? 0);
|
||||
|
||||
switch ($predictor) {
|
||||
case 10:
|
||||
// PNG prediction (on encoding, PNG None on all rows)
|
||||
$ddata[$key][$jdx] = $row_value;
|
||||
break;
|
||||
case 11:
|
||||
// PNG prediction (on encoding, PNG Sub on all rows)
|
||||
$ddata[$key][$jdx] = ($row_value + $row_left) & 0xff;
|
||||
break;
|
||||
case 12:
|
||||
// PNG prediction (on encoding, PNG Up on all rows)
|
||||
$ddata[$key][$jdx] = ($row_value + $row_up) & 0xff;
|
||||
break;
|
||||
case 13:
|
||||
// PNG prediction (on encoding, PNG Average on all rows)
|
||||
$ddata[$key][$jdx] = ($row_value + \intdiv($row_left + $row_up, 2)) & 0xff;
|
||||
break;
|
||||
case 14:
|
||||
// PNG prediction (on encoding, PNG Paeth on all rows)
|
||||
$this->minDistance($ddata, $key, $row_value, $jdx, [$row_left, $row_up, $row_upleft]);
|
||||
break;
|
||||
default:
|
||||
// PNG prediction (on encoding, PNG optimum)
|
||||
throw new PPException('Unknown PNG predictor');
|
||||
}
|
||||
}
|
||||
|
||||
$prev_row = $ddata[$key];
|
||||
} // end for each row
|
||||
}
|
||||
|
||||
/**
|
||||
* Return minimum distance for PNG unpredictor
|
||||
*
|
||||
* @param array<int, array<int, int>> $ddata Decoded data
|
||||
* @param int $key Key
|
||||
* @param int $row_value Current row value
|
||||
* @param int $jdx Jdx
|
||||
* @param array{0:int, 1:int, 2:int} $rows Left, up and up-left row values
|
||||
*/
|
||||
protected function minDistance(array &$ddata, int $key, int $row_value, int $jdx, array $rows): void
|
||||
{
|
||||
$row_left = $rows[0];
|
||||
$row_up = $rows[1];
|
||||
$row_upleft = $rows[2];
|
||||
|
||||
// initial estimate
|
||||
$pos = $row_left + $row_up - $row_upleft;
|
||||
// distances
|
||||
$psa = \abs($pos - $row_left);
|
||||
$psb = \abs($pos - $row_up);
|
||||
$psc = \abs($pos - $row_upleft);
|
||||
$pmin = \min($psa, $psb, $psc);
|
||||
switch ($pmin) {
|
||||
case $psa:
|
||||
$ddata[$key][$jdx] = ($row_value + $row_left) & 0xff;
|
||||
break;
|
||||
case $psb:
|
||||
$ddata[$key][$jdx] = ($row_value + $row_up) & 0xff;
|
||||
break;
|
||||
case $psc:
|
||||
$ddata[$key][$jdx] = ($row_value + $row_upleft) & 0xff;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process XREF types
|
||||
*
|
||||
* @param array<int, RawObjectArray> $sarr Stream data
|
||||
* @param XrefData $xref XREF data
|
||||
* @param array<int, int> $wbt WBT data
|
||||
* @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 Parsing state
|
||||
* @param bool $filltrailer Fill trailer
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*
|
||||
* @SuppressWarnings("PHPMD.CyclomaticComplexity")
|
||||
*/
|
||||
protected function processXrefType(array $sarr, array &$xref, array &$wbt, array &$state, bool $filltrailer): void
|
||||
{
|
||||
foreach ($sarr as $key => $val) {
|
||||
if ($val[0] !== '/') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!\is_string($val[1])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$next = $sarr[$key + 1] ?? null;
|
||||
|
||||
switch ($val[1]) {
|
||||
case 'Type':
|
||||
$state['valid_crs'] = \is_array($next) && $next[0] === '/' && $next[1] === 'XRef';
|
||||
break;
|
||||
case 'Index':
|
||||
$state['index_sections'] = $this->parseXrefIndexSections($next);
|
||||
break;
|
||||
case 'Prev':
|
||||
$this->processXrefPrev($next, $state['prevxref']);
|
||||
break;
|
||||
case 'Size':
|
||||
if (\is_array($next) && $next[0] === 'numeric') {
|
||||
$state['size'] = (int) $next[1];
|
||||
}
|
||||
|
||||
break;
|
||||
case 'W':
|
||||
// number of bytes (in the decoded stream) of the corresponding field
|
||||
if (\is_array($next)) {
|
||||
$wbt[0] = (int) ($next[1][0][1] ?? 0);
|
||||
$wbt[1] = (int) ($next[1][1][1] ?? 0);
|
||||
$wbt[2] = (int) ($next[1][2][1] ?? 0);
|
||||
}
|
||||
break;
|
||||
case 'DecodeParms':
|
||||
$this->processXrefDecodeParms($next, $state['columns'], $state['predictor']);
|
||||
break;
|
||||
}
|
||||
|
||||
$this->processXrefTypeFt($val[1], $sarr, $key, $xref, $filltrailer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the xref stream Index array into normalized [startObj, count] pairs.
|
||||
*
|
||||
* @param RawObjectArray|null $indexObj Index object token.
|
||||
*
|
||||
* @return array<int, array{0:int, 1:int}>|null
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Parser\Exception
|
||||
*/
|
||||
protected function parseXrefIndexSections(?array $indexObj): ?array
|
||||
{
|
||||
if (!\is_array($indexObj)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$values = $indexObj[1];
|
||||
if (!\is_array($values)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ((\count($values) % 2) !== 0) {
|
||||
throw new PPException('Invalid xref stream Index array: expected even number of values');
|
||||
}
|
||||
|
||||
$sections = [];
|
||||
for ($idx = 0, $max = \count($values); $idx < $max; $idx += 2) {
|
||||
$startToken = $values[$idx] ?? null;
|
||||
$countToken = $values[$idx + 1] ?? null;
|
||||
if (!\is_array($startToken) || !\is_array($countToken)) {
|
||||
throw new PPException('Invalid xref stream Index array: expected numeric values');
|
||||
}
|
||||
|
||||
if ($startToken[0] !== 'numeric' || $countToken[0] !== 'numeric') {
|
||||
throw new PPException('Invalid xref stream Index array: expected numeric values');
|
||||
}
|
||||
|
||||
$startObj = (int) $startToken[1];
|
||||
$count = (int) $countToken[1];
|
||||
if ($startObj < 0 || $count < 0) {
|
||||
throw new PPException('Invalid xref stream Index array: values must be non-negative');
|
||||
}
|
||||
|
||||
$sections[] = [$startObj, $count];
|
||||
}
|
||||
|
||||
return $sections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the full object-number map for decoded xref stream rows.
|
||||
*
|
||||
* @param array<int, array{0:int, 1:int}> $indexSections Normalized Index sections.
|
||||
*
|
||||
* @return array<int, int>
|
||||
*/
|
||||
protected function buildXrefObjectNumbers(array $indexSections): array
|
||||
{
|
||||
$objNumbers = [];
|
||||
foreach ($indexSections as $section) {
|
||||
$startObj = $section[0];
|
||||
$count = $section[1];
|
||||
$limit = $startObj + $count;
|
||||
for ($objNum = $startObj; $objNum < $limit; ++$objNum) {
|
||||
$objNumbers[] = $objNum;
|
||||
}
|
||||
}
|
||||
|
||||
return $objNumbers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process XREF type Prev
|
||||
*
|
||||
* @param RawObjectArray|null $next Next token
|
||||
* @param int|null $prevxref Previous XREF
|
||||
*/
|
||||
protected function processXrefPrev(?array $next, ?int &$prevxref): void
|
||||
{
|
||||
if (\is_array($next) && $next[0] === 'numeric') {
|
||||
// get previous xref offset
|
||||
$prevxref = (int) $next[1];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process XREF type DecodeParms
|
||||
*
|
||||
* @param RawObjectArray|null $next Next token
|
||||
* @param int $columns Number of columns
|
||||
* @param int $predictor Predictor value
|
||||
*/
|
||||
protected function processXrefDecodeParms(?array $next, int &$columns, int &$predictor): void
|
||||
{
|
||||
$decpar = $next[1] ?? null;
|
||||
if (!\is_array($decpar)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($decpar as $kdc => $vdc) {
|
||||
$nextDecpar = $decpar[$kdc + 1] ?? null;
|
||||
if (\is_array($nextDecpar) && $vdc[0] === '/' && $vdc[1] === 'Columns' && $nextDecpar[0] === 'numeric') {
|
||||
$columns = (int) $nextDecpar[1];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (\is_array($nextDecpar) && $vdc[0] === '/' && $vdc[1] === 'Predictor' && $nextDecpar[0] === 'numeric') {
|
||||
$predictor = (int) $nextDecpar[1];
|
||||
}
|
||||
}
|
||||
|
||||
$columns = \max(0, $columns);
|
||||
$predictor = \max(0, $predictor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process XREF type
|
||||
*
|
||||
* @param string $type Type
|
||||
* @param array<int, RawObjectArray> $sarr Stream data
|
||||
* @param int $key Key
|
||||
* @param XrefData $xref XREF data
|
||||
* @param bool $filltrailer Fill trailer
|
||||
*/
|
||||
protected function processXrefTypeFt(string $type, array $sarr, int $key, array &$xref, bool $filltrailer): void
|
||||
{
|
||||
if (!$filltrailer) {
|
||||
return;
|
||||
}
|
||||
|
||||
$next = $sarr[$key + 1] ?? null;
|
||||
|
||||
switch ($type) {
|
||||
case 'Size':
|
||||
if (\is_array($next) && $next[0] === 'numeric') {
|
||||
$xref['trailer']['size'] = (int) $next[1];
|
||||
}
|
||||
|
||||
break;
|
||||
case 'ID':
|
||||
$id0 = $next[1][0][1] ?? null;
|
||||
$id1 = $next[1][1][1] ?? null;
|
||||
if (!\is_array($next) || !\is_string($id0) || !\is_string($id1) || $id0 === '' || $id1 === '') {
|
||||
break;
|
||||
}
|
||||
$xref['trailer']['id'] = [
|
||||
$id0,
|
||||
$id1,
|
||||
];
|
||||
break;
|
||||
default:
|
||||
$xref = $this->processXrefObjref($type, $sarr, $key, $xref);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process XREF type Objref
|
||||
*
|
||||
* @param string $type Type
|
||||
* @param array<int, RawObjectArray> $sarr Stream data
|
||||
* @param int $key Key
|
||||
* @param XrefData $xref XREF data
|
||||
*
|
||||
* @return XrefData XREF data.
|
||||
*/
|
||||
protected function processXrefObjref(string $type, array $sarr, int $key, array $xref): array
|
||||
{
|
||||
$next = $sarr[$key + 1] ?? null;
|
||||
if (!\is_array($next) || $next[0] !== 'objref') {
|
||||
return $xref;
|
||||
}
|
||||
|
||||
$val = $next[1];
|
||||
if (!\is_string($val)) {
|
||||
return $xref;
|
||||
}
|
||||
|
||||
switch ($type) {
|
||||
case 'Root':
|
||||
$xref['trailer']['root'] = $val;
|
||||
break;
|
||||
case 'Info':
|
||||
$xref['trailer']['info'] = $val;
|
||||
break;
|
||||
case 'Encrypt':
|
||||
$xref['trailer']['encrypt'] = $val;
|
||||
break;
|
||||
}
|
||||
|
||||
return $xref;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user