1<?php 2 3/** 4 * This file is part of FPDI 5 * 6 * @package setasign\Fpdi 7 * @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com) 8 * @license http://opensource.org/licenses/mit-license The MIT License 9 */ 10 11namespace setasign\Fpdi; 12 13use setasign\Fpdi\PdfParser\CrossReference\CrossReferenceException; 14use setasign\Fpdi\PdfParser\Filter\FilterException; 15use setasign\Fpdi\PdfParser\PdfParser; 16use setasign\Fpdi\PdfParser\PdfParserException; 17use setasign\Fpdi\PdfParser\StreamReader; 18use setasign\Fpdi\PdfParser\Type\PdfArray; 19use setasign\Fpdi\PdfParser\Type\PdfBoolean; 20use setasign\Fpdi\PdfParser\Type\PdfDictionary; 21use setasign\Fpdi\PdfParser\Type\PdfHexString; 22use setasign\Fpdi\PdfParser\Type\PdfIndirectObject; 23use setasign\Fpdi\PdfParser\Type\PdfIndirectObjectReference; 24use setasign\Fpdi\PdfParser\Type\PdfName; 25use setasign\Fpdi\PdfParser\Type\PdfNull; 26use setasign\Fpdi\PdfParser\Type\PdfNumeric; 27use setasign\Fpdi\PdfParser\Type\PdfStream; 28use setasign\Fpdi\PdfParser\Type\PdfString; 29use setasign\Fpdi\PdfParser\Type\PdfToken; 30use setasign\Fpdi\PdfParser\Type\PdfType; 31use setasign\Fpdi\PdfParser\Type\PdfTypeException; 32use setasign\Fpdi\PdfReader\DataStructure\Rectangle; 33use setasign\Fpdi\PdfReader\PageBoundaries; 34use setasign\Fpdi\PdfReader\PdfReader; 35use setasign\Fpdi\PdfReader\PdfReaderException; 36use /* This namespace/class is used by the commercial FPDI PDF-Parser add-on. */ 37 /** @noinspection PhpUndefinedClassInspection */ 38 /** @noinspection PhpUndefinedNamespaceInspection */ 39 setasign\FpdiPdfParser\PdfParser\PdfParser as FpdiPdfParser; 40 41/** 42 * The FpdiTrait 43 * 44 * This trait offers the core functionalities of FPDI. By passing them to a trait we can reuse it with e.g. TCPDF in a 45 * very easy way. 46 */ 47trait 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|null 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 * @param array $parserParams Individual parameters passed to the parser instance. 128 * @return PdfParser|FpdiPdfParser 129 */ 130 protected function getPdfParserInstance(StreamReader $streamReader, array $parserParams = []) 131 { 132 // note: if you get an exception here - turn off errors/warnings on not found classes for your autoloader. 133 // psr-4 (https://www.php-fig.org/psr/psr-4/) says: Autoloader implementations MUST NOT throw 134 // exceptions, MUST NOT raise errors of any level, and SHOULD NOT return a value. 135 /** @noinspection PhpUndefinedClassInspection */ 136 if (\class_exists(FpdiPdfParser::class)) { 137 /** @noinspection PhpUndefinedClassInspection */ 138 return new FpdiPdfParser($streamReader, $parserParams); 139 } 140 141 return new PdfParser($streamReader); 142 } 143 144 /** 145 * Get an unique reader id by the $file parameter. 146 * 147 * @param string|resource|PdfReader|StreamReader $file An open file descriptor, a path to a file, a PdfReader 148 * instance or a StreamReader instance. 149 * @param array $parserParams Individual parameters passed to the parser instance. 150 * @return string 151 */ 152 protected function getPdfReaderId($file, array $parserParams = []) 153 { 154 if (\is_resource($file)) { 155 $id = (string) $file; 156 } elseif (\is_string($file)) { 157 $id = \realpath($file); 158 if ($id === false) { 159 $id = $file; 160 } 161 } elseif (\is_object($file)) { 162 $id = \spl_object_hash($file); 163 } else { 164 throw new \InvalidArgumentException( 165 \sprintf('Invalid type in $file parameter (%s)', \gettype($file)) 166 ); 167 } 168 169 /** @noinspection OffsetOperationsInspection */ 170 if (isset($this->readers[$id])) { 171 return $id; 172 } 173 174 if (\is_resource($file)) { 175 $streamReader = new StreamReader($file); 176 } elseif (\is_string($file)) { 177 $streamReader = StreamReader::createByFile($file); 178 $this->createdReaders[] = $id; 179 } else { 180 $streamReader = $file; 181 } 182 183 $reader = new PdfReader($this->getPdfParserInstance($streamReader, $parserParams)); 184 /** @noinspection OffsetOperationsInspection */ 185 $this->readers[$id] = $reader; 186 187 return $id; 188 } 189 190 /** 191 * Get a pdf reader instance by its id. 192 * 193 * @param string $id 194 * @return PdfReader 195 */ 196 protected function getPdfReader($id) 197 { 198 if (isset($this->readers[$id])) { 199 return $this->readers[$id]; 200 } 201 202 throw new \InvalidArgumentException( 203 \sprintf('No pdf reader with the given id (%s) exists.', $id) 204 ); 205 } 206 207 /** 208 * Set the source PDF file. 209 * 210 * @param string|resource|StreamReader $file Path to the file or a stream resource or a StreamReader instance. 211 * @return int The page count of the PDF document. 212 * @throws PdfParserException 213 */ 214 public function setSourceFile($file) 215 { 216 return $this->setSourceFileWithParserParams($file); 217 } 218 219 /** 220 * Set the source PDF file with parameters which are passed to the parser instance. 221 * 222 * This method allows us to pass e.g. authentication information to the parser instance. 223 * 224 * @param string|resource|StreamReader $file Path to the file or a stream resource or a StreamReader instance. 225 * @param array $parserParams Individual parameters passed to the parser instance. 226 * @return int The page count of the PDF document. 227 * @throws CrossReferenceException 228 * @throws PdfParserException 229 * @throws PdfTypeException 230 */ 231 public function setSourceFileWithParserParams($file, array $parserParams = []) 232 { 233 $this->currentReaderId = $this->getPdfReaderId($file, $parserParams); 234 $this->objectsToCopy[$this->currentReaderId] = []; 235 236 $reader = $this->getPdfReader($this->currentReaderId); 237 $this->setMinPdfVersion($reader->getPdfVersion()); 238 239 return $reader->getPageCount(); 240 } 241 242 /** 243 * Imports a page. 244 * 245 * @param int $pageNumber The page number. 246 * @param string $box The page boundary to import. Default set to PageBoundaries::CROP_BOX. 247 * @param bool $groupXObject Define the form XObject as a group XObject to support transparency (if used). 248 * @param bool $importExternalLinks Define whether external links are imported or not. 249 * @return string A unique string identifying the imported page. 250 * @throws CrossReferenceException 251 * @throws FilterException 252 * @throws PdfParserException 253 * @throws PdfTypeException 254 * @throws PdfReaderException 255 * @see PageBoundaries 256 */ 257 public function importPage( 258 $pageNumber, 259 $box = PageBoundaries::CROP_BOX, 260 $groupXObject = true, 261 $importExternalLinks = false 262 ) { 263 if ($this->currentReaderId === null) { 264 throw new \BadMethodCallException('No reader initiated. Call setSourceFile() first.'); 265 } 266 267 $pageId = $this->currentReaderId; 268 269 $pageNumber = (int)$pageNumber; 270 $pageId .= '|' . $pageNumber . '|' . ($groupXObject ? '1' : '0') . '|' . ($importExternalLinks ? '1' : '0'); 271 272 // for backwards compatibility with FPDI 1 273 $box = \ltrim($box, '/'); 274 if (!PageBoundaries::isValidName($box)) { 275 throw new \InvalidArgumentException( 276 \sprintf('Box name is invalid: "%s"', $box) 277 ); 278 } 279 280 $pageId .= '|' . $box; 281 282 if (isset($this->importedPages[$pageId])) { 283 return $pageId; 284 } 285 286 $reader = $this->getPdfReader($this->currentReaderId); 287 $page = $reader->getPage($pageNumber); 288 289 $bbox = $page->getBoundary($box); 290 if ($bbox === false) { 291 throw new PdfReaderException( 292 \sprintf("Page doesn't have a boundary box (%s).", $box), 293 PdfReaderException::MISSING_DATA 294 ); 295 } 296 297 $dict = new PdfDictionary(); 298 $dict->value['Type'] = PdfName::create('XObject'); 299 $dict->value['Subtype'] = PdfName::create('Form'); 300 $dict->value['FormType'] = PdfNumeric::create(1); 301 $dict->value['BBox'] = $bbox->toPdfArray(); 302 303 if ($groupXObject) { 304 $this->setMinPdfVersion('1.4'); 305 $dict->value['Group'] = PdfDictionary::create([ 306 'Type' => PdfName::create('Group'), 307 'S' => PdfName::create('Transparency') 308 ]); 309 } 310 311 $resources = $page->getAttribute('Resources'); 312 if ($resources !== null) { 313 $dict->value['Resources'] = $resources; 314 } 315 316 list($width, $height) = $page->getWidthAndHeight($box); 317 318 $a = 1; 319 $b = 0; 320 $c = 0; 321 $d = 1; 322 $e = -$bbox->getLlx(); 323 $f = -$bbox->getLly(); 324 325 $rotation = $page->getRotation(); 326 327 if ($rotation !== 0) { 328 $rotation *= -1; 329 $angle = $rotation * M_PI / 180; 330 $a = \cos($angle); 331 $b = \sin($angle); 332 $c = -$b; 333 $d = $a; 334 335 switch ($rotation) { 336 case -90: 337 $e = -$bbox->getLly(); 338 $f = $bbox->getUrx(); 339 break; 340 case -180: 341 $e = $bbox->getUrx(); 342 $f = $bbox->getUry(); 343 break; 344 case -270: 345 $e = $bbox->getUry(); 346 $f = -$bbox->getLlx(); 347 break; 348 } 349 } 350 351 // we need to rotate/translate 352 if ($a != 1 || $b != 0 || $c != 0 || $d != 1 || $e != 0 || $f != 0) { 353 $dict->value['Matrix'] = PdfArray::create([ 354 PdfNumeric::create($a), PdfNumeric::create($b), PdfNumeric::create($c), 355 PdfNumeric::create($d), PdfNumeric::create($e), PdfNumeric::create($f) 356 ]); 357 } 358 359 // try to use the existing content stream 360 $pageDict = $page->getPageDictionary(); 361 362 try { 363 $contentsObject = PdfType::resolve(PdfDictionary::get($pageDict, 'Contents'), $reader->getParser(), true); 364 $contents = PdfType::resolve($contentsObject, $reader->getParser()); 365 366 // just copy the stream reference if it is only a single stream 367 if ( 368 ($contentsIsStream = ($contents instanceof PdfStream)) 369 || ($contents instanceof PdfArray && \count($contents->value) === 1) 370 ) { 371 if ($contentsIsStream) { 372 /** 373 * @var PdfIndirectObject $contentsObject 374 */ 375 $stream = $contents; 376 } else { 377 $stream = PdfType::resolve($contents->value[0], $reader->getParser()); 378 } 379 380 $filter = PdfDictionary::get($stream->value, 'Filter'); 381 if (!$filter instanceof PdfNull) { 382 $dict->value['Filter'] = $filter; 383 } 384 $length = PdfType::resolve(PdfDictionary::get($stream->value, 'Length'), $reader->getParser()); 385 $dict->value['Length'] = $length; 386 $stream->value = $dict; 387 // otherwise extract it from the array and re-compress the whole stream 388 } else { 389 $streamContent = $this->compress 390 ? \gzcompress($page->getContentStream()) 391 : $page->getContentStream(); 392 393 $dict->value['Length'] = PdfNumeric::create(\strlen($streamContent)); 394 if ($this->compress) { 395 $dict->value['Filter'] = PdfName::create('FlateDecode'); 396 } 397 398 $stream = PdfStream::create($dict, $streamContent); 399 } 400 // Catch faulty pages and use an empty content stream 401 } catch (FpdiException $e) { 402 $dict->value['Length'] = PdfNumeric::create(0); 403 $stream = PdfStream::create($dict, ''); 404 } 405 406 $externalLinks = []; 407 if ($importExternalLinks) { 408 $externalLinks = $page->getExternalLinks($box); 409 } 410 411 $this->importedPages[$pageId] = [ 412 'objectNumber' => null, 413 'readerId' => $this->currentReaderId, 414 'id' => 'TPL' . $this->getNextTemplateId(), 415 'width' => $width / $this->k, 416 'height' => $height / $this->k, 417 'stream' => $stream, 418 'externalLinks' => $externalLinks 419 ]; 420 421 return $pageId; 422 } 423 424 /** 425 * Draws an imported page onto the page. 426 * 427 * Give only one of the size parameters (width, height) to calculate the other one automatically in view to the 428 * aspect ratio. 429 * 430 * @param mixed $pageId The page id 431 * @param float|int|array $x The abscissa of upper-left corner. Alternatively you could use an assoc array 432 * with the keys "x", "y", "width", "height", "adjustPageSize". 433 * @param float|int $y The ordinate of upper-left corner. 434 * @param float|int|null $width The width. 435 * @param float|int|null $height The height. 436 * @param bool $adjustPageSize 437 * @return array The size. 438 * @see Fpdi::getTemplateSize() 439 */ 440 public function useImportedPage($pageId, $x = 0, $y = 0, $width = null, $height = null, $adjustPageSize = false) 441 { 442 if (\is_array($x)) { 443 /** @noinspection OffsetOperationsInspection */ 444 unset($x['pageId']); 445 \extract($x, EXTR_IF_EXISTS); 446 /** @noinspection NotOptimalIfConditionsInspection */ 447 /** @phpstan-ignore function.alreadyNarrowedType */ 448 if (\is_array($x)) { 449 $x = 0; 450 } 451 } 452 453 if (!isset($this->importedPages[$pageId])) { 454 throw new \InvalidArgumentException('Imported page does not exist!'); 455 } 456 457 $importedPage = $this->importedPages[$pageId]; 458 459 $originalSize = $this->getTemplateSize($pageId); 460 $newSize = $this->getTemplateSize($pageId, $width, $height); 461 if ($adjustPageSize) { 462 $this->setPageFormat($newSize, $newSize['orientation']); 463 } 464 465 $scaleX = ($newSize['width'] / $originalSize['width']); 466 $scaleY = ($newSize['height'] / $originalSize['height']); 467 $xPt = $x * $this->k; 468 $yPt = $y * $this->k; 469 $newHeightPt = $newSize['height'] * $this->k; 470 471 $this->_out( 472 // reset standard values, translate and scale 473 \sprintf( 474 'q 0 J 1 w 0 j 0 G 0 g %.4F 0 0 %.4F %.4F %.4F cm /%s Do Q', 475 $scaleX, 476 $scaleY, 477 $xPt, 478 $this->hPt - $yPt - $newHeightPt, 479 $importedPage['id'] 480 ) 481 ); 482 483 if (count($importedPage['externalLinks']) > 0) { 484 foreach ($importedPage['externalLinks'] as $externalLink) { 485 // mPDF uses also 'externalLinks' but doesn't come with a rect-value 486 if (!isset($externalLink['rect'])) { 487 continue; 488 } 489 490 /** @var Rectangle $rect */ 491 $rect = $externalLink['rect']; 492 $this->Link( 493 $x + $rect->getLlx() / $this->k * $scaleX, 494 $y + $newSize['height'] - ($rect->getLly() + $rect->getHeight()) / $this->k * $scaleY, 495 $rect->getWidth() / $this->k * $scaleX, 496 $rect->getHeight() / $this->k * $scaleY, 497 $externalLink['uri'] 498 ); 499 500 $this->adjustLastLink($externalLink, $xPt, $scaleX, $yPt, $newHeightPt, $scaleY, $importedPage); 501 } 502 } 503 504 return $newSize; 505 } 506 507 /** 508 * This method will add additional data to the last created link/annotation. 509 * 510 * It is separated because TCPDF uses its own logic to handle link annotations. 511 * This method is overwritten in the TCPDF implementation. 512 * 513 * @param array $externalLink 514 * @param float|int $xPt 515 * @param float|int $scaleX 516 * @param float|int $yPt 517 * @param float|int $newHeightPt 518 * @param float|int $scaleY 519 * @param array $importedPage 520 * @return void 521 */ 522 protected function adjustLastLink($externalLink, $xPt, $scaleX, $yPt, $newHeightPt, $scaleY, $importedPage) 523 { 524 // let's create a relation of the newly created link to the data of the external link 525 $lastLink = count($this->PageLinks[$this->page]); 526 $this->PageLinks[$this->page][$lastLink - 1]['importedLink'] = $externalLink; 527 if (count($externalLink['quadPoints']) > 0) { 528 $quadPoints = []; 529 for ($i = 0, $n = count($externalLink['quadPoints']); $i < $n; $i += 2) { 530 $quadPoints[] = $xPt + $externalLink['quadPoints'][$i] * $scaleX; 531 $quadPoints[] = $this->hPt - $yPt - $newHeightPt + $externalLink['quadPoints'][$i + 1] * $scaleY; 532 } 533 534 $this->PageLinks[$this->page][$lastLink - 1]['quadPoints'] = $quadPoints; 535 } 536 } 537 538 /** 539 * Get the size of an imported page. 540 * 541 * Give only one of the size parameters (width, height) to calculate the other one automatically in view to the 542 * aspect ratio. 543 * 544 * @param mixed $tpl The template id 545 * @param float|int|null $width The width. 546 * @param float|int|null $height The height. 547 * @return array|bool An array with following keys: width, height, 0 (=width), 1 (=height), orientation (L or P) 548 */ 549 public function getImportedPageSize($tpl, $width = null, $height = null) 550 { 551 if (isset($this->importedPages[$tpl])) { 552 $importedPage = $this->importedPages[$tpl]; 553 554 if ($width === null && $height === null) { 555 $width = $importedPage['width']; 556 $height = $importedPage['height']; 557 } elseif ($width === null) { 558 $width = $height * $importedPage['width'] / $importedPage['height']; 559 } 560 561 if ($height === null) { 562 $height = $width * $importedPage['height'] / $importedPage['width']; 563 } 564 565 if ($height <= 0. || $width <= 0.) { 566 throw new \InvalidArgumentException('Width or height parameter needs to be larger than zero.'); 567 } 568 569 return [ 570 'width' => $width, 571 'height' => $height, 572 0 => $width, 573 1 => $height, 574 'orientation' => $width > $height ? 'L' : 'P' 575 ]; 576 } 577 578 return false; 579 } 580 581 /** 582 * Writes a PdfType object to the resulting buffer. 583 * 584 * @param PdfType $value 585 * @throws PdfTypeException 586 */ 587 protected function writePdfType(PdfType $value) 588 { 589 if ($value instanceof PdfNumeric) { 590 if (\is_int($value->value)) { 591 $this->_put($value->value . ' ', false); 592 } else { 593 $this->_put(\rtrim(\rtrim(\sprintf('%.5F', $value->value), '0'), '.') . ' ', false); 594 } 595 } elseif ($value instanceof PdfName) { 596 $this->_put('/' . $value->value . ' ', false); 597 } elseif ($value instanceof PdfString) { 598 $this->_put('(' . $value->value . ')', false); 599 } elseif ($value instanceof PdfHexString) { 600 $this->_put('<' . $value->value . '>', false); 601 } elseif ($value instanceof PdfBoolean) { 602 $this->_put($value->value ? 'true ' : 'false ', false); 603 } elseif ($value instanceof PdfArray) { 604 $this->_put('[', false); 605 foreach ($value->value as $entry) { 606 $this->writePdfType($entry); 607 } 608 $this->_put(']'); 609 } elseif ($value instanceof PdfDictionary) { 610 $this->_put('<<', false); 611 foreach ($value->value as $name => $entry) { 612 $this->_put('/' . $name . ' ', false); 613 $this->writePdfType($entry); 614 } 615 $this->_put('>>'); 616 } elseif ($value instanceof PdfToken) { 617 $this->_put($value->value); 618 } elseif ($value instanceof PdfNull) { 619 $this->_put('null ', false); 620 } elseif ($value instanceof PdfStream) { 621 $this->writePdfType($value->value); 622 $this->_put('stream'); 623 $this->_put($value->getStream()); 624 $this->_put('endstream'); 625 } elseif ($value instanceof PdfIndirectObjectReference) { 626 if (!isset($this->objectMap[$this->currentReaderId])) { 627 $this->objectMap[$this->currentReaderId] = []; 628 } 629 630 if (!isset($this->objectMap[$this->currentReaderId][$value->value])) { 631 $this->objectMap[$this->currentReaderId][$value->value] = ++$this->n; 632 $this->objectsToCopy[$this->currentReaderId][] = $value->value; 633 } 634 635 $this->_put($this->objectMap[$this->currentReaderId][$value->value] . ' 0 R ', false); 636 } elseif ($value instanceof PdfIndirectObject) { 637 $n = $this->objectMap[$this->currentReaderId][$value->objectNumber]; 638 $this->_newobj($n); 639 $this->writePdfType($value->value); 640 641 // add newline before "endobj" for all objects in view to PDF/A conformance 642 if ( 643 !( 644 ($value->value instanceof PdfArray) || 645 ($value->value instanceof PdfDictionary) || 646 ($value->value instanceof PdfToken) || 647 ($value->value instanceof PdfStream) 648 ) 649 ) { 650 $this->_put("\n", false); 651 } 652 653 $this->_put('endobj'); 654 } 655 } 656} 657