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