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 311 and 401] [Versions 39 and 401]

   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          try {
 338              $contentsObject = PdfType::resolve(PdfDictionary::get($pageDict, 'Contents'), $reader->getParser(), true);
 339              $contents =  PdfType::resolve($contentsObject, $reader->getParser());
 340  
 341              // just copy the stream reference if it is only a single stream
 342              if (
 343                  ($contentsIsStream = ($contents instanceof PdfStream))
 344                  || ($contents instanceof PdfArray && \count($contents->value) === 1)
 345              ) {
 346                  if ($contentsIsStream) {
 347                      /**
 348                       * @var PdfIndirectObject $contentsObject
 349                       */
 350                      $stream = $contents;
 351                  } else {
 352                      $stream = PdfType::resolve($contents->value[0], $reader->getParser());
 353                  }
 354  
 355                  $filter = PdfDictionary::get($stream->value, 'Filter');
 356                  if (!$filter instanceof PdfNull) {
 357                      $dict->value['Filter'] = $filter;
 358                  }
 359                  $length = PdfType::resolve(PdfDictionary::get($stream->value, 'Length'), $reader->getParser());
 360                  $dict->value['Length'] = $length;
 361                  $stream->value = $dict;
 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          // Catch faulty pages and use an empty content stream
 376          } catch (FpdiException $e) {
 377              $dict->value['Length'] = PdfNumeric::create(0);
 378              $stream = PdfStream::create($dict, '');
 379          }
 380  
 381          $this->importedPages[$pageId] = [
 382              'objectNumber' => null,
 383              'readerId' => $this->currentReaderId,
 384              'id' => 'TPL' . $this->getNextTemplateId(),
 385              'width' => $width / $this->k,
 386              'height' => $height / $this->k,
 387              'stream' => $stream
 388          ];
 389  
 390          return $pageId;
 391      }
 392  
 393      /**
 394       * Draws an imported page onto the page.
 395       *
 396       * Give only one of the size parameters (width, height) to calculate the other one automatically in view to the
 397       * aspect ratio.
 398       *
 399       * @param mixed $pageId The page id
 400       * @param float|int|array $x The abscissa of upper-left corner. Alternatively you could use an assoc array
 401       *                           with the keys "x", "y", "width", "height", "adjustPageSize".
 402       * @param float|int $y The ordinate of upper-left corner.
 403       * @param float|int|null $width The width.
 404       * @param float|int|null $height The height.
 405       * @param bool $adjustPageSize
 406       * @return array The size.
 407       * @see Fpdi::getTemplateSize()
 408       */
 409      public function useImportedPage($pageId, $x = 0, $y = 0, $width = null, $height = null, $adjustPageSize = false)
 410      {
 411          if (\is_array($x)) {
 412              /** @noinspection OffsetOperationsInspection */
 413              unset($x['pageId']);
 414              \extract($x, EXTR_IF_EXISTS);
 415              /** @noinspection NotOptimalIfConditionsInspection */
 416              if (\is_array($x)) {
 417                  $x = 0;
 418              }
 419          }
 420  
 421          if (!isset($this->importedPages[$pageId])) {
 422              throw new \InvalidArgumentException('Imported page does not exist!');
 423          }
 424  
 425          $importedPage = $this->importedPages[$pageId];
 426  
 427          $originalSize = $this->getTemplateSize($pageId);
 428          $newSize = $this->getTemplateSize($pageId, $width, $height);
 429          if ($adjustPageSize) {
 430              $this->setPageFormat($newSize, $newSize['orientation']);
 431          }
 432  
 433          $this->_out(
 434              // reset standard values, translate and scale
 435              \sprintf(
 436                  'q 0 J 1 w 0 j 0 G 0 g %.4F 0 0 %.4F %.4F %.4F cm /%s Do Q',
 437                  ($newSize['width'] / $originalSize['width']),
 438                  ($newSize['height'] / $originalSize['height']),
 439                  $x * $this->k,
 440                  ($this->h - $y - $newSize['height']) * $this->k,
 441                  $importedPage['id']
 442              )
 443          );
 444  
 445          return $newSize;
 446      }
 447  
 448      /**
 449       * Get the size of an imported page.
 450       *
 451       * Give only one of the size parameters (width, height) to calculate the other one automatically in view to the
 452       * aspect ratio.
 453       *
 454       * @param mixed $tpl The template id
 455       * @param float|int|null $width The width.
 456       * @param float|int|null $height The height.
 457       * @return array|bool An array with following keys: width, height, 0 (=width), 1 (=height), orientation (L or P)
 458       */
 459      public function getImportedPageSize($tpl, $width = null, $height = null)
 460      {
 461          if (isset($this->importedPages[$tpl])) {
 462              $importedPage = $this->importedPages[$tpl];
 463  
 464              if ($width === null && $height === null) {
 465                  $width = $importedPage['width'];
 466                  $height = $importedPage['height'];
 467              } elseif ($width === null) {
 468                  $width = $height * $importedPage['width'] / $importedPage['height'];
 469              }
 470  
 471              if ($height  === null) {
 472                  $height = $width * $importedPage['height'] / $importedPage['width'];
 473              }
 474  
 475              if ($height <= 0. || $width <= 0.) {
 476                  throw new \InvalidArgumentException('Width or height parameter needs to be larger than zero.');
 477              }
 478  
 479              return [
 480                  'width' => $width,
 481                  'height' => $height,
 482                  0 => $width,
 483                  1 => $height,
 484                  'orientation' => $width > $height ? 'L' : 'P'
 485              ];
 486          }
 487  
 488          return false;
 489      }
 490  
 491      /**
 492       * Writes a PdfType object to the resulting buffer.
 493       *
 494       * @param PdfType $value
 495       * @throws PdfTypeException
 496       */
 497      protected function writePdfType(PdfType $value)
 498      {
 499          if ($value instanceof PdfNumeric) {
 500              if (\is_int($value->value)) {
 501                  $this->_put($value->value . ' ', false);
 502              } else {
 503                  $this->_put(\rtrim(\rtrim(\sprintf('%.5F', $value->value), '0'), '.') . ' ', false);
 504              }
 505          } elseif ($value instanceof PdfName) {
 506              $this->_put('/' . $value->value . ' ', false);
 507          } elseif ($value instanceof PdfString) {
 508              $this->_put('(' . $value->value . ')', false);
 509          } elseif ($value instanceof PdfHexString) {
 510              $this->_put('<' . $value->value . '>');
 511          } elseif ($value instanceof PdfBoolean) {
 512              $this->_put($value->value ? 'true ' : 'false ', false);
 513          } elseif ($value instanceof PdfArray) {
 514              $this->_put('[', false);
 515              foreach ($value->value as $entry) {
 516                  $this->writePdfType($entry);
 517              }
 518              $this->_put(']');
 519          } elseif ($value instanceof PdfDictionary) {
 520              $this->_put('<<', false);
 521              foreach ($value->value as $name => $entry) {
 522                  $this->_put('/' . $name . ' ', false);
 523                  $this->writePdfType($entry);
 524              }
 525              $this->_put('>>');
 526          } elseif ($value instanceof PdfToken) {
 527              $this->_put($value->value);
 528          } elseif ($value instanceof PdfNull) {
 529              $this->_put('null ');
 530          } elseif ($value instanceof PdfStream) {
 531              /**
 532               * @var $value PdfStream
 533               */
 534              $this->writePdfType($value->value);
 535              $this->_put('stream');
 536              $this->_put($value->getStream());
 537              $this->_put('endstream');
 538          } elseif ($value instanceof PdfIndirectObjectReference) {
 539              if (!isset($this->objectMap[$this->currentReaderId])) {
 540                  $this->objectMap[$this->currentReaderId] = [];
 541              }
 542  
 543              if (!isset($this->objectMap[$this->currentReaderId][$value->value])) {
 544                  $this->objectMap[$this->currentReaderId][$value->value] = ++$this->n;
 545                  $this->objectsToCopy[$this->currentReaderId][] = $value->value;
 546              }
 547  
 548              $this->_put($this->objectMap[$this->currentReaderId][$value->value] . ' 0 R ', false);
 549          } elseif ($value instanceof PdfIndirectObject) {
 550              /**
 551               * @var PdfIndirectObject $value
 552               */
 553              $n = $this->objectMap[$this->currentReaderId][$value->objectNumber];
 554              $this->_newobj($n);
 555              $this->writePdfType($value->value);
 556              $this->_put('endobj');
 557          }
 558      }
 559  }