1<?php
2
3namespace Mpdf;
4
5use setasign\Fpdi\PdfParser\CrossReference\CrossReferenceException;
6use setasign\Fpdi\PdfParser\Filter\AsciiHex;
7use setasign\Fpdi\PdfParser\Type\PdfArray;
8use setasign\Fpdi\PdfParser\Type\PdfIndirectObjectReference;
9use setasign\Fpdi\PdfReader\PageBoundaries;
10use setasign\Fpdi\PdfParser\Type\PdfHexString;
11use setasign\Fpdi\PdfParser\Type\PdfIndirectObject;
12use setasign\Fpdi\PdfParser\Type\PdfNull;
13use setasign\Fpdi\PdfParser\Type\PdfNumeric;
14use setasign\Fpdi\PdfParser\Type\PdfStream;
15use setasign\Fpdi\PdfParser\Type\PdfString;
16use setasign\Fpdi\PdfParser\Type\PdfType;
17use setasign\Fpdi\PdfParser\Type\PdfTypeException;
18
19/**
20 * @mixin Mpdf
21 */
22trait FpdiTrait
23{
24	use \setasign\Fpdi\FpdiTrait {
25		writePdfType as fpdiWritePdfType;
26		useImportedPage as fpdiUseImportedPage;
27		importPage as fpdiImportPage;
28	}
29
30	protected $k = Mpdf::SCALE;
31
32	/**
33	 * The currently used object number.
34	 *
35	 * @var int
36	 */
37	public $currentObjectNumber;
38
39	/**
40	 * A counter for template ids.
41	 *
42	 * @var int
43	 */
44	protected $templateId = 0;
45
46	protected function setPageFormat($format, $orientation)
47	{
48		// in mPDF this needs to be "P" (why ever)
49		$orientation = 'P';
50		$this->_setPageSize([$format['width'], $format['height']], $orientation);
51
52		if ($orientation != $this->DefOrientation) {
53			$this->OrientationChanges[$this->page] = true;
54		}
55
56		$this->wPt = $this->fwPt;
57		$this->hPt = $this->fhPt;
58		$this->w = $this->fw;
59		$this->h = $this->fh;
60
61		$this->CurOrientation = $orientation;
62		$this->ResetMargins();
63		$this->pgwidth = $this->w - $this->lMargin - $this->rMargin;
64		$this->PageBreakTrigger = $this->h - $this->bMargin;
65
66		$this->pageDim[$this->page]['w'] = $this->w;
67		$this->pageDim[$this->page]['h'] = $this->h;
68	}
69
70	/**
71	 * Set the minimal PDF version.
72	 *
73	 * @param string $pdfVersion
74	 */
75	protected function setMinPdfVersion($pdfVersion)
76	{
77		if (\version_compare($pdfVersion, $this->pdf_version, '>')) {
78			$this->pdf_version = $pdfVersion;
79		}
80	}
81
82	/**
83	 * Get the next template id.
84	 *
85	 * @return int
86	 */
87	protected function getNextTemplateId()
88	{
89		return $this->templateId++;
90	}
91
92	/**
93	 * Draws an imported page or a template onto the page or another template.
94	 *
95	 * Omit one of the size parameters (width, height) to calculate the other one automatically in view to the aspect
96	 * ratio.
97	 *
98	 * @param mixed $tpl The template id
99	 * @param float|int|array $x The abscissa of upper-left corner. Alternatively you could use an assoc array
100	 *                           with the keys "x", "y", "width", "height", "adjustPageSize".
101	 * @param float|int $y The ordinate of upper-left corner.
102	 * @param float|int|null $width The width.
103	 * @param float|int|null $height The height.
104	 * @param bool $adjustPageSize
105	 * @return array The size
106	 * @see Fpdi::getTemplateSize()
107	 */
108	public function useTemplate($tpl, $x = 0, $y = 0, $width = null, $height = null, $adjustPageSize = false)
109	{
110		return $this->useImportedPage($tpl, $x, $y, $width, $height, $adjustPageSize);
111	}
112
113	/**
114	 * Draws an imported page onto the page.
115	 *
116	 * Omit one of the size parameters (width, height) to calculate the other one automatically in view to the aspect
117	 * ratio.
118	 *
119	 * @param mixed $pageId The page id
120	 * @param float|int|array $x The abscissa of upper-left corner. Alternatively you could use an assoc array
121	 *                           with the keys "x", "y", "width", "height", "adjustPageSize".
122	 * @param float|int $y The ordinate of upper-left corner.
123	 * @param float|int|null $width The width.
124	 * @param float|int|null $height The height.
125	 * @param bool $adjustPageSize
126	 * @return array The size.
127	 * @see Fpdi::getTemplateSize()
128	 */
129	public function useImportedPage($pageId, $x = 0, $y = 0, $width = null, $height = null, $adjustPageSize = false)
130	{
131		if ($this->state == 0) {
132			$this->AddPage();
133		}
134
135		/* Extract $x if an array */
136		if (is_array($x)) {
137			unset($x['pageId']);
138			extract($x, EXTR_IF_EXISTS);
139			if (is_array($x)) {
140				$x = 0;
141			}
142		}
143
144		$newSize = $this->fpdiUseImportedPage($pageId, $x, $y, $width, $height, $adjustPageSize);
145
146		$this->setImportedPageLinks($pageId, $x, $y, $newSize);
147
148		return $newSize;
149	}
150
151	/**
152	 * Imports a page.
153	 *
154	 * @param int $pageNumber The page number.
155	 * @param string $box The page boundary to import. Default set to PageBoundaries::CROP_BOX.
156	 * @param bool $groupXObject Define the form XObject as a group XObject to support transparency (if used).
157	 * @return string A unique string identifying the imported page.
158	 * @throws CrossReferenceException
159	 * @throws FilterException
160	 * @throws PdfParserException
161	 * @throws PdfTypeException
162	 * @throws PdfReaderException
163	 * @see PageBoundaries
164	 */
165	public function importPage($pageNumber, $box = PageBoundaries::CROP_BOX, $groupXObject = true)
166	{
167		$pageId = $this->fpdiImportPage($pageNumber, $box, $groupXObject);
168
169		$this->importedPages[$pageId]['externalLinks'] = $this->getImportedExternalPageLinks($pageNumber);
170
171		return $pageId;
172	}
173
174	/**
175	 * Imports the external page links
176	 *
177	 * @param int $pageNumber The page number.
178	 * @return array
179	 * @throws CrossReferenceException
180	 * @throws PdfTypeException
181	 * @throws \setasign\Fpdi\PdfParser\PdfParserException
182	 */
183	public function getImportedExternalPageLinks($pageNumber)
184	{
185		$links = [];
186
187		$reader = $this->getPdfReader($this->currentReaderId);
188		$parser = $reader->getParser();
189
190		$page = $reader->getPage($pageNumber);
191		$page->getPageDictionary();
192
193		$annotations = $page->getAttribute('Annots');
194		if ($annotations instanceof PdfIndirectObjectReference) {
195			$annotations = PdfType::resolve($parser->getIndirectObject($annotations->value), $parser);
196		}
197
198		if ($annotations instanceof PdfArray) {
199
200			$getAttribute = function ($array, $key) {
201				if (isset($array[$key]->value)) {
202					return $array[$key]->value;
203				}
204
205				return '';
206			};
207
208			foreach ($annotations->value as $annotation) {
209				$annotation = PdfType::resolve($annotation, $parser)->value;
210
211				/* Skip over any annotations that aren't links */
212				$type = $getAttribute($annotation, 'Type');
213				$subtype = $getAttribute($annotation, 'Subtype');
214				if ($type !== 'Annot' || $subtype !== 'Link' || !isset($annotation['A'])) {
215					continue;
216				}
217
218				/* Calculate the link positioning */
219				$position = $getAttribute($annotation, 'Rect');
220
221				if (count($position) !== 4) {
222					continue;
223				}
224
225				$x1 = $getAttribute($position, 0) / Mpdf::SCALE;
226				$y1 = $getAttribute($position, 1) / Mpdf::SCALE;
227				$x2 = $getAttribute($position, 2) / Mpdf::SCALE;
228				$y2 = $getAttribute($position, 3) / Mpdf::SCALE;
229				$width = $x2 - $x1;
230				$height = $y2 - $y1;
231
232				$link = $annotation['A'] instanceof PdfIndirectObjectReference ? PdfType::resolve($annotation['A'], $parser)->value : $getAttribute($annotation, 'A');
233
234				if (isset($link['URI'])) {
235					$links[] = [
236						'x' => $x1,
237						'y' => $y1,
238						'width' => $width,
239						'height' => $height,
240						'url' => $getAttribute($link, 'URI')
241					];
242				}
243			}
244		}
245
246		return $links;
247	}
248
249	/**
250	 * @param mixed $pageId The page id
251	 * @param int|float $x The abscissa of upper-left corner.
252	 * @param int|float $y The ordinate of upper-right corner.
253	 * @param array $newSize The size.
254	 */
255	public function setImportedPageLinks($pageId, $x, $y, $newSize)
256	{
257		$originalSize = $this->getTemplateSize($pageId);
258		$pageHeightDifference = $this->h - $newSize['height'];
259
260		/* Handle different aspect ratio */
261		$widthRatio = $newSize['width'] / $originalSize['width'];
262		$heightRatio = $newSize['height'] / $originalSize['height'];
263
264		foreach ($this->importedPages[$pageId]['externalLinks'] as $item) {
265
266			$item['x'] *= $widthRatio;
267			$item['width'] *= $widthRatio;
268
269			$item['y'] *= $heightRatio;
270			$item['height'] *= $heightRatio;
271
272			$this->Link(
273				$item['x'] + $x,
274				/* convert Y to be measured from the top of the page */
275				$this->h - $item['y'] - $item['height'] - $pageHeightDifference + $y,
276				$item['width'],
277				$item['height'],
278				$item['url']
279			);
280		}
281	}
282
283	/**
284	 * Get the size of an imported page or template.
285	 *
286	 * Omit one of the size parameters (width, height) to calculate the other one automatically in view to the aspect
287	 * ratio.
288	 *
289	 * @param mixed $tpl The template id
290	 * @param float|int|null $width The width.
291	 * @param float|int|null $height The height.
292	 * @return array|bool An array with following keys: width, height, 0 (=width), 1 (=height), orientation (L or P)
293	 */
294	public function getTemplateSize($tpl, $width = null, $height = null)
295	{
296		return $this->getImportedPageSize($tpl, $width, $height);
297	}
298
299	/**
300	 * @throws CrossReferenceException
301	 * @throws PdfTypeException
302	 * @throws \setasign\Fpdi\PdfParser\PdfParserException
303	 */
304	public function writeImportedPagesAndResolvedObjects()
305	{
306		$this->currentReaderId = null;
307
308		foreach ($this->importedPages as $key => $pageData) {
309			$this->writer->object();
310			$this->importedPages[$key]['objectNumber'] = $this->n;
311			$this->currentReaderId = $pageData['readerId'];
312			$this->writePdfType($pageData['stream']);
313			$this->_put('endobj');
314		}
315
316		foreach (\array_keys($this->readers) as $readerId) {
317			$parser = $this->getPdfReader($readerId)->getParser();
318			$this->currentReaderId = $readerId;
319
320			while (($objectNumber = \array_pop($this->objectsToCopy[$readerId])) !== null) {
321				try {
322					$object = $parser->getIndirectObject($objectNumber);
323
324				} catch (CrossReferenceException $e) {
325					if ($e->getCode() === CrossReferenceException::OBJECT_NOT_FOUND) {
326						$object = PdfIndirectObject::create($objectNumber, 0, new PdfNull());
327					} else {
328						throw $e;
329					}
330				}
331
332				$this->writePdfType($object);
333			}
334		}
335
336		$this->currentReaderId = null;
337	}
338
339	public function getImportedPages()
340	{
341		return $this->importedPages;
342	}
343
344	protected function _put($s, $newLine = true)
345	{
346		if ($newLine) {
347			$this->buffer .= $s . "\n";
348		} else {
349			$this->buffer .= $s;
350		}
351	}
352
353	/**
354	 * Writes a PdfType object to the resulting buffer.
355	 *
356	 * @param PdfType $value
357	 * @throws PdfTypeException
358	 */
359	public function writePdfType(PdfType $value)
360	{
361		if (!$this->encrypted) {
362			if ($value instanceof PdfIndirectObject) {
363				/**
364				 * @var $value PdfIndirectObject
365				 */
366				$n = $this->objectMap[$this->currentReaderId][$value->objectNumber];
367				$this->writer->object($n);
368				$this->writePdfType($value->value);
369				$this->_put('endobj');
370				return;
371			}
372
373			$this->fpdiWritePdfType($value);
374			return;
375		}
376
377		if ($value instanceof PdfString) {
378			$string = PdfString::unescape($value->value);
379			$string = $this->protection->rc4($this->protection->objectKey($this->currentObjectNumber), $string);
380			$value->value = $this->writer->escape($string);
381
382		} elseif ($value instanceof PdfHexString) {
383			$filter = new AsciiHex();
384			$string = $filter->decode($value->value);
385			$string = $this->protection->rc4($this->protection->objectKey($this->currentObjectNumber), $string);
386			$value->value = $filter->encode($string, true);
387
388		} elseif ($value instanceof PdfStream) {
389			$stream = $value->getStream();
390			$stream = $this->protection->rc4($this->protection->objectKey($this->currentObjectNumber), $stream);
391			$dictionary = $value->value;
392			$dictionary->value['Length'] = PdfNumeric::create(\strlen($stream));
393			$value = PdfStream::create($dictionary, $stream);
394
395		} elseif ($value instanceof PdfIndirectObject) {
396			/**
397			 * @var $value PdfIndirectObject
398			 */
399			$this->currentObjectNumber = $this->objectMap[$this->currentReaderId][$value->objectNumber];
400			/**
401			 * @var $value PdfIndirectObject
402			 */
403			$n = $this->objectMap[$this->currentReaderId][$value->objectNumber];
404			$this->writer->object($n);
405			$this->writePdfType($value->value);
406			$this->_put('endobj');
407			return;
408		}
409
410		$this->fpdiWritePdfType($value);
411	}
412}
413