See Release Notes
Long Term Support Release
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body