Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

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

   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\CrossReference;
  12  
  13  use setasign\Fpdi\PdfParser\PdfParser;
  14  use setasign\Fpdi\PdfParser\StreamReader;
  15  
  16  /**
  17   * Class FixedReader
  18   *
  19   * This reader allows a very less overhead parsing of single entries of the cross-reference, because the main entries
  20   * are only read when needed and not in a single run.
  21   */
  22  class FixedReader extends AbstractReader implements ReaderInterface
  23  {
  24      /**
  25       * @var StreamReader
  26       */
  27      protected $reader;
  28  
  29      /**
  30       * Data of subsections.
  31       *
  32       * @var array
  33       */
  34      protected $subSections;
  35  
  36      /**
  37       * FixedReader constructor.
  38       *
  39       * @param PdfParser $parser
  40       * @throws CrossReferenceException
  41       */
  42      public function __construct(PdfParser $parser)
  43      {
  44          $this->reader = $parser->getStreamReader();
  45          $this->read();
  46          parent::__construct($parser);
  47      }
  48  
  49      /**
  50       * Get all subsection data.
  51       *
  52       * @return array
  53       */
  54      public function getSubSections()
  55      {
  56          return $this->subSections;
  57      }
  58  
  59      /**
  60       * @inheritdoc
  61       */
  62      public function getOffsetFor($objectNumber)
  63      {
  64          foreach ($this->subSections as $offset => list($startObject, $objectCount)) {
  65              /**
  66               * @var int $startObject
  67               * @var int $objectCount
  68               */
  69              if ($objectNumber >= $startObject && $objectNumber < ($startObject + $objectCount)) {
  70                  $position = $offset + 20 * ($objectNumber - $startObject);
  71                  $this->reader->ensure($position, 20);
  72                  $line = $this->reader->readBytes(20);
  73                  if ($line[17] === 'f') {
  74                      return false;
  75                  }
  76  
  77                  return (int) \substr($line, 0, 10);
  78              }
  79          }
  80  
  81          return false;
  82      }
  83  
  84      /**
  85       * Read the cross-reference.
  86       *
  87       * This reader will only read the subsections in this method. The offsets were resolved individually by this
  88       * information.
  89       *
  90       * @throws CrossReferenceException
  91       */
  92      protected function read()
  93      {
  94          $subSections = [];
  95  
  96          $startObject = $entryCount = $lastLineStart = null;
  97          $validityChecked = false;
  98          while (($line = $this->reader->readLine(20)) !== false) {
  99              if (\strpos($line, 'trailer') !== false) {
 100                  $this->reader->reset($lastLineStart);
 101                  break;
 102              }
 103  
 104              // jump over if line content doesn't match the expected string
 105              if (\sscanf($line, '%d %d', $startObject, $entryCount) !== 2) {
 106                  continue;
 107              }
 108  
 109              $oldPosition = $this->reader->getPosition();
 110              $position = $oldPosition + $this->reader->getOffset();
 111  
 112              if (!$validityChecked && $entryCount > 0) {
 113                  $nextLine = $this->reader->readBytes(21);
 114                  /* Check the next line for maximum of 20 bytes and not longer
 115                   * By catching 21 bytes and trimming the length should be still 21.
 116                   */
 117                  if (\strlen(\trim($nextLine)) !== 21) {
 118                      throw new CrossReferenceException(
 119                          'Cross-reference entries are larger than 20 bytes.',
 120                          CrossReferenceException::ENTRIES_TOO_LARGE
 121                      );
 122                  }
 123  
 124                  /* Check for less than 20 bytes: cut the line to 20 bytes and trim; have to result in exactly 18 bytes.
 125                   * If it would have less bytes the substring would get the first bytes of the next line which would
 126                   * evaluate to a 20 bytes long string after trimming.
 127                   */
 128                  if (\strlen(\trim(\substr($nextLine, 0, 20))) !== 18) {
 129                      throw new CrossReferenceException(
 130                          'Cross-reference entries are less than 20 bytes.',
 131                          CrossReferenceException::ENTRIES_TOO_SHORT
 132                      );
 133                  }
 134  
 135                  $validityChecked = true;
 136              }
 137  
 138              $subSections[$position] = [$startObject, $entryCount];
 139  
 140              $lastLineStart = $position + $entryCount * 20;
 141              $this->reader->reset($lastLineStart);
 142          }
 143  
 144          // reset after the last correct parsed line
 145          $this->reader->reset($lastLineStart);
 146  
 147          if (\count($subSections) === 0) {
 148              throw new CrossReferenceException(
 149                  'No entries found in cross-reference.',
 150                  CrossReferenceException::NO_ENTRIES
 151              );
 152          }
 153  
 154          $this->subSections = $subSections;
 155      }
 156  
 157      /**
 158       * Fixes an invalid object number shift.
 159       *
 160       * This method can be used to repair documents with an invalid subsection header:
 161       *
 162       * <code>
 163       * xref
 164       * 1 7
 165       * 0000000000 65535 f
 166       * 0000000009 00000 n
 167       * 0000412075 00000 n
 168       * 0000412172 00000 n
 169       * 0000412359 00000 n
 170       * 0000412417 00000 n
 171       * 0000412468 00000 n
 172       * </code>
 173       *
 174       * It shall only be called on the first table.
 175       *
 176       * @return bool
 177       */
 178      public function fixFaultySubSectionShift()
 179      {
 180          $subSections = $this->getSubSections();
 181          if (\count($subSections) > 1) {
 182              return false;
 183          }
 184  
 185          $subSection = \current($subSections);
 186          if ($subSection[0] != 1) {
 187              return false;
 188          }
 189  
 190          if ($this->getOffsetFor(1) === false) {
 191              foreach ($subSections as $offset => list($startObject, $objectCount)) {
 192                  $this->subSections[$offset] = [$startObject - 1, $objectCount];
 193              }
 194              return true;
 195          }
 196  
 197          return false;
 198      }
 199  }