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