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