Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403]

   1  <?php
   2  /**
   3   * This file is part of FPDI
   4   *
   5   * @package   setasign\Fpdi
   6   * @copyright Copyright (c) 2019 Setasign - Jan Slabon (https://www.setasign.com)
   7   * @license   http://opensource.org/licenses/mit-license The MIT License
   8   */
   9  
  10  namespace setasign\Fpdi\PdfReader;
  11  
  12  use setasign\Fpdi\PdfParser\CrossReference\CrossReferenceException;
  13  use setasign\Fpdi\PdfParser\PdfParser;
  14  use setasign\Fpdi\PdfParser\PdfParserException;
  15  use setasign\Fpdi\PdfParser\Type\PdfArray;
  16  use setasign\Fpdi\PdfParser\Type\PdfDictionary;
  17  use setasign\Fpdi\PdfParser\Type\PdfIndirectObjectReference;
  18  use setasign\Fpdi\PdfParser\Type\PdfNumeric;
  19  use setasign\Fpdi\PdfParser\Type\PdfType;
  20  use setasign\Fpdi\PdfParser\Type\PdfTypeException;
  21  
  22  /**
  23   * A PDF reader class
  24   *
  25   * @package setasign\Fpdi\PdfReader
  26   */
  27  class PdfReader
  28  {
  29      /**
  30       * @var PdfParser
  31       */
  32      protected $parser;
  33  
  34      /**
  35       * @var int
  36       */
  37      protected $pageCount;
  38  
  39      /**
  40       * Indirect objects of resolved pages.
  41       *
  42       * @var PdfIndirectObjectReference[]
  43       */
  44      protected $pages = [];
  45  
  46      /**
  47       * PdfReader constructor.
  48       *
  49       * @param PdfParser $parser
  50       */
  51      public function __construct(PdfParser $parser)
  52      {
  53          $this->parser = $parser;
  54      }
  55  
  56      /**
  57       * PdfReader destructor.
  58       */
  59      public function __destruct()
  60      {
  61          if ($this->parser !== null) {
  62              /** @noinspection PhpInternalEntityUsedInspection */
  63              $this->parser->cleanUp();
  64          }
  65      }
  66  
  67      /**
  68       * Get the pdf parser instance.
  69       *
  70       * @return PdfParser
  71       */
  72      public function getParser()
  73      {
  74          return $this->parser;
  75      }
  76  
  77      /**
  78       * Get the PDF version.
  79       *
  80       * @return string
  81       * @throws PdfParserException
  82       */
  83      public function getPdfVersion()
  84      {
  85          return \implode('.', $this->parser->getPdfVersion());
  86      }
  87  
  88      /**
  89       * Get the page count.
  90       *
  91       * @return int
  92       * @throws PdfTypeException
  93       * @throws CrossReferenceException
  94       * @throws PdfParserException
  95       */
  96      public function getPageCount()
  97      {
  98          if ($this->pageCount === null) {
  99              $catalog = $this->parser->getCatalog();
 100  
 101              $pages = PdfType::resolve(PdfDictionary::get($catalog, 'Pages'), $this->parser);
 102              $count = PdfType::resolve(PdfDictionary::get($pages, 'Count'), $this->parser);
 103  
 104              $this->pageCount = PdfNumeric::ensure($count)->value;
 105          }
 106  
 107          return $this->pageCount;
 108      }
 109  
 110      /**
 111       * Get a page instance.
 112       *
 113       * @param int $pageNumber
 114       * @return Page
 115       * @throws PdfTypeException
 116       * @throws CrossReferenceException
 117       * @throws PdfParserException
 118       * @throws \InvalidArgumentException
 119       */
 120      public function getPage($pageNumber)
 121      {
 122          if (!\is_numeric($pageNumber)) {
 123              throw new \InvalidArgumentException(
 124                  'Page number needs to be a number.'
 125              );
 126          }
 127  
 128          if ($pageNumber < 1 || $pageNumber > $this->getPageCount()) {
 129              throw new \InvalidArgumentException(
 130                  \sprintf(
 131                      'Page number "%s" out of available page range (1 - %s)',
 132                      $pageNumber,
 133                      $this->getPageCount()
 134                  )
 135              );
 136          }
 137  
 138          $this->readPages();
 139  
 140          $page = $this->pages[$pageNumber - 1];
 141  
 142          if ($page instanceof PdfIndirectObjectReference) {
 143              $readPages = function ($kids) use (&$readPages) {
 144                  $kids = PdfArray::ensure($kids);
 145  
 146                  /** @noinspection LoopWhichDoesNotLoopInspection */
 147                  foreach ($kids->value as $reference) {
 148                      $reference = PdfIndirectObjectReference::ensure($reference);
 149                      $object = $this->parser->getIndirectObject($reference->value);
 150                      $type = PdfDictionary::get($object->value, 'Type');
 151  
 152                      if ($type->value === 'Pages') {
 153                          return $readPages(PdfDictionary::get($object->value, 'Kids'));
 154                      }
 155  
 156                      return $object;
 157                  }
 158  
 159                  throw new PdfReaderException(
 160                      'Kids array cannot be empty.',
 161                      PdfReaderException::KIDS_EMPTY
 162                  );
 163              };
 164  
 165              $page = $this->parser->getIndirectObject($page->value);
 166              $dict = PdfType::resolve($page, $this->parser);
 167              $type = PdfDictionary::get($dict, 'Type');
 168              if ($type->value === 'Pages') {
 169                  $kids = PdfType::resolve(PdfDictionary::get($dict, 'Kids'), $this->parser);
 170                  $page = $this->pages[$pageNumber - 1] = $readPages($kids);
 171              } else {
 172                  $this->pages[$pageNumber - 1] = $page;
 173              }
 174          }
 175  
 176          return new Page($page, $this->parser);
 177      }
 178  
 179      /**
 180       * Walk the page tree and resolve all indirect objects of all pages.
 181       *
 182       * @throws PdfTypeException
 183       * @throws CrossReferenceException
 184       * @throws PdfParserException
 185       */
 186      protected function readPages()
 187      {
 188          if (\count($this->pages) > 0) {
 189              return;
 190          }
 191  
 192          $readPages = function ($kids, $count) use (&$readPages) {
 193              $kids = PdfArray::ensure($kids);
 194              $isLeaf = $count->value === \count($kids->value);
 195  
 196              foreach ($kids->value as $reference) {
 197                  $reference = PdfIndirectObjectReference::ensure($reference);
 198  
 199                  if ($isLeaf) {
 200                      $this->pages[] = $reference;
 201                      continue;
 202                  }
 203  
 204                  $object = $this->parser->getIndirectObject($reference->value);
 205                  $type = PdfDictionary::get($object->value, 'Type');
 206  
 207                  if ($type->value === 'Pages') {
 208                      $readPages(PdfDictionary::get($object->value, 'Kids'), PdfDictionary::get($object->value, 'Count'));
 209                  } else {
 210                      $this->pages[] = $object;
 211                  }
 212              }
 213          };
 214  
 215          $catalog = $this->parser->getCatalog();
 216          $pages = PdfType::resolve(PdfDictionary::get($catalog, 'Pages'), $this->parser);
 217          $count = PdfType::resolve(PdfDictionary::get($pages, 'Count'), $this->parser);
 218          $kids = PdfType::resolve(PdfDictionary::get($pages, 'Kids'), $this->parser);
 219          $readPages($kids, $count);
 220      }
 221  }