Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

Differences Between: [Versions 310 and 311] [Versions 311 and 403] [Versions 39 and 311]

   1  <?php
   2  
   3  /**
   4   * This file is part of FPDI
   5   *
   6   * @package   setasign\Fpdi
   7   * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com)
   8   * @license   http://opensource.org/licenses/mit-license The MIT License
   9   */
  10  
  11  namespace setasign\Fpdi\PdfParser\Type;
  12  
  13  use setasign\Fpdi\PdfParser\CrossReference\CrossReferenceException;
  14  use setasign\Fpdi\PdfParser\Filter\Ascii85;
  15  use setasign\Fpdi\PdfParser\Filter\AsciiHex;
  16  use setasign\Fpdi\PdfParser\Filter\FilterException;
  17  use setasign\Fpdi\PdfParser\Filter\Flate;
  18  use setasign\Fpdi\PdfParser\Filter\Lzw;
  19  use setasign\Fpdi\PdfParser\PdfParser;
  20  use setasign\Fpdi\PdfParser\PdfParserException;
  21  use setasign\Fpdi\PdfParser\StreamReader;
  22  use setasign\FpdiPdfParser\PdfParser\Filter\Predictor;
  23  
  24  /**
  25   * Class representing a PDF stream object
  26   */
  27  class PdfStream extends PdfType
  28  {
  29      /**
  30       * Parses a stream from a stream reader.
  31       *
  32       * @param PdfDictionary $dictionary
  33       * @param StreamReader $reader
  34       * @param PdfParser $parser Optional to keep backwards compatibility
  35       * @return self
  36       * @throws PdfTypeException
  37       */
  38      public static function parse(PdfDictionary $dictionary, StreamReader $reader, PdfParser $parser = null)
  39      {
  40          $v = new self();
  41          $v->value = $dictionary;
  42          $v->reader = $reader;
  43          $v->parser = $parser;
  44  
  45          $offset = $reader->getOffset();
  46  
  47          // Find the first "newline"
  48          while (($firstByte = $reader->getByte($offset)) !== false) {
  49              if ($firstByte !== "\n" && $firstByte !== "\r") {
  50                  $offset++;
  51              } else {
  52                  break;
  53              }
  54          }
  55  
  56          if ($firstByte === false) {
  57              throw new PdfTypeException(
  58                  'Unable to parse stream data. No newline after the stream keyword found.',
  59                  PdfTypeException::NO_NEWLINE_AFTER_STREAM_KEYWORD
  60              );
  61          }
  62  
  63          $sndByte = $reader->getByte($offset + 1);
  64          if ($firstByte === "\n" || $firstByte === "\r") {
  65              $offset++;
  66          }
  67  
  68          if ($sndByte === "\n" && $firstByte !== "\n") {
  69              $offset++;
  70          }
  71  
  72          $reader->setOffset($offset);
  73          // let's only save the byte-offset and read the stream only when needed
  74          $v->stream = $reader->getPosition() + $reader->getOffset();
  75  
  76          return $v;
  77      }
  78  
  79      /**
  80       * Helper method to create an instance.
  81       *
  82       * @param PdfDictionary $dictionary
  83       * @param string $stream
  84       * @return self
  85       */
  86      public static function create(PdfDictionary $dictionary, $stream)
  87      {
  88          $v = new self();
  89          $v->value = $dictionary;
  90          $v->stream = (string) $stream;
  91  
  92          return $v;
  93      }
  94  
  95      /**
  96       * Ensures that the passed value is a PdfStream instance.
  97       *
  98       * @param mixed $stream
  99       * @return self
 100       * @throws PdfTypeException
 101       */
 102      public static function ensure($stream)
 103      {
 104          return PdfType::ensureType(self::class, $stream, 'Stream value expected.');
 105      }
 106  
 107      /**
 108       * The stream or its byte-offset position.
 109       *
 110       * @var int|string
 111       */
 112      protected $stream;
 113  
 114      /**
 115       * The stream reader instance.
 116       *
 117       * @var StreamReader|null
 118       */
 119      protected $reader;
 120  
 121      /**
 122       * The PDF parser instance.
 123       *
 124       * @var PdfParser
 125       */
 126      protected $parser;
 127  
 128      /**
 129       * Get the stream data.
 130       *
 131       * @param bool $cache Whether cache the stream data or not.
 132       * @return bool|string
 133       * @throws PdfTypeException
 134       * @throws CrossReferenceException
 135       * @throws PdfParserException
 136       */
 137      public function getStream($cache = false)
 138      {
 139          if (\is_int($this->stream)) {
 140              $length = PdfDictionary::get($this->value, 'Length');
 141              if ($this->parser !== null) {
 142                  $length = PdfType::resolve($length, $this->parser);
 143              }
 144  
 145              if (!($length instanceof PdfNumeric) || $length->value === 0) {
 146                  $this->reader->reset($this->stream, 100000);
 147                  $buffer = $this->extractStream();
 148              } else {
 149                  $this->reader->reset($this->stream, $length->value);
 150                  $buffer = $this->reader->getBuffer(false);
 151                  if ($this->parser !== null) {
 152                      $this->reader->reset($this->stream + strlen($buffer));
 153                      $this->parser->getTokenizer()->clearStack();
 154                      $token = $this->parser->readValue();
 155                      if ($token === false || !($token instanceof PdfToken) || $token->value !== 'endstream') {
 156                          $this->reader->reset($this->stream, 100000);
 157                          $buffer = $this->extractStream();
 158                          $this->reader->reset($this->stream + strlen($buffer));
 159                      }
 160                  }
 161              }
 162  
 163              if ($cache === false) {
 164                  return $buffer;
 165              }
 166  
 167              $this->stream = $buffer;
 168              $this->reader = null;
 169          }
 170  
 171          return $this->stream;
 172      }
 173  
 174      /**
 175       * Extract the stream "manually".
 176       *
 177       * @return string
 178       * @throws PdfTypeException
 179       */
 180      protected function extractStream()
 181      {
 182          while (true) {
 183              $buffer = $this->reader->getBuffer(false);
 184              $length = \strpos($buffer, 'endstream');
 185              if ($length === false) {
 186                  if (!$this->reader->increaseLength(100000)) {
 187                      throw new PdfTypeException('Cannot extract stream.');
 188                  }
 189                  continue;
 190              }
 191              break;
 192          }
 193  
 194          $buffer = \substr($buffer, 0, $length);
 195          $lastByte = \substr($buffer, -1);
 196  
 197          /* Check for EOL marker =
 198           *   CARRIAGE RETURN (\r) and a LINE FEED (\n) or just a LINE FEED (\n},
 199           *   and not by a CARRIAGE RETURN (\r) alone
 200           */
 201          if ($lastByte === "\n") {
 202              $buffer = \substr($buffer, 0, -1);
 203  
 204              $lastByte = \substr($buffer, -1);
 205              if ($lastByte === "\r") {
 206                  $buffer = \substr($buffer, 0, -1);
 207              }
 208          }
 209  
 210          // There are streams in the wild, which have only white signs in them but need to be parsed manually due
 211          // to a problem encountered before (e.g. Length === 0). We should set them to empty streams to avoid problems
 212          // in further processing (e.g. applying of filters).
 213          if (trim($buffer) === '') {
 214              $buffer = '';
 215          }
 216  
 217          return $buffer;
 218      }
 219  
 220      /**
 221       * Get the unfiltered stream data.
 222       *
 223       * @return string
 224       * @throws FilterException
 225       * @throws PdfParserException
 226       */
 227      public function getUnfilteredStream()
 228      {
 229          $stream = $this->getStream();
 230          $filters = PdfDictionary::get($this->value, 'Filter');
 231          if ($filters instanceof PdfNull) {
 232              return $stream;
 233          }
 234  
 235          if ($filters instanceof PdfArray) {
 236              $filters = $filters->value;
 237          } else {
 238              $filters = [$filters];
 239          }
 240  
 241          $decodeParams = PdfDictionary::get($this->value, 'DecodeParms');
 242          if ($decodeParams instanceof PdfArray) {
 243              $decodeParams = $decodeParams->value;
 244          } else {
 245              $decodeParams = [$decodeParams];
 246          }
 247  
 248          foreach ($filters as $key => $filter) {
 249              if (!($filter instanceof PdfName)) {
 250                  continue;
 251              }
 252  
 253              $decodeParam = null;
 254              if (isset($decodeParams[$key])) {
 255                  $decodeParam = ($decodeParams[$key] instanceof PdfDictionary ? $decodeParams[$key] : null);
 256              }
 257  
 258              switch ($filter->value) {
 259                  case 'FlateDecode':
 260                  case 'Fl':
 261                  case 'LZWDecode':
 262                  case 'LZW':
 263                      if (\strpos($filter->value, 'LZW') === 0) {
 264                          $filterObject = new Lzw();
 265                      } else {
 266                          $filterObject = new Flate();
 267                      }
 268  
 269                      $stream = $filterObject->decode($stream);
 270  
 271                      if ($decodeParam instanceof PdfDictionary) {
 272                          $predictor = PdfDictionary::get($decodeParam, 'Predictor', PdfNumeric::create(1));
 273                          if ($predictor->value !== 1) {
 274                              if (!\class_exists(Predictor::class)) {
 275                                  throw new PdfParserException(
 276                                      'This PDF document makes use of features which are only implemented in the ' .
 277                                      'commercial "FPDI PDF-Parser" add-on (see https://www.setasign.com/fpdi-pdf-' .
 278                                      'parser).',
 279                                      PdfParserException::IMPLEMENTED_IN_FPDI_PDF_PARSER
 280                                  );
 281                              }
 282  
 283                              $colors = PdfDictionary::get($decodeParam, 'Colors', PdfNumeric::create(1));
 284                              $bitsPerComponent = PdfDictionary::get(
 285                                  $decodeParam,
 286                                  'BitsPerComponent',
 287                                  PdfNumeric::create(8)
 288                              );
 289  
 290                              $columns = PdfDictionary::get($decodeParam, 'Columns', PdfNumeric::create(1));
 291  
 292                              $filterObject = new Predictor(
 293                                  $predictor->value,
 294                                  $colors->value,
 295                                  $bitsPerComponent->value,
 296                                  $columns->value
 297                              );
 298  
 299                              $stream = $filterObject->decode($stream);
 300                          }
 301                      }
 302  
 303                      break;
 304                  case 'ASCII85Decode':
 305                  case 'A85':
 306                      $filterObject = new Ascii85();
 307                      $stream = $filterObject->decode($stream);
 308                      break;
 309  
 310                  case 'ASCIIHexDecode':
 311                  case 'AHx':
 312                      $filterObject = new AsciiHex();
 313                      $stream = $filterObject->decode($stream);
 314                      break;
 315  
 316                  default:
 317                      throw new FilterException(
 318                          \sprintf('Unsupported filter "%s".', $filter->value),
 319                          FilterException::UNSUPPORTED_FILTER
 320                      );
 321              }
 322          }
 323  
 324          return $stream;
 325      }
 326  }