1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2023 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; 24 25use function count; 26 27/** 28 * Class PdfRenderer 29 */ 30class PdfRenderer extends AbstractRenderer 31{ 32 /** 33 * PDF compression - Zlib extension is required 34 * 35 * @var bool const 36 */ 37 private const COMPRESSION = true; 38 39 /** 40 * If true reduce the RAM memory usage by caching temporary data on filesystem (slower). 41 * 42 * @var bool const 43 */ 44 private const DISK_CACHE = false; 45 46 /** 47 * true means that the input text is unicode (PDF) 48 * 49 * @var bool const 50 */ 51 private const UNICODE = true; 52 53 // Font sub-setting in TCPDF is slow. 54 private const SUBSETTING = false; 55 56 public TcpdfWrapper $tcpdf; 57 58 /** @var array<ReportPdfFootnote> Array of elements in the footer notes */ 59 public array $printedfootnotes = []; 60 61 // The last cell height 62 public float $lastCellHeight = 0.0; 63 64 // The largest font size within a TextBox to calculate the height 65 public float $largestFontHeight = 0.0; 66 67 // The last pictures page number 68 public int $lastpicpage = 0; 69 70 /** 71 * PDF Header -PDF 72 * 73 * @return void 74 */ 75 public function header(): void 76 { 77 foreach ($this->headerElements as $element) { 78 if ($element instanceof ReportBaseElement) { 79 $element->render($this); 80 } elseif ($element === 'footnotetexts') { 81 $this->footnotes(); 82 } elseif ($element === 'addpage') { 83 $this->newPage(); 84 } 85 } 86 } 87 88 /** 89 * PDF Body -PDF 90 * 91 * @return void 92 */ 93 public function body(): void 94 { 95 $this->tcpdf->AddPage(); 96 97 foreach ($this->bodyElements as $element) { 98 if ($element instanceof ReportBaseElement) { 99 $element->render($this); 100 } elseif ($element === 'footnotetexts') { 101 $this->footnotes(); 102 } elseif ($element === 'addpage') { 103 $this->newPage(); 104 } 105 } 106 } 107 108 /** 109 * Generate footnotes 110 * 111 * @return void 112 */ 113 public function footnotes(): void 114 { 115 foreach ($this->printedfootnotes as $element) { 116 if ($this->tcpdf->GetY() + $element->getFootnoteHeight($this) > $this->tcpdf->getPageHeight()) { 117 $this->tcpdf->AddPage(); 118 } 119 120 $element->renderFootnote($this); 121 122 if ($this->tcpdf->GetY() > $this->tcpdf->getPageHeight()) { 123 $this->tcpdf->AddPage(); 124 } 125 } 126 } 127 128 /** 129 * PDF Footer -PDF 130 * 131 * @return void 132 */ 133 public function footer(): void 134 { 135 foreach ($this->footerElements as $element) { 136 if ($element instanceof ReportBaseElement) { 137 $element->render($this); 138 } elseif ($element === 'footnotetexts') { 139 $this->footnotes(); 140 } elseif ($element === 'addpage') { 141 $this->newPage(); 142 } 143 } 144 } 145 146 /** 147 * Remove the header. 148 * 149 * @param int $index 150 * 151 * @return void 152 */ 153 public function removeHeader(int $index): void 154 { 155 unset($this->headerElements[$index]); 156 } 157 158 /** 159 * Remove the body. 160 * 161 * @param int $index 162 * 163 * @return void 164 */ 165 public function removeBody(int $index): void 166 { 167 unset($this->bodyElements[$index]); 168 } 169 170 /** 171 * Clear the Header -PDF 172 * 173 * @return void 174 */ 175 public function clearHeader(): void 176 { 177 unset($this->headerElements); 178 $this->headerElements = []; 179 } 180 181 /** 182 * Get the currently used style name -PDF 183 * 184 * @return string 185 */ 186 public function getCurrentStyle(): string 187 { 188 return $this->currentStyle; 189 } 190 191 /** 192 * Setup a style for usage -PDF 193 * 194 * @param string $s Style name 195 * 196 * @return void 197 */ 198 public function setCurrentStyle(string $s): void 199 { 200 $this->currentStyle = $s; 201 $style = $this->getStyle($s); 202 $this->tcpdf->setFont($style['font'], $style['style'], $style['size']); 203 } 204 205 /** 206 * Get the style -PDF 207 * 208 * @param string $s Style name 209 * 210 * @return array{'name': string, 'font': string, 'style': string, 'size': float} 211 */ 212 public function getStyle(string $s): array 213 { 214 return $this->styles[$s] ?? $this->styles[$this->getCurrentStyle()]; 215 } 216 217 /** 218 * Add margin when static horizontal position is used -PDF 219 * RTL supported 220 * 221 * @param float $x Static position 222 * 223 * @return float 224 */ 225 public function addMarginX(float $x): float 226 { 227 $m = $this->tcpdf->getMargins(); 228 if ($this->tcpdf->getRTL()) { 229 $x += $m['right']; 230 } else { 231 $x += $m['left']; 232 } 233 $this->tcpdf->setX($x); 234 235 return $x; 236 } 237 238 /** 239 * Get the maximum line width to draw from the curren position -PDF 240 * RTL supported 241 * 242 * @return float 243 */ 244 public function getMaxLineWidth(): float 245 { 246 $m = $this->tcpdf->getMargins(); 247 if ($this->tcpdf->getRTL()) { 248 return $this->tcpdf->getRemainingWidth() + $m['right']; 249 } 250 251 return $this->tcpdf->getRemainingWidth() + $m['left']; 252 } 253 254 /** 255 * Get the height of the footnote. 256 * 257 * @return float 258 */ 259 public function getFootnotesHeight(): float 260 { 261 $h = 0; 262 foreach ($this->printedfootnotes as $element) { 263 $h += $element->getHeight($this); 264 } 265 266 return $h; 267 } 268 269 /** 270 * Returns the the current font size height -PDF 271 * 272 * @return float 273 */ 274 public function getCurrentStyleHeight(): float 275 { 276 if ($this->currentStyle === '') { 277 return $this->default_font_size; 278 } 279 $style = $this->getStyle($this->currentStyle); 280 281 return $style['size']; 282 } 283 284 /** 285 * Checks the Footnote and numbers them 286 * 287 * @param ReportPdfFootnote $footnote 288 * 289 * @return ReportPdfFootnote|bool object if already numbered, false otherwise 290 */ 291 public function checkFootnote(ReportPdfFootnote $footnote) 292 { 293 $ct = count($this->printedfootnotes); 294 $val = $footnote->getValue(); 295 $i = 0; 296 while ($i < $ct) { 297 if ($this->printedfootnotes[$i]->getValue() === $val) { 298 // If this footnote already exist then set up the numbers for this object 299 $footnote->setNum($i + 1); 300 $footnote->setAddlink((string) ($i + 1)); 301 302 return $this->printedfootnotes[$i]; 303 } 304 $i++; 305 } 306 // If this Footnote has not been set up yet 307 $footnote->setNum($ct + 1); 308 $footnote->setAddlink((string) $this->tcpdf->AddLink()); 309 $this->printedfootnotes[] = $footnote; 310 311 return false; 312 } 313 314 /** 315 * Used this function instead of AddPage() 316 * This function will make sure that images will not be overwritten 317 * 318 * @return void 319 */ 320 public function newPage(): void 321 { 322 if ($this->lastpicpage > $this->tcpdf->getPage()) { 323 $this->tcpdf->setPage($this->lastpicpage); 324 } 325 $this->tcpdf->AddPage(); 326 } 327 328 /** 329 * Add a page if needed -PDF 330 * 331 * @param float $height Cell height 332 * 333 * @return bool true in case of page break, false otherwise 334 */ 335 public function checkPageBreakPDF(float $height): bool 336 { 337 return $this->tcpdf->checkPageBreak($height); 338 } 339 340 /** 341 * Returns the remaining width between the current position and margins -PDF 342 * 343 * @return float Remaining width 344 */ 345 public function getRemainingWidthPDF(): float 346 { 347 return $this->tcpdf->getRemainingWidth(); 348 } 349 /** 350 * PDF Setup - ReportPdf 351 * 352 * @return void 353 */ 354 public function setup(): void 355 { 356 parent::setup(); 357 358 $this->tcpdf = new TcpdfWrapper( 359 $this->orientation, 360 self::UNITS, 361 [$this->page_width, $this->page_height], 362 self::UNICODE, 363 'UTF-8', 364 self::DISK_CACHE 365 ); 366 367 $this->tcpdf->setMargins($this->left_margin, $this->top_margin, $this->right_margin); 368 $this->tcpdf->setHeaderMargin($this->header_margin); 369 $this->tcpdf->setFooterMargin($this->footer_margin); 370 $this->tcpdf->setAutoPageBreak(true, $this->bottom_margin); 371 $this->tcpdf->setFontSubsetting(self::SUBSETTING); 372 $this->tcpdf->setCompression(self::COMPRESSION); 373 $this->tcpdf->setRTL($this->rtl); 374 $this->tcpdf->setCreator(Webtrees::NAME . ' ' . Webtrees::VERSION); 375 $this->tcpdf->setAuthor($this->rauthor); 376 $this->tcpdf->setTitle($this->title); 377 $this->tcpdf->setSubject($this->rsubject); 378 $this->tcpdf->setKeywords($this->rkeywords); 379 $this->tcpdf->setHeaderData('', 0, $this->title); 380 $this->tcpdf->setHeaderFont([$this->default_font, '', $this->default_font_size]); 381 382 if ($this->show_generated_by) { 383 // The default style name for Generated by.... is 'genby' 384 $element = new ReportPdfCell(0.0, 10.0, '', 'C', '', 'genby', 1, ReportBaseElement::CURRENT_POSITION, ReportBaseElement::CURRENT_POSITION, false, 0, '', '', true); 385 $element->addText($this->generated_by); 386 $element->setUrl(Webtrees::URL); 387 $this->addElementToFooter($element); 388 } 389 } 390 391 /** 392 * Run the report. 393 * 394 * @return void 395 */ 396 public function run(): void 397 { 398 $this->body(); 399 echo $this->tcpdf->Output('doc.pdf', 'S'); 400 } 401 402 /** 403 * Create a new Cell object. 404 * 405 * @param float $width cell width (expressed in points) 406 * @param float $height cell height (expressed in points) 407 * @param string $border Border style 408 * @param string $align Text alignment 409 * @param string $bgcolor Background color code 410 * @param string $style The name of the text style 411 * @param int $ln Indicates where the current position should go after the call 412 * @param float $top Y-position 413 * @param float $left X-position 414 * @param bool $fill Indicates if the cell background must be painted (1) or transparent (0). Default value: 1 415 * @param int $stretch Stretch carachter mode 416 * @param string $bocolor Border color 417 * @param string $tcolor Text color 418 * @param bool $reseth 419 * 420 * @return ReportBaseCell 421 */ 422 public function createCell(float $width, float $height, string $border, string $align, string $bgcolor, string $style, int $ln, float $top, float $left, bool $fill, int $stretch, string $bocolor, string $tcolor, bool $reseth): ReportBaseCell 423 { 424 return new ReportPdfCell($width, $height, $border, $align, $bgcolor, $style, $ln, $top, $left, $fill, $stretch, $bocolor, $tcolor, $reseth); 425 } 426 427 /** 428 * Create a new TextBox object. 429 * 430 * @param float $width Text box width 431 * @param float $height Text box height 432 * @param bool $border 433 * @param string $bgcolor Background color code in HTML 434 * @param bool $newline 435 * @param float $left 436 * @param float $top 437 * @param bool $pagecheck 438 * @param string $style 439 * @param bool $fill 440 * @param bool $padding 441 * @param bool $reseth 442 * 443 * @return ReportBaseTextbox 444 */ 445 public function createTextBox( 446 float $width, 447 float $height, 448 bool $border, 449 string $bgcolor, 450 bool $newline, 451 float $left, 452 float $top, 453 bool $pagecheck, 454 string $style, 455 bool $fill, 456 bool $padding, 457 bool $reseth 458 ): ReportBaseTextbox { 459 return new ReportPdfTextBox($width, $height, $border, $bgcolor, $newline, $left, $top, $pagecheck, $style, $fill, $padding, $reseth); 460 } 461 462 /** 463 * Create a text element. 464 * 465 * @param string $style 466 * @param string $color 467 * 468 * @return ReportBaseText 469 */ 470 public function createText(string $style, string $color): ReportBaseText 471 { 472 return new ReportPdfText($style, $color); 473 } 474 475 /** 476 * Create a new Footnote object. 477 * 478 * @param string $style Style name 479 * 480 * @return ReportBaseFootnote 481 */ 482 public function createFootnote(string $style): ReportBaseFootnote 483 { 484 return new ReportPdfFootnote($style); 485 } 486 487 /** 488 * Create a new image object. 489 * 490 * @param string $file Filename 491 * @param float $x 492 * @param float $y 493 * @param float $w Image width 494 * @param float $h Image height 495 * @param string $align L:left, C:center, R:right or empty to use x/y 496 * @param string $ln T:same line, N:next line 497 * 498 * @return ReportBaseImage 499 */ 500 public function createImage(string $file, float $x, float $y, float $w, float $h, string $align, string $ln): ReportBaseImage 501 { 502 return new ReportPdfImage($file, $x, $y, $w, $h, $align, $ln); 503 } 504 505 /** 506 * Create a new image object from Media Object. 507 * 508 * @param MediaFile $media_file 509 * @param float $x 510 * @param float $y 511 * @param float $w Image width 512 * @param float $h Image height 513 * @param string $align L:left, C:center, R:right or empty to use x/y 514 * @param string $ln T:same line, N:next line 515 * 516 * @return ReportBaseImage 517 */ 518 public function createImageFromObject( 519 MediaFile $media_file, 520 float $x, 521 float $y, 522 float $w, 523 float $h, 524 string $align, 525 string $ln 526 ): ReportBaseImage { 527 return new ReportPdfImage('@' . $media_file->fileContents(), $x, $y, $w, $h, $align, $ln); 528 } 529 530 /** 531 * Create a line. 532 * 533 * @param float $x1 534 * @param float $y1 535 * @param float $x2 536 * @param float $y2 537 * 538 * @return ReportBaseLine 539 */ 540 public function createLine(float $x1, float $y1, float $x2, float $y2): ReportBaseLine 541 { 542 return new ReportPdfLine($x1, $y1, $x2, $y2); 543 } 544} 545