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