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