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