xref: /webtrees/app/Report/PdfRenderer.php (revision ceab9748a7c11244030d33fbda589872b29688e2)
1<?php
2
3/**
4 * webtrees: online genealogy
5 * Copyright (C) 2021 webtrees development team
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 */
17
18declare(strict_types=1);
19
20namespace Fisharebest\Webtrees\Report;
21
22use Fisharebest\Webtrees\MediaFile;
23use Fisharebest\Webtrees\Webtrees;
24use League\Flysystem\FilesystemOperator;
25
26use function count;
27
28/**
29 * Class PdfRenderer
30 */
31class PdfRenderer extends AbstractRenderer
32{
33    /**
34     * PDF compression - Zlib extension is required
35     *
36     * @var bool const
37     */
38    private const COMPRESSION = true;
39
40    /**
41     * If true reduce the RAM memory usage by caching temporary data on filesystem (slower).
42     *
43     * @var bool const
44     */
45    private const DISK_CACHE = false;
46
47    /**
48     * true means that the input text is unicode (PDF)
49     *
50     * @var bool const
51     */
52    private const UNICODE = true;
53
54    // Font sub-setting in TCPDF is slow.
55    private const SUBSETTING = false;
56
57    public TcpdfWrapper $tcpdf;
58
59    /** @var array<ReportPdfFootnote> Array of elements in the footer notes */
60    public array $printedfootnotes = [];
61
62    /** @var float The last cell height */
63    public float $lastCellHeight = 0.0;
64
65    /** @var float The largest font size within a TextBox to calculate the height */
66    public float $largestFontHeight = 0.0;
67
68    /** @var int The last pictures page number */
69    public int $lastpicpage = 0;
70
71    /** @var PdfRenderer The current report. */
72    public $wt_report;
73
74    /**
75     * PDF Header -PDF
76     *
77     * @return void
78     */
79    public function header(): void
80    {
81        foreach ($this->headerElements as $element) {
82            if ($element instanceof ReportBaseElement) {
83                $element->render($this);
84            } elseif ($element === 'footnotetexts') {
85                $this->footnotes();
86            } elseif ($element === 'addpage') {
87                $this->newPage();
88            }
89        }
90    }
91
92    /**
93     * PDF Body -PDF
94     *
95     * @return void
96     */
97    public function body(): void
98    {
99        $this->tcpdf->AddPage();
100
101        foreach ($this->bodyElements as $key => $element) {
102            if ($element instanceof ReportBaseElement) {
103                $element->render($this);
104            } elseif ($element === 'footnotetexts') {
105                $this->footnotes();
106            } elseif ($element === 'addpage') {
107                $this->newPage();
108            }
109        }
110    }
111
112    /**
113     * PDF Footnotes -PDF
114     *
115     * @return void
116     */
117    public function footnotes(): void
118    {
119        foreach ($this->printedfootnotes as $element) {
120            if ($this->tcpdf->GetY() + $element->getFootnoteHeight($this) > $this->tcpdf->getPageHeight()) {
121                $this->tcpdf->AddPage();
122            }
123
124            $element->renderFootnote($this);
125
126            if ($this->tcpdf->GetY() > $this->tcpdf->getPageHeight()) {
127                $this->tcpdf->AddPage();
128            }
129        }
130    }
131
132    /**
133     * PDF Footer -PDF
134     *
135     * @return void
136     */
137    public function footer(): void
138    {
139        foreach ($this->footerElements as $element) {
140            if ($element instanceof ReportBaseElement) {
141                $element->render($this);
142            } elseif ($element === 'footnotetexts') {
143                $this->footnotes();
144            } elseif ($element === 'addpage') {
145                $this->newPage();
146            }
147        }
148    }
149
150    /**
151     * Remove the header.
152     *
153     * @param int $index
154     *
155     * @return void
156     */
157    public function removeHeader(int $index): void
158    {
159        unset($this->headerElements[$index]);
160    }
161
162    /**
163     * Remove the body.
164     *
165     * @param int $index
166     *
167     * @return void
168     */
169    public function removeBody(int $index): void
170    {
171        unset($this->bodyElements[$index]);
172    }
173
174    /**
175     * Remove the footer.
176     *
177     * @param int $index
178     *
179     * @return void
180     */
181    public function removeFooter(int $index): void
182    {
183        unset($this->footerElements[$index]);
184    }
185
186    /**
187     * Clear the Header -PDF
188     *
189     * @return void
190     */
191    public function clearHeader(): void
192    {
193        unset($this->headerElements);
194        $this->headerElements = [];
195    }
196
197    /**
198     * Get the currently used style name -PDF
199     *
200     * @return string
201     */
202    public function getCurrentStyle(): string
203    {
204        return $this->currentStyle;
205    }
206
207    /**
208     * Setup a style for usage -PDF
209     *
210     * @param string $s Style name
211     *
212     * @return void
213     */
214    public function setCurrentStyle(string $s): void
215    {
216        $this->currentStyle = $s;
217        $style              = $this->getStyle($s);
218        $this->tcpdf->SetFont($style['font'], $style['style'], $style['size']);
219    }
220
221    /**
222     * Get the style -PDF
223     *
224     * @param string $s Style name
225     *
226     * @return array
227     */
228    public function getStyle(string $s): array
229    {
230        if (!isset($this->styles[$s])) {
231            $s                = $this->getCurrentStyle();
232            $this->styles[$s] = $s;
233        }
234
235        return $this->styles[$s];
236    }
237
238    /**
239     * Add margin when static horizontal position is used -PDF
240     * RTL supported
241     *
242     * @param float $x Static position
243     *
244     * @return float
245     */
246    public function addMarginX(float $x): float
247    {
248        $m = $this->tcpdf->getMargins();
249        if ($this->tcpdf->getRTL()) {
250            $x += $m['right'];
251        } else {
252            $x += $m['left'];
253        }
254        $this->tcpdf->SetX($x);
255
256        return $x;
257    }
258
259    /**
260     * Get the maximum line width to draw from the curren position -PDF
261     * RTL supported
262     *
263     * @return float
264     */
265    public function getMaxLineWidth(): float
266    {
267        $m = $this->tcpdf->getMargins();
268        if ($this->tcpdf->getRTL()) {
269            return $this->tcpdf->getRemainingWidth() + $m['right'];
270        }
271
272        return $this->tcpdf->getRemainingWidth() + $m['left'];
273    }
274
275    /**
276     * Get the height of the footnote.
277     *
278     * @return float
279     */
280    public function getFootnotesHeight(): float
281    {
282        $h = 0;
283        foreach ($this->printedfootnotes as $element) {
284            $h += $element->getHeight($this);
285        }
286
287        return $h;
288    }
289
290    /**
291     * Returns the the current font size height -PDF
292     *
293     * @return float
294     */
295    public function getCurrentStyleHeight(): float
296    {
297        if ($this->currentStyle === '') {
298            return $this->default_font_size;
299        }
300        $style = $this->getStyle($this->currentStyle);
301
302        return (float) $style['size'];
303    }
304
305    /**
306     * Checks the Footnote and numbers them
307     *
308     * @param ReportPdfFootnote $footnote
309     *
310     * @return ReportPdfFootnote|bool object if already numbered, false otherwise
311     */
312    public function checkFootnote(ReportPdfFootnote $footnote)
313    {
314        $ct  = count($this->printedfootnotes);
315        $val = $footnote->getValue();
316        $i   = 0;
317        while ($i < $ct) {
318            if ($this->printedfootnotes[$i]->getValue() == $val) {
319                // If this footnote already exist then set up the numbers for this object
320                $footnote->setNum($i + 1);
321                $footnote->setAddlink((string) ($i + 1));
322
323                return $this->printedfootnotes[$i];
324            }
325            $i++;
326        }
327        // If this Footnote has not been set up yet
328        $footnote->setNum($ct + 1);
329        $footnote->setAddlink((string) $this->tcpdf->AddLink());
330        $this->printedfootnotes[] = $footnote;
331
332        return false;
333    }
334
335    /**
336     * Used this function instead of AddPage()
337     * This function will make sure that images will not be overwritten
338     *
339     * @return void
340     */
341    public function newPage(): void
342    {
343        if ($this->lastpicpage > $this->tcpdf->getPage()) {
344            $this->tcpdf->setPage($this->lastpicpage);
345        }
346        $this->tcpdf->AddPage();
347    }
348
349    /**
350     * Add a page if needed -PDF
351     *
352     * @param float $height Cell height
353     *
354     * @return bool true in case of page break, false otherwise
355     */
356    public function checkPageBreakPDF(float $height): bool
357    {
358        return $this->tcpdf->checkPageBreak($height);
359    }
360
361    /**
362     * Returns the remaining width between the current position and margins -PDF
363     *
364     * @return float Remaining width
365     */
366    public function getRemainingWidthPDF(): float
367    {
368        return $this->tcpdf->getRemainingWidth();
369    }
370    /**
371     * PDF Setup - ReportPdf
372     *
373     * @return void
374     */
375    public function setup(): void
376    {
377        parent::setup();
378
379        $this->tcpdf = new TcpdfWrapper(
380            $this->orientation,
381            self::UNITS,
382            [$this->page_width, $this->page_height],
383            self::UNICODE,
384            'UTF-8',
385            self::DISK_CACHE
386        );
387
388        $this->tcpdf->SetMargins($this->left_margin, $this->top_margin, $this->right_margin);
389        $this->tcpdf->setHeaderMargin($this->header_margin);
390        $this->tcpdf->setFooterMargin($this->footer_margin);
391        $this->tcpdf->SetAutoPageBreak(true, $this->bottom_margin);
392        $this->tcpdf->setFontSubsetting(self::SUBSETTING);
393        $this->tcpdf->SetCompression(self::COMPRESSION);
394        $this->tcpdf->setRTL($this->rtl);
395        $this->tcpdf->SetCreator(Webtrees::NAME . ' ' . Webtrees::VERSION);
396        $this->tcpdf->SetAuthor($this->rauthor);
397        $this->tcpdf->SetTitle($this->title);
398        $this->tcpdf->SetSubject($this->rsubject);
399        $this->tcpdf->SetKeywords($this->rkeywords);
400        $this->tcpdf->SetHeaderData('', 0, $this->title);
401        $this->tcpdf->setHeaderFont([$this->default_font, '', $this->default_font_size]);
402
403        if ($this->show_generated_by) {
404            // The default style name for Generated by.... is 'genby'
405            $element = new ReportPdfCell(0, 10, 0, 'C', '', 'genby', 1, ReportBaseElement::CURRENT_POSITION, ReportBaseElement::CURRENT_POSITION, 0, 0, '', '', true);
406            $element->addText($this->generated_by);
407            $element->setUrl(Webtrees::URL);
408            $this->addElementToFooter($element);
409        }
410    }
411
412    /**
413     * Run the report.
414     *
415     * @return void
416     */
417    public function run(): void
418    {
419        $this->body();
420        echo $this->tcpdf->Output('doc.pdf', 'S');
421    }
422
423    /**
424     * Create a new Cell object.
425     *
426     * @param int    $width   cell width (expressed in points)
427     * @param int    $height  cell height (expressed in points)
428     * @param mixed  $border  Border style
429     * @param string $align   Text alignement
430     * @param string $bgcolor Background color code
431     * @param string $style   The name of the text style
432     * @param int    $ln      Indicates where the current position should go after the call
433     * @param mixed  $top     Y-position
434     * @param mixed  $left    X-position
435     * @param int    $fill    Indicates if the cell background must be painted (1) or transparent (0). Default value: 1
436     * @param int    $stretch Stretch carachter mode
437     * @param string $bocolor Border color
438     * @param string $tcolor  Text color
439     * @param bool   $reseth
440     *
441     * @return ReportBaseCell
442     */
443    public function createCell(int $width, int $height, $border, string $align, string $bgcolor, string $style, int $ln, $top, $left, int $fill, int $stretch, string $bocolor, string $tcolor, bool $reseth): ReportBaseCell
444    {
445        return new ReportPdfCell($width, $height, $border, $align, $bgcolor, $style, $ln, $top, $left, $fill, $stretch, $bocolor, $tcolor, $reseth);
446    }
447
448    /**
449     * Create a new TextBox object.
450     *
451     * @param float  $width   Text box width
452     * @param float  $height  Text box height
453     * @param bool   $border
454     * @param string $bgcolor Background color code in HTML
455     * @param bool   $newline
456     * @param float  $left
457     * @param float  $top
458     * @param bool   $pagecheck
459     * @param string $style
460     * @param bool   $fill
461     * @param bool   $padding
462     * @param bool   $reseth
463     *
464     * @return ReportBaseTextbox
465     */
466    public function createTextBox(
467        float $width,
468        float $height,
469        bool $border,
470        string $bgcolor,
471        bool $newline,
472        float $left,
473        float $top,
474        bool $pagecheck,
475        string $style,
476        bool $fill,
477        bool $padding,
478        bool $reseth
479    ): ReportBaseTextbox {
480        return new ReportPdfTextBox($width, $height, $border, $bgcolor, $newline, $left, $top, $pagecheck, $style, $fill, $padding, $reseth);
481    }
482
483    /**
484     * Create a text element.
485     *
486     * @param string $style
487     * @param string $color
488     *
489     * @return ReportBaseText
490     */
491    public function createText(string $style, string $color): ReportBaseText
492    {
493        return new ReportPdfText($style, $color);
494    }
495
496    /**
497     * Create a new Footnote object.
498     *
499     * @param string $style Style name
500     *
501     * @return ReportBaseFootnote
502     */
503    public function createFootnote(string $style): ReportBaseFootnote
504    {
505        return new ReportPdfFootnote($style);
506    }
507
508    /**
509     * Create a new image object.
510     *
511     * @param string $file  Filename
512     * @param float  $x
513     * @param float  $y
514     * @param float  $w     Image width
515     * @param float  $h     Image height
516     * @param string $align L:left, C:center, R:right or empty to use x/y
517     * @param string $ln    T:same line, N:next line
518     *
519     * @return ReportBaseImage
520     */
521    public function createImage(string $file, float $x, float $y, float $w, float $h, string $align, string $ln): ReportBaseImage
522    {
523        return new ReportPdfImage($file, $x, $y, $w, $h, $align, $ln);
524    }
525
526    /**
527     * Create a new image object from Media Object.
528     *
529     * @param MediaFile          $media_file
530     * @param float              $x
531     * @param float              $y
532     * @param float              $w     Image width
533     * @param float              $h     Image height
534     * @param string             $align L:left, C:center, R:right or empty to use x/y
535     * @param string             $ln    T:same line, N:next line
536     * @param FilesystemOperator $data_filesystem
537     *
538     * @return ReportBaseImage
539     */
540    public function createImageFromObject(
541        MediaFile $media_file,
542        float $x,
543        float $y,
544        float $w,
545        float $h,
546        string $align,
547        string $ln,
548        FilesystemOperator $data_filesystem
549    ): ReportBaseImage {
550        return new ReportPdfImage('@' . $media_file->fileContents($data_filesystem), $x, $y, $w, $h, $align, $ln);
551    }
552
553    /**
554     * Create a line.
555     *
556     * @param float $x1
557     * @param float $y1
558     * @param float $x2
559     * @param float $y2
560     *
561     * @return ReportBaseLine
562     */
563    public function createLine(float $x1, float $y1, float $x2, float $y2): ReportBaseLine
564    {
565        return new ReportPdfLine($x1, $y1, $x2, $y2);
566    }
567}
568