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;
  11  
  12  use setasign\Fpdi\PdfParser\CrossReference\CrossReferenceException;
  13  use setasign\Fpdi\PdfParser\Filter\FilterException;
  14  use setasign\Fpdi\PdfParser\PdfParser;
  15  use setasign\Fpdi\PdfParser\PdfParserException;
  16  use setasign\Fpdi\PdfParser\StreamReader;
  17  use setasign\Fpdi\PdfParser\Type\PdfArray;
  18  use setasign\Fpdi\PdfParser\Type\PdfBoolean;
  19  use setasign\Fpdi\PdfParser\Type\PdfDictionary;
  20  use setasign\Fpdi\PdfParser\Type\PdfHexString;
  21  use setasign\Fpdi\PdfParser\Type\PdfIndirectObject;
  22  use setasign\Fpdi\PdfParser\Type\PdfIndirectObjectReference;
  23  use setasign\Fpdi\PdfParser\Type\PdfName;
  24  use setasign\Fpdi\PdfParser\Type\PdfNull;
  25  use setasign\Fpdi\PdfParser\Type\PdfNumeric;
  26  use setasign\Fpdi\PdfParser\Type\PdfStream;
  27  use setasign\Fpdi\PdfParser\Type\PdfString;
  28  use setasign\Fpdi\PdfParser\Type\PdfToken;
  29  use setasign\Fpdi\PdfParser\Type\PdfType;
  30  use setasign\Fpdi\PdfParser\Type\PdfTypeException;
  31  use setasign\Fpdi\PdfReader\PageBoundaries;
  32  use setasign\Fpdi\PdfReader\PdfReader;
  33  use setasign\Fpdi\PdfReader\PdfReaderException;
  34  use /* This namespace/class is used by the commercial FPDI PDF-Parser add-on. */
  35      /** @noinspection PhpUndefinedClassInspection */
  36      /** @noinspection PhpUndefinedNamespaceInspection */
  37      setasign\FpdiPdfParser\PdfParser\PdfParser as FpdiPdfParser;
  38  
  39  /**
  40   * The FpdiTrait
  41   *
  42   * This trait offers the core functionalities of FPDI. By passing them to a trait we can reuse it with e.g. TCPDF in a
  43   * very easy way.
  44   *
  45   * @package setasign\Fpdi
  46   */
  47  trait FpdiTrait
  48  {
  49      /**
  50       * The pdf reader instances.
  51       *
  52       * @var PdfReader[]
  53       */
  54      protected $readers = [];
  55  
  56      /**
  57       * Instances created internally.
  58       *
  59       * @var array
  60       */
  61      protected $createdReaders = [];
  62  
  63      /**
  64       * The current reader id.
  65       *
  66       * @var string
  67       */
  68      protected $currentReaderId;
  69  
  70      /**
  71       * Data of all imported pages.
  72       *
  73       * @var array
  74       */
  75      protected $importedPages = [];
  76  
  77      /**
  78       * A map from object numbers of imported objects to new assigned object numbers by FPDF.
  79       *
  80       * @var array
  81       */
  82      protected $objectMap = [];
  83  
  84      /**
  85       * An array with information about objects, which needs to be copied to the resulting document.
  86       *
  87       * @var array
  88       */
  89      protected $objectsToCopy = [];
  90  
  91      /**
  92       * Release resources and file handles.
  93       *
  94       * This method is called internally when the document is created successfully. By default it only cleans up
  95       * stream reader instances which were created internally.
  96       *
  97       * @param bool $allReaders
  98       */
  99      public function cleanUp($allReaders = false)
 100      {
 101          $readers = $allReaders ? array_keys($this->readers) : $this->createdReaders;
 102          foreach ($readers as $id) {
 103              $this->readers[$id]->getParser()->getStreamReader()->cleanUp();
 104              unset($this->readers[$id]);
 105          }
 106  
 107          $this->createdReaders= [];
 108      }
 109  
 110      /**
 111       * Set the minimal PDF version.
 112       *
 113       * @param string $pdfVersion
 114       */
 115      protected function setMinPdfVersion($pdfVersion)
 116      {
 117          if (\version_compare($pdfVersion, $this->PDFVersion, '>')) {
 118              $this->PDFVersion = $pdfVersion;
 119          }
 120      }
 121  
 122      /** @noinspection PhpUndefinedClassInspection */
 123      /**
 124       * Get a new pdf parser instance.
 125       *
 126       * @param StreamReader $streamReader
 127       * @return PdfParser|FpdiPdfParser
 128       */
 129      protected function getPdfParserInstance(StreamReader $streamReader)
 130      {
 131          /** @noinspection PhpUndefinedClassInspection */
 132          if (\class_exists(FpdiPdfParser::class)) {
 133              /** @noinspection PhpUndefinedClassInspection */
 134              return new FpdiPdfParser($streamReader);
 135          }
 136  
 137          return new PdfParser($streamReader);
 138      }
 139  
 140      /**
 141       * Get an unique reader id by the $file parameter.
 142       *
 143       * @param string|resource|PdfReader|StreamReader $file An open file descriptor, a path to a file, a PdfReader
 144       *                                                     instance or a StreamReader instance.
 145       * @return string
 146       */
 147      protected function getPdfReaderId($file)
 148      {
 149          if (\is_resource($file)) {
 150              $id = (string) $file;
 151          } elseif (\is_string($file)) {
 152              $id = \realpath($file);
 153              if ($id === false) {
 154                  $id = $file;
 155              }
 156          } elseif (\is_object($file)) {
 157              $id = \spl_object_hash($file);
 158          } else {
 159              throw new \InvalidArgumentException(
 160                  \sprintf('Invalid type in $file parameter (%s)', \gettype($file))
 161              );
 162          }
 163  
 164          /** @noinspection OffsetOperationsInspection */
 165          if (isset($this->readers[$id])) {
 166              return $id;
 167          }
 168  
 169          if (\is_resource($file)) {
 170              $streamReader = new StreamReader($file);
 171          } elseif (\is_string($file)) {
 172              $streamReader = StreamReader::createByFile($file);
 173              $this->createdReaders[] = $id;
 174          } else {
 175              $streamReader = $file;
 176          }
 177  
 178          $reader = new PdfReader($this->getPdfParserInstance($streamReader));
 179          /** @noinspection OffsetOperationsInspection */
 180          $this->readers[$id] = $reader;
 181  
 182          return $id;
 183      }
 184  
 185      /**
 186       * Get a pdf reader instance by its id.
 187       *
 188       * @param string $id
 189       * @return PdfReader
 190       */
 191      protected function getPdfReader($id)
 192      {
 193          if (isset($this->readers[$id])) {
 194              return $this->readers[$id];
 195          }
 196  
 197          throw new \InvalidArgumentException(
 198              \sprintf('No pdf reader with the given id (%s) exists.', $id)
 199          );
 200      }
 201  
 202      /**
 203       * Set the source PDF file.
 204       *
 205       * @param string|resource|StreamReader $file Path to the file or a stream resource or a StreamReader instance.
 206       * @return int The page count of the PDF document.
 207       * @throws PdfParserException
 208       */
 209      public function setSourceFile($file)
 210      {
 211          $this->currentReaderId = $this->getPdfReaderId($file);
 212          $this->objectsToCopy[$this->currentReaderId] = [];
 213  
 214          $reader = $this->getPdfReader($this->currentReaderId);
 215          $this->setMinPdfVersion($reader->getPdfVersion());
 216  
 217          return $reader->getPageCount();
 218      }
 219  
 220      /**
 221       * Imports a page.
 222       *
 223       * @param int $pageNumber The page number.
 224       * @param string $box The page boundary to import. Default set to PageBoundaries::CROP_BOX.
 225       * @param bool $groupXObject Define the form XObject as a group XObject to support transparency (if used).
 226       * @return string A unique string identifying the imported page.
 227       * @throws CrossReferenceException
 228       * @throws FilterException
 229       * @throws PdfParserException
 230       * @throws PdfTypeException
 231       * @throws PdfReaderException
 232       * @see PageBoundaries
 233       */
 234      public function importPage($pageNumber, $box = PageBoundaries::CROP_BOX, $groupXObject = true)
 235      {
 236          if (null === $this->currentReaderId) {
 237              throw new \BadMethodCallException('No reader initiated. Call setSourceFile() first.');
 238          }
 239  
 240          $pageId = $this->currentReaderId;
 241  
 242          $pageNumber = (int)$pageNumber;
 243          $pageId .= '|' . $pageNumber . '|' . ($groupXObject ? '1' : '0');
 244  
 245          // for backwards compatibility with FPDI 1
 246          $box = \ltrim($box, '/');
 247          if (!PageBoundaries::isValidName($box)) {
 248              throw new \InvalidArgumentException(
 249                  \sprintf('Box name is invalid: "%s"', $box)
 250              );
 251          }
 252  
 253          $pageId .= '|' . $box;
 254  
 255          if (isset($this->importedPages[$pageId])) {
 256              return $pageId;
 257          }
 258  
 259          $reader = $this->getPdfReader($this->currentReaderId);
 260          $page = $reader->getPage($pageNumber);
 261  
 262          $bbox = $page->getBoundary($box);
 263          if ($bbox === false) {
 264              throw new PdfReaderException(
 265                  \sprintf("Page doesn't have a boundary box (%s).", $box),
 266                  PdfReaderException::MISSING_DATA
 267              );
 268          }
 269  
 270          $dict = new PdfDictionary();
 271          $dict->value['Type'] = PdfName::create('XObject');
 272          $dict->value['Subtype'] = PdfName::create('Form');
 273          $dict->value['FormType'] = PdfNumeric::create(1);
 274          $dict->value['BBox'] = $bbox->toPdfArray();
 275  
 276          if ($groupXObject) {
 277              $this->setMinPdfVersion('1.4');
 278              $dict->value['Group'] = PdfDictionary::create([
 279                  'Type' => PdfName::create('Group'),
 280                  'S' => PdfName::create('Transparency')
 281              ]);
 282          }
 283  
 284          $resources = $page->getAttribute('Resources');
 285          if ($resources !== null) {
 286              $dict->value['Resources'] = $resources;
 287          }
 288  
 289          list($width, $height) = $page->getWidthAndHeight($box);
 290  
 291          $a = 1;
 292          $b = 0;
 293          $c = 0;
 294          $d = 1;
 295          $e = -$bbox->getLlx();
 296          $f = -$bbox->getLly();
 297  
 298          $rotation = $page->getRotation();
 299  
 300          if ($rotation !== 0) {
 301              $rotation *= -1;
 302              $angle = $rotation * M_PI/180;
 303              $a = \cos($angle);
 304              $b = \sin($angle);
 305              $c = -$b;
 306              $d = $a;
 307  
 308              switch ($rotation) {
 309                  case -90:
 310                      $e = -$bbox->getLly();
 311                      $f = $bbox->getUrx();
 312                      break;
 313                  case -180:
 314                      $e = $bbox->getUrx();
 315                      $f = $bbox->getUry();
 316                      break;
 317                  case -270:
 318                      $e = $bbox->getUry();
 319                      $f = -$bbox->getLlx();
 320                      break;
 321              }
 322          }
 323  
 324          // we need to rotate/translate
 325          if ($a != 1 || $b != 0 || $c != 0 || $d != 1 || $e != 0 || $f != 0) {
 326              $dict->value['Matrix'] = PdfArray::create([
 327                  PdfNumeric::create($a), PdfNumeric::create($b), PdfNumeric::create($c),
 328                  PdfNumeric::create($d), PdfNumeric::create($e), PdfNumeric::create($f)
 329              ]);
 330          }
 331  
 332          // try to use the existing content stream
 333          $pageDict = $page->getPageDictionary();
 334  
 335          $contentsObject = PdfType::resolve(PdfDictionary::get($pageDict, 'Contents'), $reader->getParser(), true);
 336          $contents =  PdfType::resolve($contentsObject, $reader->getParser());
 337  
 338          // just copy the stream reference if it is only a single stream
 339          if (($contentsIsStream = ($contents instanceof PdfStream))
 340              || ($contents instanceof PdfArray && \count($contents->value) === 1)
 341          ) {
 342              if ($contentsIsStream) {
 343                  /**
 344                   * @var PdfIndirectObject $contentsObject
 345                   */
 346                  $stream = $contents;
 347              } else {
 348                  $stream = PdfType::resolve($contents->value[0], $reader->getParser());
 349              }
 350  
 351              $filter = PdfDictionary::get($stream->value, 'Filter');
 352              if (!$filter instanceof PdfNull) {
 353                  $dict->value['Filter'] = $filter;
 354              }
 355              $length = PdfType::resolve(PdfDictionary::get($stream->value, 'Length'), $reader->getParser());
 356              $dict->value['Length'] = $length;
 357              $stream->value = $dict;
 358  
 359          // otherwise extract it from the array and re-compress the whole stream
 360          } else {
 361              $streamContent = $this->compress
 362                  ? \gzcompress($page->getContentStream())
 363                  : $page->getContentStream();
 364  
 365              $dict->value['Length'] = PdfNumeric::create(\strlen($streamContent));
 366              if ($this->compress) {
 367                  $dict->value['Filter'] = PdfName::create('FlateDecode');
 368              }
 369  
 370              $stream = PdfStream::create($dict, $streamContent);
 371          }
 372  
 373          $this->importedPages[$pageId] = [
 374              'objectNumber' => null,
 375              'readerId' => $this->currentReaderId,
 376              'id' => 'TPL' . $this->getNextTemplateId(),
 377              'width' => $width / $this->k,
 378              'height' => $height / $this->k,
 379              'stream' => $stream
 380          ];
 381  
 382          return $pageId;
 383      }
 384  
 385      /**
 386       * Draws an imported page onto the page.
 387       *
 388       * Give only one of the size parameters (width, height) to calculate the other one automatically in view to the
 389       * aspect ratio.
 390       *
 391       * @param mixed $pageId The page id
 392       * @param float|int|array $x The abscissa of upper-left corner. Alternatively you could use an assoc array
 393       *                           with the keys "x", "y", "width", "height", "adjustPageSize".
 394       * @param float|int $y The ordinate of upper-left corner.
 395       * @param float|int|null $width The width.
 396       * @param float|int|null $height The height.
 397       * @param bool $adjustPageSize
 398       * @return array The size.
 399       * @see Fpdi::getTemplateSize()
 400       */
 401      public function useImportedPage($pageId, $x = 0, $y = 0, $width = null, $height = null, $adjustPageSize = false)
 402      {
 403          if (\is_array($x)) {
 404              /** @noinspection OffsetOperationsInspection */
 405              unset($x['pageId']);
 406              \extract($x, EXTR_IF_EXISTS);
 407              /** @noinspection NotOptimalIfConditionsInspection */
 408              if (\is_array($x)) {
 409                  $x = 0;
 410              }
 411          }
 412  
 413          if (!isset($this->importedPages[$pageId])) {
 414              throw new \InvalidArgumentException('Imported page does not exist!');
 415          }
 416  
 417          $importedPage = $this->importedPages[$pageId];
 418  
 419          $originalSize = $this->getTemplateSize($pageId);
 420          $newSize = $this->getTemplateSize($pageId, $width, $height);
 421          if ($adjustPageSize) {
 422              $this->setPageFormat($newSize, $newSize['orientation']);
 423          }
 424  
 425          $this->_out(
 426              // reset standard values, translate and scale
 427              \sprintf(
 428                  'q 0 J 1 w 0 j 0 G 0 g %.4F 0 0 %.4F %.4F %.4F cm /%s Do Q',
 429                  ($newSize['width'] / $originalSize['width']),
 430                  ($newSize['height'] / $originalSize['height']),
 431                  $x * $this->k,
 432                  ($this->h - $y - $newSize['height']) * $this->k,
 433                  $importedPage['id']
 434              )
 435          );
 436  
 437          return $newSize;
 438      }
 439  
 440      /**
 441       * Get the size of an imported page.
 442       *
 443       * Give only one of the size parameters (width, height) to calculate the other one automatically in view to the
 444       * aspect ratio.
 445       *
 446       * @param mixed $tpl The template id
 447       * @param float|int|null $width The width.
 448       * @param float|int|null $height The height.
 449       * @return array|bool An array with following keys: width, height, 0 (=width), 1 (=height), orientation (L or P)
 450       */
 451      public function getImportedPageSize($tpl, $width = null, $height = null)
 452      {
 453          if (isset($this->importedPages[$tpl])) {
 454              $importedPage = $this->importedPages[$tpl];
 455  
 456              if ($width === null && $height === null) {
 457                  $width = $importedPage['width'];
 458                  $height = $importedPage['height'];
 459              } elseif ($width === null) {
 460                  $width = $height * $importedPage['width'] / $importedPage['height'];
 461              }
 462  
 463              if ($height  === null) {
 464                  $height = $width * $importedPage['height'] / $importedPage['width'];
 465              }
 466  
 467              if ($height <= 0. || $width <= 0.) {
 468                  throw new \InvalidArgumentException('Width or height parameter needs to be larger than zero.');
 469              }
 470  
 471              return [
 472                  'width' => $width,
 473                  'height' => $height,
 474                  0 => $width,
 475                  1 => $height,
 476                  'orientation' => $width > $height ? 'L' : 'P'
 477              ];
 478          }
 479  
 480          return false;
 481      }
 482  
 483      /**
 484       * Writes a PdfType object to the resulting buffer.
 485       *
 486       * @param PdfType $value
 487       * @throws PdfTypeException
 488       */
 489      protected function writePdfType(PdfType $value)
 490      {
 491          if ($value instanceof PdfNumeric) {
 492              if (\is_int($value->value)) {
 493                  $this->_put($value->value . ' ', false);
 494              } else {
 495                  $this->_put(\rtrim(\rtrim(\sprintf('%.5F', $value->value), '0'), '.') . ' ', false);
 496              }
 497  
 498          } elseif ($value instanceof PdfName) {
 499              $this->_put('/' . $value->value . ' ', false);
 500  
 501          } elseif ($value instanceof PdfString) {
 502              $this->_put('(' . $value->value . ')', false);
 503  
 504          } elseif ($value instanceof PdfHexString) {
 505              $this->_put('<' . $value->value . '>');
 506  
 507          } elseif ($value instanceof PdfBoolean) {
 508              $this->_put($value->value ? 'true ' : 'false ', false);
 509  
 510          } elseif ($value instanceof PdfArray) {
 511              $this->_put('[', false);
 512              foreach ($value->value as $entry) {
 513                  $this->writePdfType($entry);
 514              }
 515              $this->_put(']');
 516  
 517          } elseif ($value instanceof PdfDictionary) {
 518              $this->_put('<<', false);
 519              foreach ($value->value as $name => $entry) {
 520                  $this->_put('/' . $name . ' ', false);
 521                  $this->writePdfType($entry);
 522              }
 523              $this->_put('>>');
 524  
 525          } elseif ($value instanceof PdfToken) {
 526              $this->_put($value->value);
 527  
 528          } elseif ($value instanceof PdfNull) {
 529              $this->_put('null ');
 530  
 531          } elseif ($value instanceof PdfStream) {
 532              /**
 533               * @var $value PdfStream
 534               */
 535              $this->writePdfType($value->value);
 536              $this->_put('stream');
 537              $this->_put($value->getStream());
 538              $this->_put('endstream');
 539  
 540          } elseif ($value instanceof PdfIndirectObjectReference) {
 541              if (!isset($this->objectMap[$this->currentReaderId])) {
 542                  $this->objectMap[$this->currentReaderId] = [];
 543              }
 544  
 545              if (!isset($this->objectMap[$this->currentReaderId][$value->value])) {
 546                  $this->objectMap[$this->currentReaderId][$value->value] = ++$this->n;
 547                  $this->objectsToCopy[$this->currentReaderId][] = $value->value;
 548              }
 549  
 550              $this->_put($this->objectMap[$this->currentReaderId][$value->value] . ' 0 R ', false);
 551  
 552          } elseif ($value instanceof PdfIndirectObject) {
 553              /**
 554               * @var $value PdfIndirectObject
 555               */
 556              $n = $this->objectMap[$this->currentReaderId][$value->objectNumber];
 557              $this->_newobj($n);
 558              $this->writePdfType($value->value);
 559              $this->_put('endobj');
 560          }
 561      }
 562  }