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 /** 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 $this->tcpdf->SetHeaderData('', 0, $this->title); 472 $this->tcpdf->setHeaderFont([$this->default_font, '', $this->default_font_size]); 473 474 $this->setReport($this); 475 476 if ($this->show_generated_by) { 477 // The default style name for Generated by.... is 'genby' 478 $element = new ReportPdfCell(0, 10, 0, 'C', '', 'genby', 1, ReportBaseElement::CURRENT_POSITION, ReportBaseElement::CURRENT_POSITION, 0, 0, '', '', true); 479 $element->addText($this->generated_by); 480 $element->setUrl(Webtrees::URL); 481 $this->addFooter($element); 482 } 483 } 484 485 /** 486 * Add an element. 487 * 488 * @param ReportBaseElement|string $element 489 * 490 * @return void 491 */ 492 public function addElement($element): void 493 { 494 if ($this->processing === 'B') { 495 $this->addBody($element); 496 497 return; 498 } 499 500 if ($this->processing === 'H') { 501 $this->addHeader($element); 502 503 return; 504 } 505 506 if ($this->processing === 'F') { 507 $this->addFooter($element); 508 509 return; 510 } 511 } 512 513 /** 514 * Run the report. 515 * 516 * @return void 517 */ 518 public function run(): void 519 { 520 $this->body(); 521 echo $this->tcpdf->Output('doc.pdf', 'S'); 522 } 523 524 /** 525 * Create a new Cell object. 526 * 527 * @param int $width cell width (expressed in points) 528 * @param int $height cell height (expressed in points) 529 * @param mixed $border Border style 530 * @param string $align Text alignement 531 * @param string $bgcolor Background color code 532 * @param string $style The name of the text style 533 * @param int $ln Indicates where the current position should go after the call 534 * @param mixed $top Y-position 535 * @param mixed $left X-position 536 * @param int $fill Indicates if the cell background must be painted (1) or transparent (0). Default value: 1 537 * @param int $stretch Stretch carachter mode 538 * @param string $bocolor Border color 539 * @param string $tcolor Text color 540 * @param bool $reseth 541 * 542 * @return ReportBaseCell 543 */ 544 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 545 { 546 return new ReportPdfCell($width, $height, $border, $align, $bgcolor, $style, $ln, $top, $left, $fill, $stretch, $bocolor, $tcolor, $reseth); 547 } 548 549 /** 550 * Create a new TextBox object. 551 * 552 * @param float $width Text box width 553 * @param float $height Text box height 554 * @param bool $border 555 * @param string $bgcolor Background color code in HTML 556 * @param bool $newline 557 * @param float $left 558 * @param float $top 559 * @param bool $pagecheck 560 * @param string $style 561 * @param bool $fill 562 * @param bool $padding 563 * @param bool $reseth 564 * 565 * @return ReportBaseTextbox 566 */ 567 public function createTextBox( 568 float $width, 569 float $height, 570 bool $border, 571 string $bgcolor, 572 bool $newline, 573 float $left, 574 float $top, 575 bool $pagecheck, 576 string $style, 577 bool $fill, 578 bool $padding, 579 bool $reseth 580 ): ReportBaseTextbox { 581 return new ReportPdfTextBox($width, $height, $border, $bgcolor, $newline, $left, $top, $pagecheck, $style, $fill, $padding, $reseth); 582 } 583 584 /** 585 * Create a text element. 586 * 587 * @param string $style 588 * @param string $color 589 * 590 * @return ReportBaseText 591 */ 592 public function createText(string $style, string $color): ReportBaseText 593 { 594 return new ReportPdfText($style, $color); 595 } 596 597 /** 598 * Create a new Footnote object. 599 * 600 * @param string $style Style name 601 * 602 * @return ReportBaseFootnote 603 */ 604 public function createFootnote(string $style): ReportBaseFootnote 605 { 606 return new ReportPdfFootnote($style); 607 } 608 609 /** 610 * Create a new image object. 611 * 612 * @param string $file Filename 613 * @param float $x 614 * @param float $y 615 * @param float $w Image width 616 * @param float $h Image height 617 * @param string $align L:left, C:center, R:right or empty to use x/y 618 * @param string $ln T:same line, N:next line 619 * 620 * @return ReportBaseImage 621 */ 622 public function createImage(string $file, float $x, float $y, float $w, float $h, string $align, string $ln): ReportBaseImage 623 { 624 return new ReportPdfImage($file, $x, $y, $w, $h, $align, $ln); 625 } 626 627 /** 628 * Create a new image object from Media Object. 629 * 630 * @param MediaFile $media_file 631 * @param float $x 632 * @param float $y 633 * @param float $w Image width 634 * @param float $h Image height 635 * @param string $align L:left, C:center, R:right or empty to use x/y 636 * @param string $ln T:same line, N:next line 637 * @param FilesystemOperator $data_filesystem 638 * 639 * @return ReportBaseImage 640 */ 641 public function createImageFromObject( 642 MediaFile $media_file, 643 float $x, 644 float $y, 645 float $w, 646 float $h, 647 string $align, 648 string $ln, 649 FilesystemOperator $data_filesystem 650 ): ReportBaseImage { 651 return new ReportPdfImage('@' . $media_file->fileContents($data_filesystem), $x, $y, $w, $h, $align, $ln); 652 } 653 654 /** 655 * Create a line. 656 * 657 * @param float $x1 658 * @param float $y1 659 * @param float $x2 660 * @param float $y2 661 * 662 * @return ReportBaseLine 663 */ 664 public function createLine(float $x1, float $y1, float $x2, float $y2): ReportBaseLine 665 { 666 return new ReportPdfLine($x1, $y1, $x2, $y2); 667 } 668} 669