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
12/**
13 * Trait FpdfTplTrait
14 *
15 * This class adds a templating feature to tFPDF.
16 *
17 * @package setasign\Fpdi
18 */
19trait FpdfTplTrait
20{
21    /**
22     * Data of all created templates.
23     *
24     * @var array
25     */
26    protected $templates = [];
27
28    /**
29     * The template id for the currently created template.
30     *
31     * @var null|int
32     */
33    protected $currentTemplateId;
34
35    /**
36     * A counter for template ids.
37     *
38     * @var int
39     */
40    protected $templateId = 0;
41
42    /**
43     * Set the page format of the current page.
44     *
45     * @param array $size An array with two values defining the size.
46     * @param string $orientation "L" for landscape, "P" for portrait.
47     * @throws \BadMethodCallException
48     */
49    public function setPageFormat($size, $orientation)
50    {
51        if ($this->currentTemplateId !== null) {
52            throw new \BadMethodCallException('The page format cannot be changed when writing to a template.');
53        }
54
55        if (!\in_array($orientation, ['P', 'L'], true)) {
56            throw new \InvalidArgumentException(\sprintf(
57                'Invalid page orientation "%s"! Only "P" and "L" are allowed!',
58                $orientation
59            ));
60        }
61
62        $size = $this->_getpagesize($size);
63
64        if ($orientation != $this->CurOrientation
65            || $size[0] != $this->CurPageSize[0]
66            || $size[1] != $this->CurPageSize[1]
67        ) {
68            // New size or orientation
69            if ($orientation === 'P') {
70                $this->w = $size[0];
71                $this->h = $size[1];
72            } else {
73                $this->w = $size[1];
74                $this->h = $size[0];
75            }
76            $this->wPt = $this->w * $this->k;
77            $this->hPt = $this->h * $this->k;
78            $this->PageBreakTrigger = $this->h - $this->bMargin;
79            $this->CurOrientation = $orientation;
80            $this->CurPageSize = $size;
81
82            $this->PageInfo[$this->page]['size'] = array($this->wPt, $this->hPt);
83        }
84    }
85
86    /**
87     * Draws a template onto the page or another template.
88     *
89     * Give only one of the size parameters (width, height) to calculate the other one automatically in view to the
90     * aspect ratio.
91     *
92     * @param mixed $tpl The template id
93     * @param array|float|int $x The abscissa of upper-left corner. Alternatively you could use an assoc array
94     *                           with the keys "x", "y", "width", "height", "adjustPageSize".
95     * @param float|int $y The ordinate of upper-left corner.
96     * @param float|int|null $width The width.
97     * @param float|int|null $height The height.
98     * @param bool $adjustPageSize
99     * @return array The size
100     * @see FpdfTplTrait::getTemplateSize()
101     */
102    public function useTemplate($tpl, $x = 0, $y = 0, $width = null, $height = null, $adjustPageSize = false)
103    {
104        if (!isset($this->templates[$tpl])) {
105            throw new \InvalidArgumentException('Template does not exist!');
106        }
107
108        if (\is_array($x)) {
109            unset($x['tpl']);
110            \extract($x, EXTR_IF_EXISTS);
111            /** @noinspection NotOptimalIfConditionsInspection */
112            /** @noinspection CallableParameterUseCaseInTypeContextInspection */
113            if (\is_array($x)) {
114                $x = 0;
115            }
116        }
117
118        $template = $this->templates[$tpl];
119
120        $originalSize = $this->getTemplateSize($tpl);
121        $newSize = $this->getTemplateSize($tpl, $width, $height);
122        if ($adjustPageSize) {
123            $this->setPageFormat($newSize, $newSize['orientation']);
124        }
125
126        $this->_out(
127        // reset standard values, translate and scale
128            \sprintf(
129                'q 0 J 1 w 0 j 0 G 0 g %.4F 0 0 %.4F %.4F %.4F cm /%s Do Q',
130                ($newSize['width'] / $originalSize['width']),
131                ($newSize['height'] / $originalSize['height']),
132                $x * $this->k,
133                ($this->h - $y - $newSize['height']) * $this->k,
134                $template['id']
135            )
136        );
137
138        return $newSize;
139    }
140
141    /**
142     * Get the size of a template.
143     *
144     * Give only one of the size parameters (width, height) to calculate the other one automatically in view to the
145     * aspect ratio.
146     *
147     * @param mixed $tpl The template id
148     * @param float|int|null $width The width.
149     * @param float|int|null $height The height.
150     * @return array|bool An array with following keys: width, height, 0 (=width), 1 (=height), orientation (L or P)
151     */
152    public function getTemplateSize($tpl, $width = null, $height = null)
153    {
154        if (!isset($this->templates[$tpl])) {
155            return false;
156        }
157
158        if ($width === null && $height === null) {
159            $width = $this->templates[$tpl]['width'];
160            $height = $this->templates[$tpl]['height'];
161        } elseif ($width === null) {
162            $width = $height * $this->templates[$tpl]['width'] / $this->templates[$tpl]['height'];
163        }
164
165        if ($height === null) {
166            $height = $width * $this->templates[$tpl]['height'] / $this->templates[$tpl]['width'];
167        }
168
169        if ($height <= 0. || $width <= 0.) {
170            throw new \InvalidArgumentException('Width or height parameter needs to be larger than zero.');
171        }
172
173        return [
174            'width' => $width,
175            'height' => $height,
176            0 => $width,
177            1 => $height,
178            'orientation' => $width > $height ? 'L' : 'P'
179        ];
180    }
181
182    /**
183     * Begins a new template.
184     *
185     * @param float|int|null $width The width of the template. If null, the current page width is used.
186     * @param float|int|null $height The height of the template. If null, the current page height is used.
187     * @param bool $groupXObject Define the form XObject as a group XObject to support transparency (if used).
188     * @return int A template identifier.
189     */
190    public function beginTemplate($width = null, $height = null, $groupXObject = false)
191    {
192        if ($width === null) {
193            $width = $this->w;
194        }
195
196        if ($height === null) {
197            $height = $this->h;
198        }
199
200        $templateId = $this->getNextTemplateId();
201
202        // initiate buffer with current state of FPDF
203        $buffer = "2 J\n"
204            . \sprintf('%.2F w', $this->LineWidth * $this->k) . "\n";
205
206        if ($this->FontFamily) {
207            $buffer .= \sprintf("BT /F%d %.2F Tf ET\n", $this->CurrentFont['i'], $this->FontSizePt);
208        }
209
210        if ($this->DrawColor !== '0 G') {
211            $buffer .= $this->DrawColor . "\n";
212        }
213        if ($this->FillColor !== '0 g') {
214            $buffer .= $this->FillColor . "\n";
215        }
216
217        if ($groupXObject && \version_compare('1.4', $this->PDFVersion, '>')) {
218            $this->PDFVersion = '1.4';
219        }
220
221        $this->templates[$templateId] = [
222            'objectNumber' => null,
223            'id' => 'TPL' . $templateId,
224            'buffer' => $buffer,
225            'width' => $width,
226            'height' => $height,
227            'groupXObject' => $groupXObject,
228            'state' => [
229                'x' => $this->x,
230                'y' => $this->y,
231                'AutoPageBreak' => $this->AutoPageBreak,
232                'bMargin' => $this->bMargin,
233                'tMargin' => $this->tMargin,
234                'lMargin' => $this->lMargin,
235                'rMargin' => $this->rMargin,
236                'h' => $this->h,
237                'w' => $this->w,
238                'FontFamily' => $this->FontFamily,
239                'FontStyle' => $this->FontStyle,
240                'FontSizePt' => $this->FontSizePt,
241                'FontSize' => $this->FontSize,
242                'underline' => $this->underline,
243                'TextColor' => $this->TextColor,
244                'DrawColor' => $this->DrawColor,
245                'FillColor' => $this->FillColor,
246                'ColorFlag' => $this->ColorFlag
247            ]
248        ];
249
250        $this->SetAutoPageBreak(false);
251        $this->currentTemplateId = $templateId;
252
253        $this->h = $height;
254        $this->w = $width;
255
256        $this->SetXY($this->lMargin, $this->tMargin);
257        $this->SetRightMargin($this->w - $width + $this->rMargin);
258
259        return $templateId;
260    }
261
262    /**
263     * Ends a template.
264     *
265     * @return bool|int|null A template identifier.
266     */
267    public function endTemplate()
268    {
269        if (null === $this->currentTemplateId) {
270            return false;
271        }
272
273        $templateId = $this->currentTemplateId;
274        $template = $this->templates[$templateId];
275
276        $state = $template['state'];
277        $this->SetXY($state['x'], $state['y']);
278        $this->tMargin = $state['tMargin'];
279        $this->lMargin = $state['lMargin'];
280        $this->rMargin = $state['rMargin'];
281        $this->h = $state['h'];
282        $this->w = $state['w'];
283        $this->SetAutoPageBreak($state['AutoPageBreak'], $state['bMargin']);
284
285        $this->FontFamily = $state['FontFamily'];
286        $this->FontStyle = $state['FontStyle'];
287        $this->FontSizePt = $state['FontSizePt'];
288        $this->FontSize = $state['FontSize'];
289
290        $this->TextColor = $state['TextColor'];
291        $this->DrawColor = $state['DrawColor'];
292        $this->FillColor = $state['FillColor'];
293        $this->ColorFlag = $state['ColorFlag'];
294
295        $this->underline = $state['underline'];
296
297        $fontKey = $this->FontFamily . $this->FontStyle;
298        if ($fontKey) {
299            $this->CurrentFont =& $this->fonts[$fontKey];
300        } else {
301            unset($this->CurrentFont);
302        }
303
304        $this->currentTemplateId = null;
305
306        return $templateId;
307    }
308
309    /**
310     * Get the next template id.
311     *
312     * @return int
313     */
314    protected function getNextTemplateId()
315    {
316        return $this->templateId++;
317    }
318
319    /* overwritten FPDF methods: */
320
321    /**
322     * @inheritdoc
323     */
324    public function AddPage($orientation = '', $size = '', $rotation = 0)
325    {
326        if ($this->currentTemplateId !== null) {
327            throw new \BadMethodCallException('Pages cannot be added when writing to a template.');
328        }
329        parent::AddPage($orientation, $size, $rotation);
330    }
331
332    /**
333     * @inheritdoc
334     */
335    public function Link($x, $y, $w, $h, $link)
336    {
337        if ($this->currentTemplateId !== null) {
338            throw new \BadMethodCallException('Links cannot be set when writing to a template.');
339        }
340        parent::Link($x, $y, $w, $h, $link);
341    }
342
343    /**
344     * @inheritdoc
345     */
346    public function SetLink($link, $y = 0, $page = -1)
347    {
348        if ($this->currentTemplateId !== null) {
349            throw new \BadMethodCallException('Links cannot be set when writing to a template.');
350        }
351        return parent::SetLink($link, $y, $page);
352    }
353
354    /**
355     * @inheritdoc
356     */
357    public function SetDrawColor($r, $g = null, $b = null)
358    {
359        parent::SetDrawColor($r, $g, $b);
360        if ($this->page === 0 && $this->currentTemplateId !== null) {
361            $this->_out($this->DrawColor);
362        }
363    }
364
365    /**
366     * @inheritdoc
367     */
368    public function SetFillColor($r, $g = null, $b = null)
369    {
370        parent::SetFillColor($r, $g, $b);
371        if ($this->page === 0 && $this->currentTemplateId !== null) {
372            $this->_out($this->FillColor);
373        }
374    }
375
376    /**
377     * @inheritdoc
378     */
379    public function SetLineWidth($width)
380    {
381        parent::SetLineWidth($width);
382        if ($this->page === 0 && $this->currentTemplateId !== null) {
383            $this->_out(\sprintf('%.2F w', $width * $this->k));
384        }
385    }
386
387    /**
388     * @inheritdoc
389     */
390    public function SetFont($family, $style = '', $size = 0)
391    {
392        parent::SetFont($family, $style, $size);
393        if ($this->page === 0 && $this->currentTemplateId !== null) {
394            $this->_out(\sprintf('BT /F%d %.2F Tf ET', $this->CurrentFont['i'], $this->FontSizePt));
395        }
396    }
397
398    /**
399     * @inheritdoc
400     */
401    public function SetFontSize($size)
402    {
403        parent::SetFontSize($size);
404        if ($this->page === 0 && $this->currentTemplateId !== null) {
405            $this->_out(sprintf('BT /F%d %.2F Tf ET', $this->CurrentFont['i'], $this->FontSizePt));
406        }
407    }
408
409    /**
410     * @inheritdoc
411     */
412    protected function _putimages()
413    {
414        parent::_putimages();
415
416        foreach ($this->templates as $key => $template) {
417            $this->_newobj();
418            $this->templates[$key]['objectNumber'] = $this->n;
419
420            $this->_put('<</Type /XObject /Subtype /Form /FormType 1');
421            $this->_put(\sprintf('/BBox[0 0 %.2F %.2F]', $template['width'] * $this->k, $template['height'] * $this->k));
422            $this->_put('/Resources 2 0 R'); // default resources dictionary of FPDF
423
424            if ($this->compress) {
425                $buffer = \gzcompress($template['buffer']);
426                $this->_put('/Filter/FlateDecode');
427            } else {
428                $buffer = $template['buffer'];
429            }
430
431            $this->_put('/Length ' . \strlen($buffer));
432
433            if ($template['groupXObject']) {
434                $this->_put('/Group <</Type/Group/S/Transparency>>');
435            }
436
437            $this->_put('>>');
438            $this->_putstream($buffer);
439            $this->_put('endobj');
440        }
441    }
442
443    /**
444     * @inheritdoc
445     */
446    protected function _putxobjectdict()
447    {
448        foreach ($this->templates as $key => $template) {
449            $this->_put('/' . $template['id'] . ' ' . $template['objectNumber'] . ' 0 R');
450        }
451
452        parent::_putxobjectdict();
453    }
454
455    /**
456     * @inheritdoc
457     */
458    public function _out($s)
459    {
460        if ($this->currentTemplateId !== null) {
461            $this->templates[$this->currentTemplateId]['buffer'] .= $s . "\n";
462        } else {
463            parent::_out($s);
464        }
465    }
466}