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 * Set the report. 199 * 200 * @param PdfRenderer $report 201 * 202 * @return void 203 */ 204 public function setReport(PdfRenderer $report): void 205 { 206 $this->wt_report = $report; 207 } 208 209 /** 210 * Get the currently used style name -PDF 211 * 212 * @return string 213 */ 214 public function getCurrentStyle(): string 215 { 216 return $this->currentStyle; 217 } 218 219 /** 220 * Setup a style for usage -PDF 221 * 222 * @param string $s Style name 223 * 224 * @return void 225 */ 226 public function setCurrentStyle(string $s): void 227 { 228 $this->currentStyle = $s; 229 $style = $this->wt_report->getStyle($s); 230 $this->tcpdf->SetFont($style['font'], $style['style'], $style['size']); 231 } 232 233 /** 234 * Get the style -PDF 235 * 236 * @param string $s Style name 237 * 238 * @return array 239 */ 240 public function getStyle(string $s): array 241 { 242 if (!isset($this->wt_report->styles[$s])) { 243 $s = $this->getCurrentStyle(); 244 $this->wt_report->styles[$s] = $s; 245 } 246 247 return $this->wt_report->styles[$s]; 248 } 249 250 /** 251 * Add margin when static horizontal position is used -PDF 252 * RTL supported 253 * 254 * @param float $x Static position 255 * 256 * @return float 257 */ 258 public function addMarginX(float $x): float 259 { 260 $m = $this->tcpdf->getMargins(); 261 if ($this->tcpdf->getRTL()) { 262 $x += $m['right']; 263 } else { 264 $x += $m['left']; 265 } 266 $this->tcpdf->SetX($x); 267 268 return $x; 269 } 270 271 /** 272 * Get the maximum line width to draw from the curren position -PDF 273 * RTL supported 274 * 275 * @return float 276 */ 277 public function getMaxLineWidth(): float 278 { 279 $m = $this->tcpdf->getMargins(); 280 if ($this->tcpdf->getRTL()) { 281 return $this->tcpdf->getRemainingWidth() + $m['right']; 282 } 283 284 return $this->tcpdf->getRemainingWidth() + $m['left']; 285 } 286 287 /** 288 * Get the height of the footnote. 289 * 290 * @return float 291 */ 292 public function getFootnotesHeight(): float 293 { 294 $h = 0; 295 foreach ($this->printedfootnotes as $element) { 296 $h += $element->getHeight($this); 297 } 298 299 return $h; 300 } 301 302 /** 303 * Returns the the current font size height -PDF 304 * 305 * @return float 306 */ 307 public function getCurrentStyleHeight(): float 308 { 309 if ($this->currentStyle === '') { 310 return $this->wt_report->default_font_size; 311 } 312 $style = $this->wt_report->getStyle($this->currentStyle); 313 314 return (float) $style['size']; 315 } 316 317 /** 318 * Checks the Footnote and numbers them 319 * 320 * @param ReportPdfFootnote $footnote 321 * 322 * @return ReportPdfFootnote|bool object if already numbered, false otherwise 323 */ 324 public function checkFootnote(ReportPdfFootnote $footnote) 325 { 326 $ct = count($this->printedfootnotes); 327 $val = $footnote->getValue(); 328 $i = 0; 329 while ($i < $ct) { 330 if ($this->printedfootnotes[$i]->getValue() == $val) { 331 // If this footnote already exist then set up the numbers for this object 332 $footnote->setNum($i + 1); 333 $footnote->setAddlink((string) ($i + 1)); 334 335 return $this->printedfootnotes[$i]; 336 } 337 $i++; 338 } 339 // If this Footnote has not been set up yet 340 $footnote->setNum($ct + 1); 341 $footnote->setAddlink((string) $this->tcpdf->AddLink()); 342 $this->printedfootnotes[] = $footnote; 343 344 return false; 345 } 346 347 /** 348 * Used this function instead of AddPage() 349 * This function will make sure that images will not be overwritten 350 * 351 * @return void 352 */ 353 public function newPage(): void 354 { 355 if ($this->lastpicpage > $this->tcpdf->getPage()) { 356 $this->tcpdf->setPage($this->lastpicpage); 357 } 358 $this->tcpdf->AddPage(); 359 } 360 361 /** 362 * Add a page if needed -PDF 363 * 364 * @param float $height Cell height 365 * 366 * @return bool true in case of page break, false otherwise 367 */ 368 public function checkPageBreakPDF(float $height): bool 369 { 370 return $this->tcpdf->checkPageBreak($height); 371 } 372 373 /** 374 * Returns the remaining width between the current position and margins -PDF 375 * 376 * @return float Remaining width 377 */ 378 public function getRemainingWidthPDF(): float 379 { 380 return $this->tcpdf->getRemainingWidth(); 381 } 382 /** 383 * PDF Setup - ReportPdf 384 * 385 * @return void 386 */ 387 public function setup(): void 388 { 389 parent::setup(); 390 391 $this->tcpdf = new TcpdfWrapper( 392 $this->orientation, 393 self::UNITS, 394 [$this->page_width, $this->page_height], 395 self::UNICODE, 396 'UTF-8', 397 self::DISK_CACHE 398 ); 399 400 $this->tcpdf->SetMargins($this->left_margin, $this->top_margin, $this->right_margin); 401 $this->tcpdf->setHeaderMargin($this->header_margin); 402 $this->tcpdf->setFooterMargin($this->footer_margin); 403 $this->tcpdf->SetAutoPageBreak(true, $this->bottom_margin); 404 $this->tcpdf->setFontSubsetting(self::SUBSETTING); 405 $this->tcpdf->SetCompression(self::COMPRESSION); 406 $this->tcpdf->setRTL($this->rtl); 407 $this->tcpdf->SetCreator(Webtrees::NAME . ' ' . Webtrees::VERSION); 408 $this->tcpdf->SetAuthor($this->rauthor); 409 $this->tcpdf->SetTitle($this->title); 410 $this->tcpdf->SetSubject($this->rsubject); 411 $this->tcpdf->SetKeywords($this->rkeywords); 412 $this->tcpdf->SetHeaderData('', 0, $this->title); 413 $this->tcpdf->setHeaderFont([$this->default_font, '', $this->default_font_size]); 414 415 $this->setReport($this); 416 417 if ($this->show_generated_by) { 418 // The default style name for Generated by.... is 'genby' 419 $element = new ReportPdfCell(0, 10, 0, 'C', '', 'genby', 1, ReportBaseElement::CURRENT_POSITION, ReportBaseElement::CURRENT_POSITION, 0, 0, '', '', true); 420 $element->addText($this->generated_by); 421 $element->setUrl(Webtrees::URL); 422 $this->addElementToFooter($element); 423 } 424 } 425 426 /** 427 * Run the report. 428 * 429 * @return void 430 */ 431 public function run(): void 432 { 433 $this->body(); 434 echo $this->tcpdf->Output('doc.pdf', 'S'); 435 } 436 437 /** 438 * Create a new Cell object. 439 * 440 * @param int $width cell width (expressed in points) 441 * @param int $height cell height (expressed in points) 442 * @param mixed $border Border style 443 * @param string $align Text alignement 444 * @param string $bgcolor Background color code 445 * @param string $style The name of the text style 446 * @param int $ln Indicates where the current position should go after the call 447 * @param mixed $top Y-position 448 * @param mixed $left X-position 449 * @param int $fill Indicates if the cell background must be painted (1) or transparent (0). Default value: 1 450 * @param int $stretch Stretch carachter mode 451 * @param string $bocolor Border color 452 * @param string $tcolor Text color 453 * @param bool $reseth 454 * 455 * @return ReportBaseCell 456 */ 457 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 458 { 459 return new ReportPdfCell($width, $height, $border, $align, $bgcolor, $style, $ln, $top, $left, $fill, $stretch, $bocolor, $tcolor, $reseth); 460 } 461 462 /** 463 * Create a new TextBox object. 464 * 465 * @param float $width Text box width 466 * @param float $height Text box height 467 * @param bool $border 468 * @param string $bgcolor Background color code in HTML 469 * @param bool $newline 470 * @param float $left 471 * @param float $top 472 * @param bool $pagecheck 473 * @param string $style 474 * @param bool $fill 475 * @param bool $padding 476 * @param bool $reseth 477 * 478 * @return ReportBaseTextbox 479 */ 480 public function createTextBox( 481 float $width, 482 float $height, 483 bool $border, 484 string $bgcolor, 485 bool $newline, 486 float $left, 487 float $top, 488 bool $pagecheck, 489 string $style, 490 bool $fill, 491 bool $padding, 492 bool $reseth 493 ): ReportBaseTextbox { 494 return new ReportPdfTextBox($width, $height, $border, $bgcolor, $newline, $left, $top, $pagecheck, $style, $fill, $padding, $reseth); 495 } 496 497 /** 498 * Create a text element. 499 * 500 * @param string $style 501 * @param string $color 502 * 503 * @return ReportBaseText 504 */ 505 public function createText(string $style, string $color): ReportBaseText 506 { 507 return new ReportPdfText($style, $color); 508 } 509 510 /** 511 * Create a new Footnote object. 512 * 513 * @param string $style Style name 514 * 515 * @return ReportBaseFootnote 516 */ 517 public function createFootnote(string $style): ReportBaseFootnote 518 { 519 return new ReportPdfFootnote($style); 520 } 521 522 /** 523 * Create a new image object. 524 * 525 * @param string $file Filename 526 * @param float $x 527 * @param float $y 528 * @param float $w Image width 529 * @param float $h Image height 530 * @param string $align L:left, C:center, R:right or empty to use x/y 531 * @param string $ln T:same line, N:next line 532 * 533 * @return ReportBaseImage 534 */ 535 public function createImage(string $file, float $x, float $y, float $w, float $h, string $align, string $ln): ReportBaseImage 536 { 537 return new ReportPdfImage($file, $x, $y, $w, $h, $align, $ln); 538 } 539 540 /** 541 * Create a new image object from Media Object. 542 * 543 * @param MediaFile $media_file 544 * @param float $x 545 * @param float $y 546 * @param float $w Image width 547 * @param float $h Image height 548 * @param string $align L:left, C:center, R:right or empty to use x/y 549 * @param string $ln T:same line, N:next line 550 * @param FilesystemOperator $data_filesystem 551 * 552 * @return ReportBaseImage 553 */ 554 public function createImageFromObject( 555 MediaFile $media_file, 556 float $x, 557 float $y, 558 float $w, 559 float $h, 560 string $align, 561 string $ln, 562 FilesystemOperator $data_filesystem 563 ): ReportBaseImage { 564 return new ReportPdfImage('@' . $media_file->fileContents($data_filesystem), $x, $y, $w, $h, $align, $ln); 565 } 566 567 /** 568 * Create a line. 569 * 570 * @param float $x1 571 * @param float $y1 572 * @param float $x2 573 * @param float $y2 574 * 575 * @return ReportBaseLine 576 */ 577 public function createLine(float $x1, float $y1, float $x2, float $y2): ReportBaseLine 578 { 579 return new ReportPdfLine($x1, $y1, $x2, $y2); 580 } 581} 582