AutorÃa | Ultima modificación | Ver Log |
<?php/*** This file is part of FPDI** @package setasign\Fpdi* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)* @license http://opensource.org/licenses/mit-license The MIT License*/namespace setasign\Fpdi\PdfParser\Type;use setasign\Fpdi\PdfParser\CrossReference\CrossReferenceException;use setasign\Fpdi\PdfParser\Filter\Ascii85;use setasign\Fpdi\PdfParser\Filter\AsciiHex;use setasign\Fpdi\PdfParser\Filter\FilterException;use setasign\Fpdi\PdfParser\Filter\Flate;use setasign\Fpdi\PdfParser\Filter\Lzw;use setasign\Fpdi\PdfParser\PdfParser;use setasign\Fpdi\PdfParser\PdfParserException;use setasign\Fpdi\PdfParser\StreamReader;use setasign\FpdiPdfParser\PdfParser\Filter\Predictor;/*** Class representing a PDF stream object*/class PdfStream extends PdfType{/*** Parses a stream from a stream reader.** @param PdfDictionary $dictionary* @param StreamReader $reader* @param PdfParser|null $parser Optional to keep backwards compatibility* @return self* @throws PdfTypeException*/public static function parse(PdfDictionary $dictionary, StreamReader $reader, ?PdfParser $parser = null){$v = new self();$v->value = $dictionary;$v->reader = $reader;$v->parser = $parser;$offset = $reader->getOffset();// Find the first "newline"while (($firstByte = $reader->getByte($offset)) !== false) {$offset++;if ($firstByte === "\n" || $firstByte === "\r") {break;}}if ($firstByte === false) {throw new PdfTypeException('Unable to parse stream data. No newline after the stream keyword found.',PdfTypeException::NO_NEWLINE_AFTER_STREAM_KEYWORD);}$sndByte = $reader->getByte($offset);if ($sndByte === "\n" && $firstByte !== "\n") {$offset++;}$reader->setOffset($offset);// let's only save the byte-offset and read the stream only when needed$v->stream = $reader->getPosition() + $reader->getOffset();return $v;}/*** Helper method to create an instance.** @param PdfDictionary $dictionary* @param string $stream* @return self*/public static function create(PdfDictionary $dictionary, $stream){$v = new self();$v->value = $dictionary;$v->stream = (string) $stream;return $v;}/*** Ensures that the passed value is a PdfStream instance.** @param mixed $stream* @return self* @throws PdfTypeException*/public static function ensure($stream){return PdfType::ensureType(self::class, $stream, 'Stream value expected.');}/*** The stream or its byte-offset position.** @var int|string*/protected $stream;/*** The stream reader instance.** @var StreamReader|null*/protected $reader;/*** The PDF parser instance.** @var PdfParser*/protected $parser;/*** Get the stream data.** @param bool $cache Whether cache the stream data or not.* @return bool|string* @throws PdfTypeException* @throws CrossReferenceException* @throws PdfParserException*/public function getStream($cache = false){if (\is_int($this->stream)) {$length = PdfDictionary::get($this->value, 'Length');if ($this->parser !== null) {$length = PdfType::resolve($length, $this->parser);}if (!($length instanceof PdfNumeric) || $length->value === 0) {$this->reader->reset($this->stream, 100000);$buffer = $this->extractStream();} else {$this->reader->reset($this->stream, $length->value);$buffer = $this->reader->getBuffer(false);if ($this->parser !== null) {$this->reader->reset($this->stream + strlen($buffer));$this->parser->getTokenizer()->clearStack();$token = $this->parser->readValue();if ($token === false || !($token instanceof PdfToken) || $token->value !== 'endstream') {$this->reader->reset($this->stream, 100000);$buffer = $this->extractStream();$this->reader->reset($this->stream + strlen($buffer));}}}if ($cache === false) {return $buffer;}$this->stream = $buffer;$this->reader = null;}return $this->stream;}/*** Extract the stream "manually".** @return string* @throws PdfTypeException*/protected function extractStream(){while (true) {$buffer = $this->reader->getBuffer(false);$length = \strpos($buffer, 'endstream');if ($length === false) {if (!$this->reader->increaseLength(100000)) {throw new PdfTypeException('Cannot extract stream.');}continue;}break;}$buffer = \substr($buffer, 0, $length);$lastByte = \substr($buffer, -1);/* Check for EOL marker =* CARRIAGE RETURN (\r) and a LINE FEED (\n) or just a LINE FEED (\n},* and not by a CARRIAGE RETURN (\r) alone*/if ($lastByte === "\n") {$buffer = \substr($buffer, 0, -1);$lastByte = \substr($buffer, -1);if ($lastByte === "\r") {$buffer = \substr($buffer, 0, -1);}}// There are streams in the wild, which have only white signs in them but need to be parsed manually due// to a problem encountered before (e.g. Length === 0). We should set them to empty streams to avoid problems// in further processing (e.g. applying of filters).if (trim($buffer) === '') {$buffer = '';}return $buffer;}/*** Get all filters defined for this stream.** @return PdfType[]* @throws PdfTypeException*/public function getFilters(){$filters = PdfDictionary::get($this->value, 'Filter');if ($filters instanceof PdfNull) {return [];}if ($filters instanceof PdfArray) {$filters = $filters->value;} else {$filters = [$filters];}return $filters;}/*** Get the unfiltered stream data.** @return string* @throws FilterException* @throws PdfParserException*/public function getUnfilteredStream(){$stream = $this->getStream();$filters = $this->getFilters();if ($filters === []) {return $stream;}$decodeParams = PdfDictionary::get($this->value, 'DecodeParms');if ($decodeParams instanceof PdfArray) {$decodeParams = $decodeParams->value;} else {$decodeParams = [$decodeParams];}foreach ($filters as $key => $filter) {if (!($filter instanceof PdfName)) {continue;}$decodeParam = null;if (isset($decodeParams[$key])) {$decodeParam = ($decodeParams[$key] instanceof PdfDictionary ? $decodeParams[$key] : null);}switch ($filter->value) {case 'FlateDecode':case 'Fl':case 'LZWDecode':case 'LZW':if (\strpos($filter->value, 'LZW') === 0) {$filterObject = new Lzw();} else {$filterObject = new Flate();}$stream = $filterObject->decode($stream);if ($decodeParam instanceof PdfDictionary) {$predictor = PdfDictionary::get($decodeParam, 'Predictor', PdfNumeric::create(1));if ($predictor->value !== 1) {if (!\class_exists(Predictor::class)) {throw new PdfParserException('This PDF document makes use of features which are only implemented in the ' .'commercial "FPDI PDF-Parser" add-on (see https://www.setasign.com/fpdi-pdf-' .'parser).',PdfParserException::IMPLEMENTED_IN_FPDI_PDF_PARSER);}$colors = PdfDictionary::get($decodeParam, 'Colors', PdfNumeric::create(1));$bitsPerComponent = PdfDictionary::get($decodeParam,'BitsPerComponent',PdfNumeric::create(8));$columns = PdfDictionary::get($decodeParam, 'Columns', PdfNumeric::create(1));$filterObject = new Predictor($predictor->value,$colors->value,$bitsPerComponent->value,$columns->value);$stream = $filterObject->decode($stream);}}break;case 'ASCII85Decode':case 'A85':$filterObject = new Ascii85();$stream = $filterObject->decode($stream);break;case 'ASCIIHexDecode':case 'AHx':$filterObject = new AsciiHex();$stream = $filterObject->decode($stream);break;case 'Crypt':if (!$decodeParam instanceof PdfDictionary) {break;}// Filter is "Identity"$name = PdfDictionary::get($decodeParam, 'Name');if (!$name instanceof PdfName || $name->value !== 'Identity') {break;}throw new FilterException('Support for Crypt filters other than "Identity" is not implemented.',FilterException::UNSUPPORTED_FILTER);default:throw new FilterException(\sprintf('Unsupported filter "%s".', $filter->value),FilterException::UNSUPPORTED_FILTER);}}return $stream;}}