FpdiTrait.php 19 KB

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