xref: /webtrees/app/Report/PdfRenderer.php (revision bb88f16b1377db732afb12b8b709b3f5f899347a)
1<?php
2
3/**
4 * webtrees: online genealogy
5 * Copyright (C) 2019 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 <http://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\FilesystemInterface;
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    /**
55     * false means that the full font is embedded, true means only the used chars
56     * in TCPDF v5.9 font subsetting is a very slow process, this leads to larger files
57     *
58     * @var bool const
59     */
60    private const SUBSETTING = false;
61
62    /**
63     * @var TcpdfWrapper
64     */
65    public $tcpdf;
66
67    /** @var ReportBaseElement[] Array of elements in the header */
68    public $headerElements = [];
69
70    /** @var ReportBaseElement[] Array of elements in the page header */
71    public $pageHeaderElements = [];
72
73    /** @var ReportBaseElement[] Array of elements in the footer */
74    public $footerElements = [];
75
76    /** @var ReportBaseElement[] Array of elements in the body */
77    public $bodyElements = [];
78
79    /** @var ReportPdfFootnote[] Array of elements in the footer notes */
80    public $printedfootnotes = [];
81
82    /** @var string Currently used style name */
83    public $currentStyle = '';
84
85    /** @var float The last cell height */
86    public $lastCellHeight = 0;
87
88    /** @var float The largest font size within a TextBox to calculate the height */
89    public $largestFontHeight = 0;
90
91    /** @var int The last pictures page number */
92    public $lastpicpage = 0;
93
94    /** @var PdfRenderer The current report. */
95    public $wt_report;
96
97    /**
98     * PDF Header -PDF
99     *
100     * @return void
101     */
102    public function header(): void
103    {
104        foreach ($this->headerElements as $element) {
105            if ($element instanceof ReportBaseElement) {
106                $element->render($this);
107            } elseif ($element === 'footnotetexts') {
108                $this->footnotes();
109            } elseif ($element === 'addpage') {
110                $this->newPage();
111            }
112        }
113
114        foreach ($this->pageHeaderElements as $element) {
115            if ($element instanceof ReportBaseElement) {
116                $element->render($this);
117            } elseif ($element === 'footnotetexts') {
118                $this->footnotes();
119            } elseif ($element === 'addpage') {
120                $this->newPage();
121            }
122        }
123    }
124
125    /**
126     * PDF Body -PDF
127     *
128     * @return void
129     */
130    public function body(): void
131    {
132        $this->tcpdf->AddPage();
133
134        foreach ($this->bodyElements as $key => $element) {
135            if ($element instanceof ReportBaseElement) {
136                $element->render($this);
137            } elseif ($element === 'footnotetexts') {
138                $this->footnotes();
139            } elseif ($element === 'addpage') {
140                $this->newPage();
141            }
142        }
143    }
144
145    /**
146     * PDF Footnotes -PDF
147     *
148     * @return void
149     */
150    public function footnotes(): void
151    {
152        foreach ($this->printedfootnotes as $element) {
153            if (($this->tcpdf->GetY() + $element->getFootnoteHeight($this)) > $this->tcpdf->getPageHeight()) {
154                $this->tcpdf->AddPage();
155            }
156
157            $element->renderFootnote($this);
158
159            if ($this->tcpdf->GetY() > $this->tcpdf->getPageHeight()) {
160                $this->tcpdf->AddPage();
161            }
162        }
163    }
164
165    /**
166     * PDF Footer -PDF
167     *
168     * @return void
169     */
170    public function footer(): void
171    {
172        foreach ($this->footerElements as $element) {
173            if ($element instanceof ReportBaseElement) {
174                $element->render($this);
175            } elseif ($element === 'footnotetexts') {
176                $this->footnotes();
177            } elseif ($element === 'addpage') {
178                $this->newPage();
179            }
180        }
181    }
182
183    /**
184     * Add an element to the Header -PDF
185     *
186     * @param ReportBaseElement|string $element
187     *
188     * @return void
189     */
190    public function addHeader($element): void
191    {
192        $this->headerElements[] = $element;
193    }
194
195    /**
196     * Add an element to the Page Header -PDF
197     *
198     * @param ReportBaseElement|string $element
199     *
200     * @return void
201     */
202    public function addPageHeader($element): void
203    {
204        $this->pageHeaderElements[] = $element;
205    }
206
207    /**
208     * Add an element to the Body -PDF
209     *
210     * @param ReportBaseElement|string $element
211     *
212     * @return void
213     */
214    public function addBody($element): void
215    {
216        $this->bodyElements[] = $element;
217    }
218
219    /**
220     * Add an element to the Footer -PDF
221     *
222     * @param ReportBaseElement|string $element
223     *
224     * @return void
225     */
226    public function addFooter($element): void
227    {
228        $this->footerElements[] = $element;
229    }
230
231    /**
232     * Remove the header.
233     *
234     * @param int $index
235     *
236     * @return void
237     */
238    public function removeHeader(int $index): void
239    {
240        unset($this->headerElements[$index]);
241    }
242
243    /**
244     * Remove the page header.
245     *
246     * @param int $index
247     *
248     * @return void
249     */
250    public function removePageHeader(int $index): void
251    {
252        unset($this->pageHeaderElements[$index]);
253    }
254
255    /**
256     * Remove the body.
257     *
258     * @param int $index
259     *
260     * @return void
261     */
262    public function removeBody(int $index): void
263    {
264        unset($this->bodyElements[$index]);
265    }
266
267    /**
268     * Remove the footer.
269     *
270     * @param int $index
271     *
272     * @return void
273     */
274    public function removeFooter(int $index): void
275    {
276        unset($this->footerElements[$index]);
277    }
278
279    /**
280     * Clear the Header -PDF
281     *
282     * @return void
283     */
284    public function clearHeader(): void
285    {
286        unset($this->headerElements);
287        $this->headerElements = [];
288    }
289
290    /**
291     * Clear the Page Header -PDF
292     *
293     * @return void
294     */
295    public function clearPageHeader(): void
296    {
297        unset($this->pageHeaderElements);
298        $this->pageHeaderElements = [];
299    }
300
301    /**
302     * Set the report.
303     *
304     * @param PdfRenderer $report
305     *
306     * @return void
307     */
308    public function setReport(PdfRenderer $report): void
309    {
310        $this->wt_report = $report;
311    }
312
313    /**
314     * Get the currently used style name -PDF
315     *
316     * @return string
317     */
318    public function getCurrentStyle(): string
319    {
320        return $this->currentStyle;
321    }
322
323    /**
324     * Setup a style for usage -PDF
325     *
326     * @param string $s Style name
327     *
328     * @return void
329     */
330    public function setCurrentStyle(string $s): void
331    {
332        $this->currentStyle = $s;
333        $style              = $this->wt_report->getStyle($s);
334        $this->tcpdf->SetFont($style['font'], $style['style'], $style['size']);
335    }
336
337    /**
338     * Get the style -PDF
339     *
340     * @param string $s Style name
341     *
342     * @return array
343     */
344    public function getStyle(string $s): array
345    {
346        if (!isset($this->wt_report->styles[$s])) {
347            $s                           = $this->getCurrentStyle();
348            $this->wt_report->styles[$s] = $s;
349        }
350
351        return $this->wt_report->styles[$s];
352    }
353
354    /**
355     * Add margin when static horizontal position is used -PDF
356     * RTL supported
357     *
358     * @param float $x Static position
359     *
360     * @return float
361     */
362    public function addMarginX(float $x): float
363    {
364        $m = $this->tcpdf->getMargins();
365        if ($this->tcpdf->getRTL()) {
366            $x += $m['right'];
367        } else {
368            $x += $m['left'];
369        }
370        $this->tcpdf->SetX($x);
371
372        return $x;
373    }
374
375    /**
376     * Get the maximum line width to draw from the curren position -PDF
377     * RTL supported
378     *
379     * @return float
380     */
381    public function getMaxLineWidth(): float
382    {
383        $m = $this->tcpdf->getMargins();
384        if ($this->tcpdf->getRTL()) {
385            return ($this->tcpdf->getRemainingWidth() + $m['right']);
386        }
387
388        return ($this->tcpdf->getRemainingWidth() + $m['left']);
389    }
390
391    /**
392     * Get the height of the footnote.
393     *
394     * @return float
395     */
396    public function getFootnotesHeight(): float
397    {
398        $h = 0;
399        foreach ($this->printedfootnotes as $element) {
400            $h += $element->getHeight($this);
401        }
402
403        return $h;
404    }
405
406    /**
407     * Returns the the current font size height -PDF
408     *
409     * @return float
410     */
411    public function getCurrentStyleHeight(): float
412    {
413        if ($this->currentStyle === '') {
414            return $this->wt_report->default_font_size;
415        }
416        $style = $this->wt_report->getStyle($this->currentStyle);
417
418        return (float) $style['size'];
419    }
420
421    /**
422     * Checks the Footnote and numbers them
423     *
424     * @param ReportPdfFootnote $footnote
425     *
426     * @return ReportPdfFootnote|bool object if already numbered, false otherwise
427     */
428    public function checkFootnote(ReportPdfFootnote $footnote)
429    {
430        $ct  = count($this->printedfootnotes);
431        $val = $footnote->getValue();
432        $i   = 0;
433        while ($i < $ct) {
434            if ($this->printedfootnotes[$i]->getValue() == $val) {
435                // If this footnote already exist then set up the numbers for this object
436                $footnote->setNum($i + 1);
437                $footnote->setAddlink((string) ($i + 1));
438
439                return $this->printedfootnotes[$i];
440            }
441            $i++;
442        }
443        // If this Footnote has not been set up yet
444        $footnote->setNum($ct + 1);
445        $footnote->setAddlink((string) $this->tcpdf->AddLink());
446        $this->printedfootnotes[] = $footnote;
447
448        return false;
449    }
450
451    /**
452     * Used this function instead of AddPage()
453     * This function will make sure that images will not be overwritten
454     *
455     * @return void
456     */
457    public function newPage(): void
458    {
459        if ($this->lastpicpage > $this->tcpdf->getPage()) {
460            $this->tcpdf->setPage($this->lastpicpage);
461        }
462        $this->tcpdf->AddPage();
463    }
464
465    /**
466     * Add a page if needed -PDF
467     *
468     * @param float $height Cell height
469     *
470     * @return bool true in case of page break, false otherwise
471     */
472    public function checkPageBreakPDF(float $height): bool
473    {
474        return $this->tcpdf->checkPageBreak($height);
475    }
476
477    /**
478     * Returns the remaining width between the current position and margins -PDF
479     *
480     * @return float Remaining width
481     */
482    public function getRemainingWidthPDF(): float
483    {
484        return $this->tcpdf->getRemainingWidth();
485    }
486    /**
487     * PDF Setup - ReportPdf
488     *
489     * @return void
490     */
491    public function setup(): void
492    {
493        parent::setup();
494
495        // Setup the PDF class with custom size pages because WT supports more page sizes. If WT sends an unknown size name then the default would be A4
496        $this->tcpdf = new TcpdfWrapper($this->orientation, parent::UNITS, [
497            $this->page_width,
498            $this->page_height,
499        ], self::UNICODE, 'UTF-8', self::DISK_CACHE);
500
501        // Setup the PDF margins
502        $this->tcpdf->SetMargins($this->left_margin, $this->top_margin, $this->right_margin);
503        $this->tcpdf->setHeaderMargin($this->header_margin);
504        $this->tcpdf->setFooterMargin($this->footer_margin);
505        //Set auto page breaks
506        $this->tcpdf->SetAutoPageBreak(true, $this->bottom_margin);
507        // Set font subsetting
508        $this->tcpdf->setFontSubsetting(self::SUBSETTING);
509        // Setup PDF compression
510        $this->tcpdf->SetCompression(self::COMPRESSION);
511        // Setup RTL support
512        $this->tcpdf->setRTL($this->rtl);
513        // Set the document information
514        $this->tcpdf->SetCreator(Webtrees::NAME . ' ' . Webtrees::VERSION);
515        $this->tcpdf->SetAuthor($this->rauthor);
516        $this->tcpdf->SetTitle($this->title);
517        $this->tcpdf->SetSubject($this->rsubject);
518        $this->tcpdf->SetKeywords($this->rkeywords);
519
520        $this->setReport($this);
521
522        if ($this->show_generated_by) {
523            // The default style name for Generated by.... is 'genby'
524            $element = new ReportPdfCell(0, 10, 0, 'C', '', 'genby', 1, ReportBaseElement::CURRENT_POSITION, ReportBaseElement::CURRENT_POSITION, 0, 0, '', '', true);
525            $element->addText($this->generated_by);
526            $element->setUrl(Webtrees::URL);
527            $this->addFooter($element);
528        }
529    }
530
531    /**
532     * Add an element.
533     *
534     * @param ReportBaseElement|string $element
535     *
536     * @return void
537     */
538    public function addElement($element): void
539    {
540        if ($this->processing === 'B') {
541            $this->addBody($element);
542
543            return;
544        }
545
546        if ($this->processing === 'H') {
547            $this->addHeader($element);
548
549            return;
550        }
551
552        if ($this->processing === 'F') {
553            $this->addFooter($element);
554
555            return;
556        }
557    }
558
559    /**
560     * Run the report.
561     *
562     * @return void
563     */
564    public function run(): void
565    {
566        $this->body();
567        echo $this->tcpdf->Output('doc.pdf', 'S');
568    }
569
570    /**
571     * Create a new Cell object.
572     *
573     * @param int    $width   cell width (expressed in points)
574     * @param int    $height  cell height (expressed in points)
575     * @param mixed  $border  Border style
576     * @param string $align   Text alignement
577     * @param string $bgcolor Background color code
578     * @param string $style   The name of the text style
579     * @param int    $ln      Indicates where the current position should go after the call
580     * @param mixed  $top     Y-position
581     * @param mixed  $left    X-position
582     * @param int    $fill    Indicates if the cell background must be painted (1) or transparent (0). Default value: 1
583     * @param int    $stretch Stretch carachter mode
584     * @param string $bocolor Border color
585     * @param string $tcolor  Text color
586     * @param bool   $reseth
587     *
588     * @return ReportBaseCell
589     */
590    public function createCell($width, $height, $border, $align, $bgcolor, $style, $ln, $top, $left, $fill, $stretch, $bocolor, $tcolor, $reseth): ReportBaseCell
591    {
592        return new ReportPdfCell($width, $height, $border, $align, $bgcolor, $style, $ln, $top, $left, $fill, $stretch, $bocolor, $tcolor, $reseth);
593    }
594
595    /**
596     * Create a new TextBox object.
597     *
598     * @param float  $width   Text box width
599     * @param float  $height  Text box height
600     * @param bool   $border
601     * @param string $bgcolor Background color code in HTML
602     * @param bool   $newline
603     * @param float  $left
604     * @param float  $top
605     * @param bool   $pagecheck
606     * @param string $style
607     * @param bool   $fill
608     * @param bool   $padding
609     * @param bool   $reseth
610     *
611     * @return ReportBaseTextbox
612     */
613    public function createTextBox(
614        float $width,
615        float $height,
616        bool $border,
617        string $bgcolor,
618        bool $newline,
619        float $left,
620        float $top,
621        bool $pagecheck,
622        string $style,
623        bool $fill,
624        bool $padding,
625        bool $reseth
626    ): ReportBaseTextbox {
627        return new ReportPdfTextBox($width, $height, $border, $bgcolor, $newline, $left, $top, $pagecheck, $style, $fill, $padding, $reseth);
628    }
629
630    /**
631     * Create a text element.
632     *
633     * @param string $style
634     * @param string $color
635     *
636     * @return ReportBaseText
637     */
638    public function createText(string $style, string $color): ReportBaseText
639    {
640        return new ReportPdfText($style, $color);
641    }
642
643    /**
644     * Create a new Footnote object.
645     *
646     * @param string $style Style name
647     *
648     * @return ReportBaseFootnote
649     */
650    public function createFootnote($style): ReportBaseFootnote
651    {
652        return new ReportPdfFootnote($style);
653    }
654
655    /**
656     * Create a new Page Header object
657     *
658     * @return ReportBasePageHeader
659     */
660    public function createPageHeader(): ReportBasePageHeader
661    {
662        return new ReportPdfPageHeader();
663    }
664
665    /**
666     * Create a new image object.
667     *
668     * @param string $file  Filename
669     * @param float  $x
670     * @param float  $y
671     * @param float  $w     Image width
672     * @param float  $h     Image height
673     * @param string $align L:left, C:center, R:right or empty to use x/y
674     * @param string $ln    T:same line, N:next line
675     *
676     * @return ReportBaseImage
677     */
678    public function createImage(string $file, float $x, float $y, float $w, float $h, string $align, string $ln): ReportBaseImage
679    {
680        return new ReportPdfImage($file, $x, $y, $w, $h, $align, $ln);
681    }
682
683    /**
684     * Create a new image object from Media Object.
685     *
686     * @param MediaFile           $media_file
687     * @param float               $x
688     * @param float               $y
689     * @param float               $w     Image width
690     * @param float               $h     Image height
691     * @param string              $align L:left, C:center, R:right or empty to use x/y
692     * @param string              $ln    T:same line, N:next line
693     * @param FilesystemInterface $data_filesystem
694     *
695     * @return ReportBaseImage
696     */
697    public function createImageFromObject(
698        MediaFile $media_file,
699        float $x,
700        float $y,
701        float $w,
702        float $h,
703        string $align,
704        string $ln,
705        FilesystemInterface $data_filesystem
706    ): ReportBaseImage {
707        return new ReportPdfImage('@' . $media_file->fileContents($data_filesystem), $x, $y, $w, $h, $align, $ln);
708    }
709
710    /**
711     * Create a line.
712     *
713     * @param float $x1
714     * @param float $y1
715     * @param float $x2
716     * @param float $y2
717     *
718     * @return ReportBaseLine
719     */
720    public function createLine(float $x1, float $y1, float $x2, float $y2): ReportBaseLine
721    {
722        return new ReportPdfLine($x1, $y1, $x2, $y2);
723    }
724
725    /**
726     * Create an HTML element.
727     *
728     * @param string   $tag
729     * @param string[] $attrs
730     *
731     * @return ReportBaseHtml
732     */
733    public function createHTML(string $tag, array $attrs): ReportBaseHtml
734    {
735        return new ReportPdfHtml($tag, $attrs);
736    }
737}
738