1<?php 2/** 3 * This file is part of FPDI 4 * 5 * @package setasign\Fpdi 6 * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) 7 * @license http://opensource.org/licenses/mit-license The MIT License 8 */ 9 10namespace setasign\Fpdi; 11 12use setasign\Fpdi\PdfParser\CrossReference\CrossReferenceException; 13use setasign\Fpdi\PdfParser\Filter\FilterException; 14use setasign\Fpdi\PdfParser\PdfParser; 15use setasign\Fpdi\PdfParser\PdfParserException; 16use setasign\Fpdi\PdfParser\StreamReader; 17use setasign\Fpdi\PdfParser\Type\PdfArray; 18use setasign\Fpdi\PdfParser\Type\PdfBoolean; 19use setasign\Fpdi\PdfParser\Type\PdfDictionary; 20use setasign\Fpdi\PdfParser\Type\PdfHexString; 21use setasign\Fpdi\PdfParser\Type\PdfIndirectObject; 22use setasign\Fpdi\PdfParser\Type\PdfIndirectObjectReference; 23use setasign\Fpdi\PdfParser\Type\PdfName; 24use setasign\Fpdi\PdfParser\Type\PdfNull; 25use setasign\Fpdi\PdfParser\Type\PdfNumeric; 26use setasign\Fpdi\PdfParser\Type\PdfStream; 27use setasign\Fpdi\PdfParser\Type\PdfString; 28use setasign\Fpdi\PdfParser\Type\PdfToken; 29use setasign\Fpdi\PdfParser\Type\PdfType; 30use setasign\Fpdi\PdfParser\Type\PdfTypeException; 31use setasign\Fpdi\PdfReader\PageBoundaries; 32use setasign\Fpdi\PdfReader\PdfReader; 33use setasign\Fpdi\PdfReader\PdfReaderException; 34use /* 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 */ 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 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} 563