Differences Between: [Versions 310 and 400] [Versions 311 and 400] [Versions 39 and 400] [Versions 400 and 401] [Versions 400 and 402] [Versions 400 and 403]
1 <?php 2 3 namespace PhpOffice\PhpSpreadsheet\Writer; 4 5 use HTMLPurifier; 6 use PhpOffice\PhpSpreadsheet\Calculation\Calculation; 7 use PhpOffice\PhpSpreadsheet\Cell\Cell; 8 use PhpOffice\PhpSpreadsheet\Cell\Coordinate; 9 use PhpOffice\PhpSpreadsheet\Chart\Chart; 10 use PhpOffice\PhpSpreadsheet\RichText\RichText; 11 use PhpOffice\PhpSpreadsheet\RichText\Run; 12 use PhpOffice\PhpSpreadsheet\Settings; 13 use PhpOffice\PhpSpreadsheet\Shared\Drawing as SharedDrawing; 14 use PhpOffice\PhpSpreadsheet\Shared\File; 15 use PhpOffice\PhpSpreadsheet\Shared\Font as SharedFont; 16 use PhpOffice\PhpSpreadsheet\Shared\StringHelper; 17 use PhpOffice\PhpSpreadsheet\Spreadsheet; 18 use PhpOffice\PhpSpreadsheet\Style\Alignment; 19 use PhpOffice\PhpSpreadsheet\Style\Border; 20 use PhpOffice\PhpSpreadsheet\Style\Borders; 21 use PhpOffice\PhpSpreadsheet\Style\Fill; 22 use PhpOffice\PhpSpreadsheet\Style\Font; 23 use PhpOffice\PhpSpreadsheet\Style\NumberFormat; 24 use PhpOffice\PhpSpreadsheet\Style\Style; 25 use PhpOffice\PhpSpreadsheet\Worksheet\Drawing; 26 use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing; 27 use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; 28 29 class Html extends BaseWriter 30 { 31 /** 32 * Spreadsheet object. 33 * 34 * @var Spreadsheet 35 */ 36 protected $spreadsheet; 37 38 /** 39 * Sheet index to write. 40 * 41 * @var null|int 42 */ 43 private $sheetIndex = 0; 44 45 /** 46 * Images root. 47 * 48 * @var string 49 */ 50 private $imagesRoot = ''; 51 52 /** 53 * embed images, or link to images. 54 * 55 * @var bool 56 */ 57 private $embedImages = false; 58 59 /** 60 * Use inline CSS? 61 * 62 * @var bool 63 */ 64 private $useInlineCss = false; 65 66 /** 67 * Use embedded CSS? 68 * 69 * @var bool 70 */ 71 private $useEmbeddedCSS = true; 72 73 /** 74 * Array of CSS styles. 75 * 76 * @var array 77 */ 78 private $cssStyles; 79 80 /** 81 * Array of column widths in points. 82 * 83 * @var array 84 */ 85 private $columnWidths; 86 87 /** 88 * Default font. 89 * 90 * @var Font 91 */ 92 private $defaultFont; 93 94 /** 95 * Flag whether spans have been calculated. 96 * 97 * @var bool 98 */ 99 private $spansAreCalculated = false; 100 101 /** 102 * Excel cells that should not be written as HTML cells. 103 * 104 * @var array 105 */ 106 private $isSpannedCell = []; 107 108 /** 109 * Excel cells that are upper-left corner in a cell merge. 110 * 111 * @var array 112 */ 113 private $isBaseCell = []; 114 115 /** 116 * Excel rows that should not be written as HTML rows. 117 * 118 * @var array 119 */ 120 private $isSpannedRow = []; 121 122 /** 123 * Is the current writer creating PDF? 124 * 125 * @var bool 126 */ 127 protected $isPdf = false; 128 129 /** 130 * Generate the Navigation block. 131 * 132 * @var bool 133 */ 134 private $generateSheetNavigationBlock = true; 135 136 /** 137 * Callback for editing generated html. 138 * 139 * @var null|callable 140 */ 141 private $editHtmlCallback; 142 143 /** 144 * Create a new HTML. 145 */ 146 public function __construct(Spreadsheet $spreadsheet) 147 { 148 $this->spreadsheet = $spreadsheet; 149 $this->defaultFont = $this->spreadsheet->getDefaultStyle()->getFont(); 150 } 151 152 /** 153 * Save Spreadsheet to file. 154 * 155 * @param resource|string $filename 156 */ 157 public function save($filename, int $flags = 0): void 158 { 159 $this->processFlags($flags); 160 161 // Open file 162 $this->openFileHandle($filename); 163 164 // Write html 165 fwrite($this->fileHandle, $this->generateHTMLAll()); 166 167 // Close file 168 $this->maybeCloseFileHandle(); 169 } 170 171 /** 172 * Save Spreadsheet as html to variable. 173 * 174 * @return string 175 */ 176 public function generateHtmlAll() 177 { 178 // garbage collect 179 $this->spreadsheet->garbageCollect(); 180 181 $saveDebugLog = Calculation::getInstance($this->spreadsheet)->getDebugLog()->getWriteDebugLog(); 182 Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog(false); 183 $saveArrayReturnType = Calculation::getArrayReturnType(); 184 Calculation::setArrayReturnType(Calculation::RETURN_ARRAY_AS_VALUE); 185 186 // Build CSS 187 $this->buildCSS(!$this->useInlineCss); 188 189 $html = ''; 190 191 // Write headers 192 $html .= $this->generateHTMLHeader(!$this->useInlineCss); 193 194 // Write navigation (tabs) 195 if ((!$this->isPdf) && ($this->generateSheetNavigationBlock)) { 196 $html .= $this->generateNavigation(); 197 } 198 199 // Write data 200 $html .= $this->generateSheetData(); 201 202 // Write footer 203 $html .= $this->generateHTMLFooter(); 204 $callback = $this->editHtmlCallback; 205 if ($callback) { 206 $html = $callback($html); 207 } 208 209 Calculation::setArrayReturnType($saveArrayReturnType); 210 Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog($saveDebugLog); 211 212 return $html; 213 } 214 215 /** 216 * Set a callback to edit the entire HTML. 217 * 218 * The callback must accept the HTML as string as first parameter, 219 * and it must return the edited HTML as string. 220 */ 221 public function setEditHtmlCallback(?callable $callback): void 222 { 223 $this->editHtmlCallback = $callback; 224 } 225 226 const VALIGN_ARR = [ 227 Alignment::VERTICAL_BOTTOM => 'bottom', 228 Alignment::VERTICAL_TOP => 'top', 229 Alignment::VERTICAL_CENTER => 'middle', 230 Alignment::VERTICAL_JUSTIFY => 'middle', 231 ]; 232 233 /** 234 * Map VAlign. 235 * 236 * @param string $vAlign Vertical alignment 237 * 238 * @return string 239 */ 240 private function mapVAlign($vAlign) 241 { 242 return array_key_exists($vAlign, self::VALIGN_ARR) ? self::VALIGN_ARR[$vAlign] : 'baseline'; 243 } 244 245 const HALIGN_ARR = [ 246 Alignment::HORIZONTAL_LEFT => 'left', 247 Alignment::HORIZONTAL_RIGHT => 'right', 248 Alignment::HORIZONTAL_CENTER => 'center', 249 Alignment::HORIZONTAL_CENTER_CONTINUOUS => 'center', 250 Alignment::HORIZONTAL_JUSTIFY => 'justify', 251 ]; 252 253 /** 254 * Map HAlign. 255 * 256 * @param string $hAlign Horizontal alignment 257 * 258 * @return string 259 */ 260 private function mapHAlign($hAlign) 261 { 262 return array_key_exists($hAlign, self::HALIGN_ARR) ? self::HALIGN_ARR[$hAlign] : ''; 263 } 264 265 const BORDER_ARR = [ 266 Border::BORDER_NONE => 'none', 267 Border::BORDER_DASHDOT => '1px dashed', 268 Border::BORDER_DASHDOTDOT => '1px dotted', 269 Border::BORDER_DASHED => '1px dashed', 270 Border::BORDER_DOTTED => '1px dotted', 271 Border::BORDER_DOUBLE => '3px double', 272 Border::BORDER_HAIR => '1px solid', 273 Border::BORDER_MEDIUM => '2px solid', 274 Border::BORDER_MEDIUMDASHDOT => '2px dashed', 275 Border::BORDER_MEDIUMDASHDOTDOT => '2px dotted', 276 Border::BORDER_SLANTDASHDOT => '2px dashed', 277 Border::BORDER_THICK => '3px solid', 278 ]; 279 280 /** 281 * Map border style. 282 * 283 * @param int $borderStyle Sheet index 284 * 285 * @return string 286 */ 287 private function mapBorderStyle($borderStyle) 288 { 289 return array_key_exists($borderStyle, self::BORDER_ARR) ? self::BORDER_ARR[$borderStyle] : '1px solid'; 290 } 291 292 /** 293 * Get sheet index. 294 */ 295 public function getSheetIndex(): ?int 296 { 297 return $this->sheetIndex; 298 } 299 300 /** 301 * Set sheet index. 302 * 303 * @param int $sheetIndex Sheet index 304 * 305 * @return $this 306 */ 307 public function setSheetIndex($sheetIndex) 308 { 309 $this->sheetIndex = $sheetIndex; 310 311 return $this; 312 } 313 314 /** 315 * Get sheet index. 316 * 317 * @return bool 318 */ 319 public function getGenerateSheetNavigationBlock() 320 { 321 return $this->generateSheetNavigationBlock; 322 } 323 324 /** 325 * Set sheet index. 326 * 327 * @param bool $generateSheetNavigationBlock Flag indicating whether the sheet navigation block should be generated or not 328 * 329 * @return $this 330 */ 331 public function setGenerateSheetNavigationBlock($generateSheetNavigationBlock) 332 { 333 $this->generateSheetNavigationBlock = (bool) $generateSheetNavigationBlock; 334 335 return $this; 336 } 337 338 /** 339 * Write all sheets (resets sheetIndex to NULL). 340 * 341 * @return $this 342 */ 343 public function writeAllSheets() 344 { 345 $this->sheetIndex = null; 346 347 return $this; 348 } 349 350 private static function generateMeta($val, $desc) 351 { 352 return $val 353 ? (' <meta name="' . $desc . '" content="' . htmlspecialchars($val, Settings::htmlEntityFlags()) . '" />' . PHP_EOL) 354 : ''; 355 } 356 357 public const BODY_LINE = ' <body>' . PHP_EOL; 358 359 /** 360 * Generate HTML header. 361 * 362 * @param bool $includeStyles Include styles? 363 * 364 * @return string 365 */ 366 public function generateHTMLHeader($includeStyles = false) 367 { 368 // Construct HTML 369 $properties = $this->spreadsheet->getProperties(); 370 $html = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">' . PHP_EOL; 371 $html .= '<html xmlns="http://www.w3.org/1999/xhtml">' . PHP_EOL; 372 $html .= ' <head>' . PHP_EOL; 373 $html .= ' <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />' . PHP_EOL; 374 $html .= ' <meta name="generator" content="PhpSpreadsheet, https://github.com/PHPOffice/PhpSpreadsheet" />' . PHP_EOL; 375 $html .= ' <title>' . htmlspecialchars($properties->getTitle(), Settings::htmlEntityFlags()) . '</title>' . PHP_EOL; 376 $html .= self::generateMeta($properties->getCreator(), 'author'); 377 $html .= self::generateMeta($properties->getTitle(), 'title'); 378 $html .= self::generateMeta($properties->getDescription(), 'description'); 379 $html .= self::generateMeta($properties->getSubject(), 'subject'); 380 $html .= self::generateMeta($properties->getKeywords(), 'keywords'); 381 $html .= self::generateMeta($properties->getCategory(), 'category'); 382 $html .= self::generateMeta($properties->getCompany(), 'company'); 383 $html .= self::generateMeta($properties->getManager(), 'manager'); 384 385 $html .= $includeStyles ? $this->generateStyles(true) : $this->generatePageDeclarations(true); 386 387 $html .= ' </head>' . PHP_EOL; 388 $html .= '' . PHP_EOL; 389 $html .= self::BODY_LINE; 390 391 return $html; 392 } 393 394 private function generateSheetPrep() 395 { 396 // Ensure that Spans have been calculated? 397 $this->calculateSpans(); 398 399 // Fetch sheets 400 if ($this->sheetIndex === null) { 401 $sheets = $this->spreadsheet->getAllSheets(); 402 } else { 403 $sheets = [$this->spreadsheet->getSheet($this->sheetIndex)]; 404 } 405 406 return $sheets; 407 } 408 409 private function generateSheetStarts($sheet, $rowMin) 410 { 411 // calculate start of <tbody>, <thead> 412 $tbodyStart = $rowMin; 413 $theadStart = $theadEnd = 0; // default: no <thead> no </thead> 414 if ($sheet->getPageSetup()->isRowsToRepeatAtTopSet()) { 415 $rowsToRepeatAtTop = $sheet->getPageSetup()->getRowsToRepeatAtTop(); 416 417 // we can only support repeating rows that start at top row 418 if ($rowsToRepeatAtTop[0] == 1) { 419 $theadStart = $rowsToRepeatAtTop[0]; 420 $theadEnd = $rowsToRepeatAtTop[1]; 421 $tbodyStart = $rowsToRepeatAtTop[1] + 1; 422 } 423 } 424 425 return [$theadStart, $theadEnd, $tbodyStart]; 426 } 427 428 private function generateSheetTags($row, $theadStart, $theadEnd, $tbodyStart) 429 { 430 // <thead> ? 431 $startTag = ($row == $theadStart) ? (' <thead>' . PHP_EOL) : ''; 432 if (!$startTag) { 433 $startTag = ($row == $tbodyStart) ? (' <tbody>' . PHP_EOL) : ''; 434 } 435 $endTag = ($row == $theadEnd) ? (' </thead>' . PHP_EOL) : ''; 436 $cellType = ($row >= $tbodyStart) ? 'td' : 'th'; 437 438 return [$cellType, $startTag, $endTag]; 439 } 440 441 /** 442 * Generate sheet data. 443 * 444 * @return string 445 */ 446 public function generateSheetData() 447 { 448 $sheets = $this->generateSheetPrep(); 449 450 // Construct HTML 451 $html = ''; 452 453 // Loop all sheets 454 $sheetId = 0; 455 foreach ($sheets as $sheet) { 456 // Write table header 457 $html .= $this->generateTableHeader($sheet); 458 459 // Get worksheet dimension 460 [$min, $max] = explode(':', $sheet->calculateWorksheetDataDimension()); 461 [$minCol, $minRow] = Coordinate::indexesFromString($min); 462 [$maxCol, $maxRow] = Coordinate::indexesFromString($max); 463 464 [$theadStart, $theadEnd, $tbodyStart] = $this->generateSheetStarts($sheet, $minRow); 465 466 // Loop through cells 467 $row = $minRow - 1; 468 while ($row++ < $maxRow) { 469 [$cellType, $startTag, $endTag] = $this->generateSheetTags($row, $theadStart, $theadEnd, $tbodyStart); 470 $html .= $startTag; 471 472 // Write row if there are HTML table cells in it 473 if (!isset($this->isSpannedRow[$sheet->getParent()->getIndex($sheet)][$row])) { 474 // Start a new rowData 475 $rowData = []; 476 // Loop through columns 477 $column = $minCol; 478 while ($column <= $maxCol) { 479 // Cell exists? 480 if ($sheet->cellExistsByColumnAndRow($column, $row)) { 481 $rowData[$column] = Coordinate::stringFromColumnIndex($column) . $row; 482 } else { 483 $rowData[$column] = ''; 484 } 485 ++$column; 486 } 487 $html .= $this->generateRow($sheet, $rowData, $row - 1, $cellType); 488 } 489 490 $html .= $endTag; 491 } 492 $html .= $this->extendRowsForChartsAndImages($sheet, $row); 493 494 // Write table footer 495 $html .= $this->generateTableFooter(); 496 // Writing PDF? 497 if ($this->isPdf && $this->useInlineCss) { 498 if ($this->sheetIndex === null && $sheetId + 1 < $this->spreadsheet->getSheetCount()) { 499 $html .= '<div style="page-break-before:always" ></div>'; 500 } 501 } 502 503 // Next sheet 504 ++$sheetId; 505 } 506 507 return $html; 508 } 509 510 /** 511 * Generate sheet tabs. 512 * 513 * @return string 514 */ 515 public function generateNavigation() 516 { 517 // Fetch sheets 518 $sheets = []; 519 if ($this->sheetIndex === null) { 520 $sheets = $this->spreadsheet->getAllSheets(); 521 } else { 522 $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex); 523 } 524 525 // Construct HTML 526 $html = ''; 527 528 // Only if there are more than 1 sheets 529 if (count($sheets) > 1) { 530 // Loop all sheets 531 $sheetId = 0; 532 533 $html .= '<ul class="navigation">' . PHP_EOL; 534 535 foreach ($sheets as $sheet) { 536 $html .= ' <li class="sheet' . $sheetId . '"><a href="#sheet' . $sheetId . '">' . $sheet->getTitle() . '</a></li>' . PHP_EOL; 537 ++$sheetId; 538 } 539 540 $html .= '</ul>' . PHP_EOL; 541 } 542 543 return $html; 544 } 545 546 /** 547 * Extend Row if chart is placed after nominal end of row. 548 * This code should be exercised by sample: 549 * Chart/32_Chart_read_write_PDF.php. 550 * However, that test is suppressed due to out-of-date 551 * Jpgraph code issuing warnings. So, don't measure 552 * code coverage for this function till that is fixed. 553 * 554 * @param int $row Row to check for charts 555 * 556 * @return array 557 * 558 * @codeCoverageIgnore 559 */ 560 private function extendRowsForCharts(Worksheet $worksheet, int $row) 561 { 562 $rowMax = $row; 563 $colMax = 'A'; 564 $anyfound = false; 565 if ($this->includeCharts) { 566 foreach ($worksheet->getChartCollection() as $chart) { 567 if ($chart instanceof Chart) { 568 $anyfound = true; 569 $chartCoordinates = $chart->getTopLeftPosition(); 570 $chartTL = Coordinate::coordinateFromString($chartCoordinates['cell']); 571 $chartCol = Coordinate::columnIndexFromString($chartTL[0]); 572 if ($chartTL[1] > $rowMax) { 573 $rowMax = $chartTL[1]; 574 if ($chartCol > Coordinate::columnIndexFromString($colMax)) { 575 $colMax = $chartTL[0]; 576 } 577 } 578 } 579 } 580 } 581 582 return [$rowMax, $colMax, $anyfound]; 583 } 584 585 private function extendRowsForChartsAndImages(Worksheet $worksheet, int $row): string 586 { 587 [$rowMax, $colMax, $anyfound] = $this->extendRowsForCharts($worksheet, $row); 588 589 foreach ($worksheet->getDrawingCollection() as $drawing) { 590 $anyfound = true; 591 $imageTL = Coordinate::coordinateFromString($drawing->getCoordinates()); 592 $imageCol = Coordinate::columnIndexFromString($imageTL[0]); 593 if ($imageTL[1] > $rowMax) { 594 $rowMax = $imageTL[1]; 595 if ($imageCol > Coordinate::columnIndexFromString($colMax)) { 596 $colMax = $imageTL[0]; 597 } 598 } 599 } 600 601 // Don't extend rows if not needed 602 if ($row === $rowMax || !$anyfound) { 603 return ''; 604 } 605 606 $html = ''; 607 ++$colMax; 608 ++$row; 609 while ($row <= $rowMax) { 610 $html .= '<tr>'; 611 for ($col = 'A'; $col != $colMax; ++$col) { 612 $htmlx = $this->writeImageInCell($worksheet, $col . $row); 613 $htmlx .= $this->includeCharts ? $this->writeChartInCell($worksheet, $col . $row) : ''; 614 if ($htmlx) { 615 $html .= "<td class='style0' style='position: relative;'>$htmlx</td>"; 616 } else { 617 $html .= "<td class='style0'></td>"; 618 } 619 } 620 ++$row; 621 $html .= '</tr>' . PHP_EOL; 622 } 623 624 return $html; 625 } 626 627 /** 628 * Convert Windows file name to file protocol URL. 629 * 630 * @param string $filename file name on local system 631 * 632 * @return string 633 */ 634 public static function winFileToUrl($filename) 635 { 636 // Windows filename 637 if (substr($filename, 1, 2) === ':\\') { 638 $filename = 'file:///' . str_replace('\\', '/', $filename); 639 } 640 641 return $filename; 642 } 643 644 /** 645 * Generate image tag in cell. 646 * 647 * @param Worksheet $worksheet \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet 648 * @param string $coordinates Cell coordinates 649 * 650 * @return string 651 */ 652 private function writeImageInCell(Worksheet $worksheet, $coordinates) 653 { 654 // Construct HTML 655 $html = ''; 656 657 // Write images 658 foreach ($worksheet->getDrawingCollection() as $drawing) { 659 if ($drawing->getCoordinates() != $coordinates) { 660 continue; 661 } 662 $filedesc = $drawing->getDescription(); 663 $filedesc = $filedesc ? htmlspecialchars($filedesc, ENT_QUOTES) : 'Embedded image'; 664 if ($drawing instanceof Drawing) { 665 $filename = $drawing->getPath(); 666 667 // Strip off eventual '.' 668 $filename = preg_replace('/^[.]/', '', $filename); 669 670 // Prepend images root 671 $filename = $this->getImagesRoot() . $filename; 672 673 // Strip off eventual '.' if followed by non-/ 674 $filename = preg_replace('@^[.]([^/])@', '$1', $filename); 675 676 // Convert UTF8 data to PCDATA 677 $filename = htmlspecialchars($filename, Settings::htmlEntityFlags()); 678 679 $html .= PHP_EOL; 680 $imageData = self::winFileToUrl($filename); 681 682 if ($this->embedImages && !$this->isPdf) { 683 $picture = @file_get_contents($filename); 684 if ($picture !== false) { 685 $imageDetails = getimagesize($filename); 686 // base64 encode the binary data 687 $base64 = base64_encode($picture); 688 $imageData = 'data:' . $imageDetails['mime'] . ';base64,' . $base64; 689 } 690 } 691 692 $html .= '<img style="position: absolute; z-index: 1; left: ' . 693 $drawing->getOffsetX() . 'px; top: ' . $drawing->getOffsetY() . 'px; width: ' . 694 $drawing->getWidth() . 'px; height: ' . $drawing->getHeight() . 'px;" src="' . 695 $imageData . '" alt="' . $filedesc . '" />'; 696 } elseif ($drawing instanceof MemoryDrawing) { 697 $imageResource = $drawing->getImageResource(); 698 if ($imageResource) { 699 ob_start(); // Let's start output buffering. 700 imagepng($imageResource); // This will normally output the image, but because of ob_start(), it won't. 701 $contents = ob_get_contents(); // Instead, output above is saved to $contents 702 ob_end_clean(); // End the output buffer. 703 704 $dataUri = 'data:image/jpeg;base64,' . base64_encode($contents); 705 706 // Because of the nature of tables, width is more important than height. 707 // max-width: 100% ensures that image doesnt overflow containing cell 708 // width: X sets width of supplied image. 709 // As a result, images bigger than cell will be contained and images smaller will not get stretched 710 $html .= '<img alt="' . $filedesc . '" src="' . $dataUri . '" style="max-width:100%;width:' . $drawing->getWidth() . 'px;" />'; 711 } 712 } 713 } 714 715 return $html; 716 } 717 718 /** 719 * Generate chart tag in cell. 720 * This code should be exercised by sample: 721 * Chart/32_Chart_read_write_PDF.php. 722 * However, that test is suppressed due to out-of-date 723 * Jpgraph code issuing warnings. So, don't measure 724 * code coverage for this function till that is fixed. 725 * 726 * @codeCoverageIgnore 727 */ 728 private function writeChartInCell(Worksheet $worksheet, string $coordinates): string 729 { 730 // Construct HTML 731 $html = ''; 732 733 // Write charts 734 foreach ($worksheet->getChartCollection() as $chart) { 735 if ($chart instanceof Chart) { 736 $chartCoordinates = $chart->getTopLeftPosition(); 737 if ($chartCoordinates['cell'] == $coordinates) { 738 $chartFileName = File::sysGetTempDir() . '/' . uniqid('', true) . '.png'; 739 if (!$chart->render($chartFileName)) { 740 return ''; 741 } 742 743 $html .= PHP_EOL; 744 $imageDetails = getimagesize($chartFileName); 745 $filedesc = $chart->getTitle(); 746 $filedesc = $filedesc ? $filedesc->getCaptionText() : ''; 747 $filedesc = $filedesc ? htmlspecialchars($filedesc, ENT_QUOTES) : 'Embedded chart'; 748 if ($fp = fopen($chartFileName, 'rb', 0)) { 749 $picture = fread($fp, filesize($chartFileName)); 750 fclose($fp); 751 // base64 encode the binary data 752 $base64 = base64_encode($picture); 753 $imageData = 'data:' . $imageDetails['mime'] . ';base64,' . $base64; 754 755 $html .= '<img style="position: absolute; z-index: 1; left: ' . $chartCoordinates['xOffset'] . 'px; top: ' . $chartCoordinates['yOffset'] . 'px; width: ' . $imageDetails[0] . 'px; height: ' . $imageDetails[1] . 'px;" src="' . $imageData . '" alt="' . $filedesc . '" />' . PHP_EOL; 756 757 unlink($chartFileName); 758 } 759 } 760 } 761 } 762 763 // Return 764 return $html; 765 } 766 767 /** 768 * Generate CSS styles. 769 * 770 * @param bool $generateSurroundingHTML Generate surrounding HTML tags? (<style> and </style>) 771 * 772 * @return string 773 */ 774 public function generateStyles($generateSurroundingHTML = true) 775 { 776 // Build CSS 777 $css = $this->buildCSS($generateSurroundingHTML); 778 779 // Construct HTML 780 $html = ''; 781 782 // Start styles 783 if ($generateSurroundingHTML) { 784 $html .= ' <style type="text/css">' . PHP_EOL; 785 $html .= (array_key_exists('html', $css)) ? (' html { ' . $this->assembleCSS($css['html']) . ' }' . PHP_EOL) : ''; 786 } 787 788 // Write all other styles 789 foreach ($css as $styleName => $styleDefinition) { 790 if ($styleName != 'html') { 791 $html .= ' ' . $styleName . ' { ' . $this->assembleCSS($styleDefinition) . ' }' . PHP_EOL; 792 } 793 } 794 $html .= $this->generatePageDeclarations(false); 795 796 // End styles 797 if ($generateSurroundingHTML) { 798 $html .= ' </style>' . PHP_EOL; 799 } 800 801 // Return 802 return $html; 803 } 804 805 private function buildCssRowHeights(Worksheet $sheet, array &$css, int $sheetIndex): void 806 { 807 // Calculate row heights 808 foreach ($sheet->getRowDimensions() as $rowDimension) { 809 $row = $rowDimension->getRowIndex() - 1; 810 811 // table.sheetN tr.rowYYYYYY { } 812 $css['table.sheet' . $sheetIndex . ' tr.row' . $row] = []; 813 814 if ($rowDimension->getRowHeight() != -1) { 815 $pt_height = $rowDimension->getRowHeight(); 816 $css['table.sheet' . $sheetIndex . ' tr.row' . $row]['height'] = $pt_height . 'pt'; 817 } 818 if ($rowDimension->getVisible() === false) { 819 $css['table.sheet' . $sheetIndex . ' tr.row' . $row]['display'] = 'none'; 820 $css['table.sheet' . $sheetIndex . ' tr.row' . $row]['visibility'] = 'hidden'; 821 } 822 } 823 } 824 825 private function buildCssPerSheet(Worksheet $sheet, array &$css): void 826 { 827 // Calculate hash code 828 $sheetIndex = $sheet->getParent()->getIndex($sheet); 829 830 // Build styles 831 // Calculate column widths 832 $sheet->calculateColumnWidths(); 833 834 // col elements, initialize 835 $highestColumnIndex = Coordinate::columnIndexFromString($sheet->getHighestColumn()) - 1; 836 $column = -1; 837 while ($column++ < $highestColumnIndex) { 838 $this->columnWidths[$sheetIndex][$column] = 42; // approximation 839 $css['table.sheet' . $sheetIndex . ' col.col' . $column]['width'] = '42pt'; 840 } 841 842 // col elements, loop through columnDimensions and set width 843 foreach ($sheet->getColumnDimensions() as $columnDimension) { 844 $column = Coordinate::columnIndexFromString($columnDimension->getColumnIndex()) - 1; 845 $width = SharedDrawing::cellDimensionToPixels($columnDimension->getWidth(), $this->defaultFont); 846 $width = SharedDrawing::pixelsToPoints($width); 847 if ($columnDimension->getVisible() === false) { 848 $css['table.sheet' . $sheetIndex . ' .column' . $column]['display'] = 'none'; 849 } 850 if ($width >= 0) { 851 $this->columnWidths[$sheetIndex][$column] = $width; 852 $css['table.sheet' . $sheetIndex . ' col.col' . $column]['width'] = $width . 'pt'; 853 } 854 } 855 856 // Default row height 857 $rowDimension = $sheet->getDefaultRowDimension(); 858 859 // table.sheetN tr { } 860 $css['table.sheet' . $sheetIndex . ' tr'] = []; 861 862 if ($rowDimension->getRowHeight() == -1) { 863 $pt_height = SharedFont::getDefaultRowHeightByFont($this->spreadsheet->getDefaultStyle()->getFont()); 864 } else { 865 $pt_height = $rowDimension->getRowHeight(); 866 } 867 $css['table.sheet' . $sheetIndex . ' tr']['height'] = $pt_height . 'pt'; 868 if ($rowDimension->getVisible() === false) { 869 $css['table.sheet' . $sheetIndex . ' tr']['display'] = 'none'; 870 $css['table.sheet' . $sheetIndex . ' tr']['visibility'] = 'hidden'; 871 } 872 873 $this->buildCssRowHeights($sheet, $css, $sheetIndex); 874 } 875 876 /** 877 * Build CSS styles. 878 * 879 * @param bool $generateSurroundingHTML Generate surrounding HTML style? (html { }) 880 * 881 * @return array 882 */ 883 public function buildCSS($generateSurroundingHTML = true) 884 { 885 // Cached? 886 if ($this->cssStyles !== null) { 887 return $this->cssStyles; 888 } 889 890 // Ensure that spans have been calculated 891 $this->calculateSpans(); 892 893 // Construct CSS 894 $css = []; 895 896 // Start styles 897 if ($generateSurroundingHTML) { 898 // html { } 899 $css['html']['font-family'] = 'Calibri, Arial, Helvetica, sans-serif'; 900 $css['html']['font-size'] = '11pt'; 901 $css['html']['background-color'] = 'white'; 902 } 903 904 // CSS for comments as found in LibreOffice 905 $css['a.comment-indicator:hover + div.comment'] = [ 906 'background' => '#ffd', 907 'position' => 'absolute', 908 'display' => 'block', 909 'border' => '1px solid black', 910 'padding' => '0.5em', 911 ]; 912 913 $css['a.comment-indicator'] = [ 914 'background' => 'red', 915 'display' => 'inline-block', 916 'border' => '1px solid black', 917 'width' => '0.5em', 918 'height' => '0.5em', 919 ]; 920 921 $css['div.comment']['display'] = 'none'; 922 923 // table { } 924 $css['table']['border-collapse'] = 'collapse'; 925 926 // .b {} 927 $css['.b']['text-align'] = 'center'; // BOOL 928 929 // .e {} 930 $css['.e']['text-align'] = 'center'; // ERROR 931 932 // .f {} 933 $css['.f']['text-align'] = 'right'; // FORMULA 934 935 // .inlineStr {} 936 $css['.inlineStr']['text-align'] = 'left'; // INLINE 937 938 // .n {} 939 $css['.n']['text-align'] = 'right'; // NUMERIC 940 941 // .s {} 942 $css['.s']['text-align'] = 'left'; // STRING 943 944 // Calculate cell style hashes 945 foreach ($this->spreadsheet->getCellXfCollection() as $index => $style) { 946 $css['td.style' . $index] = $this->createCSSStyle($style); 947 $css['th.style' . $index] = $this->createCSSStyle($style); 948 } 949 950 // Fetch sheets 951 $sheets = []; 952 if ($this->sheetIndex === null) { 953 $sheets = $this->spreadsheet->getAllSheets(); 954 } else { 955 $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex); 956 } 957 958 // Build styles per sheet 959 foreach ($sheets as $sheet) { 960 $this->buildCssPerSheet($sheet, $css); 961 } 962 963 // Cache 964 if ($this->cssStyles === null) { 965 $this->cssStyles = $css; 966 } 967 968 // Return 969 return $css; 970 } 971 972 /** 973 * Create CSS style. 974 * 975 * @return array 976 */ 977 private function createCSSStyle(Style $style) 978 { 979 // Create CSS 980 return array_merge( 981 $this->createCSSStyleAlignment($style->getAlignment()), 982 $this->createCSSStyleBorders($style->getBorders()), 983 $this->createCSSStyleFont($style->getFont()), 984 $this->createCSSStyleFill($style->getFill()) 985 ); 986 } 987 988 /** 989 * Create CSS style. 990 * 991 * @return array 992 */ 993 private function createCSSStyleAlignment(Alignment $alignment) 994 { 995 // Construct CSS 996 $css = []; 997 998 // Create CSS 999 $css['vertical-align'] = $this->mapVAlign($alignment->getVertical()); 1000 $textAlign = $this->mapHAlign($alignment->getHorizontal()); 1001 if ($textAlign) { 1002 $css['text-align'] = $textAlign; 1003 if (in_array($textAlign, ['left', 'right'])) { 1004 $css['padding-' . $textAlign] = (string) ((int) $alignment->getIndent() * 9) . 'px'; 1005 } 1006 } 1007 1008 return $css; 1009 } 1010 1011 /** 1012 * Create CSS style. 1013 * 1014 * @return array 1015 */ 1016 private function createCSSStyleFont(Font $font) 1017 { 1018 // Construct CSS 1019 $css = []; 1020 1021 // Create CSS 1022 if ($font->getBold()) { 1023 $css['font-weight'] = 'bold'; 1024 } 1025 if ($font->getUnderline() != Font::UNDERLINE_NONE && $font->getStrikethrough()) { 1026 $css['text-decoration'] = 'underline line-through'; 1027 } elseif ($font->getUnderline() != Font::UNDERLINE_NONE) { 1028 $css['text-decoration'] = 'underline'; 1029 } elseif ($font->getStrikethrough()) { 1030 $css['text-decoration'] = 'line-through'; 1031 } 1032 if ($font->getItalic()) { 1033 $css['font-style'] = 'italic'; 1034 } 1035 1036 $css['color'] = '#' . $font->getColor()->getRGB(); 1037 $css['font-family'] = '\'' . $font->getName() . '\''; 1038 $css['font-size'] = $font->getSize() . 'pt'; 1039 1040 return $css; 1041 } 1042 1043 /** 1044 * Create CSS style. 1045 * 1046 * @param Borders $borders Borders 1047 * 1048 * @return array 1049 */ 1050 private function createCSSStyleBorders(Borders $borders) 1051 { 1052 // Construct CSS 1053 $css = []; 1054 1055 // Create CSS 1056 $css['border-bottom'] = $this->createCSSStyleBorder($borders->getBottom()); 1057 $css['border-top'] = $this->createCSSStyleBorder($borders->getTop()); 1058 $css['border-left'] = $this->createCSSStyleBorder($borders->getLeft()); 1059 $css['border-right'] = $this->createCSSStyleBorder($borders->getRight()); 1060 1061 return $css; 1062 } 1063 1064 /** 1065 * Create CSS style. 1066 * 1067 * @param Border $border Border 1068 * 1069 * @return string 1070 */ 1071 private function createCSSStyleBorder(Border $border) 1072 { 1073 // Create CSS - add !important to non-none border styles for merged cells 1074 $borderStyle = $this->mapBorderStyle($border->getBorderStyle()); 1075 1076 return $borderStyle . ' #' . $border->getColor()->getRGB() . (($borderStyle == 'none') ? '' : ' !important'); 1077 } 1078 1079 /** 1080 * Create CSS style (Fill). 1081 * 1082 * @param Fill $fill Fill 1083 * 1084 * @return array 1085 */ 1086 private function createCSSStyleFill(Fill $fill) 1087 { 1088 // Construct HTML 1089 $css = []; 1090 1091 // Create CSS 1092 $value = $fill->getFillType() == Fill::FILL_NONE ? 1093 'white' : '#' . $fill->getStartColor()->getRGB(); 1094 $css['background-color'] = $value; 1095 1096 return $css; 1097 } 1098 1099 /** 1100 * Generate HTML footer. 1101 */ 1102 public function generateHTMLFooter() 1103 { 1104 // Construct HTML 1105 $html = ''; 1106 $html .= ' </body>' . PHP_EOL; 1107 $html .= '</html>' . PHP_EOL; 1108 1109 return $html; 1110 } 1111 1112 private function generateTableTagInline(Worksheet $worksheet, $id) 1113 { 1114 $style = isset($this->cssStyles['table']) ? 1115 $this->assembleCSS($this->cssStyles['table']) : ''; 1116 1117 $prntgrid = $worksheet->getPrintGridlines(); 1118 $viewgrid = $this->isPdf ? $prntgrid : $worksheet->getShowGridlines(); 1119 if ($viewgrid && $prntgrid) { 1120 $html = " <table border='1' cellpadding='1' $id cellspacing='1' style='$style' class='gridlines gridlinesp'>" . PHP_EOL; 1121 } elseif ($viewgrid) { 1122 $html = " <table border='0' cellpadding='0' $id cellspacing='0' style='$style' class='gridlines'>" . PHP_EOL; 1123 } elseif ($prntgrid) { 1124 $html = " <table border='0' cellpadding='0' $id cellspacing='0' style='$style' class='gridlinesp'>" . PHP_EOL; 1125 } else { 1126 $html = " <table border='0' cellpadding='1' $id cellspacing='0' style='$style'>" . PHP_EOL; 1127 } 1128 1129 return $html; 1130 } 1131 1132 private function generateTableTag(Worksheet $worksheet, $id, &$html, $sheetIndex): void 1133 { 1134 if (!$this->useInlineCss) { 1135 $gridlines = $worksheet->getShowGridlines() ? ' gridlines' : ''; 1136 $gridlinesp = $worksheet->getPrintGridlines() ? ' gridlinesp' : ''; 1137 $html .= " <table border='0' cellpadding='0' cellspacing='0' $id class='sheet$sheetIndex$gridlines$gridlinesp'>" . PHP_EOL; 1138 } else { 1139 $html .= $this->generateTableTagInline($worksheet, $id); 1140 } 1141 } 1142 1143 /** 1144 * Generate table header. 1145 * 1146 * @param Worksheet $worksheet The worksheet for the table we are writing 1147 * @param bool $showid whether or not to add id to table tag 1148 * 1149 * @return string 1150 */ 1151 private function generateTableHeader(Worksheet $worksheet, $showid = true) 1152 { 1153 $sheetIndex = $worksheet->getParent()->getIndex($worksheet); 1154 1155 // Construct HTML 1156 $html = ''; 1157 $id = $showid ? "id='sheet$sheetIndex'" : ''; 1158 if ($showid) { 1159 $html .= "<div style='page: page$sheetIndex'>\n"; 1160 } else { 1161 $html .= "<div style='page: page$sheetIndex' class='scrpgbrk'>\n"; 1162 } 1163 1164 $this->generateTableTag($worksheet, $id, $html, $sheetIndex); 1165 1166 // Write <col> elements 1167 $highestColumnIndex = Coordinate::columnIndexFromString($worksheet->getHighestColumn()) - 1; 1168 $i = -1; 1169 while ($i++ < $highestColumnIndex) { 1170 if (!$this->useInlineCss) { 1171 $html .= ' <col class="col' . $i . '" />' . PHP_EOL; 1172 } else { 1173 $style = isset($this->cssStyles['table.sheet' . $sheetIndex . ' col.col' . $i]) ? 1174 $this->assembleCSS($this->cssStyles['table.sheet' . $sheetIndex . ' col.col' . $i]) : ''; 1175 $html .= ' <col style="' . $style . '" />' . PHP_EOL; 1176 } 1177 } 1178 1179 return $html; 1180 } 1181 1182 /** 1183 * Generate table footer. 1184 */ 1185 private function generateTableFooter() 1186 { 1187 return ' </tbody></table>' . PHP_EOL . '</div>' . PHP_EOL; 1188 } 1189 1190 /** 1191 * Generate row start. 1192 * 1193 * @param int $sheetIndex Sheet index (0-based) 1194 * @param int $row row number 1195 * 1196 * @return string 1197 */ 1198 private function generateRowStart(Worksheet $worksheet, $sheetIndex, $row) 1199 { 1200 $html = ''; 1201 if (count($worksheet->getBreaks()) > 0) { 1202 $breaks = $worksheet->getBreaks(); 1203 1204 // check if a break is needed before this row 1205 if (isset($breaks['A' . $row])) { 1206 // close table: </table> 1207 $html .= $this->generateTableFooter(); 1208 if ($this->isPdf && $this->useInlineCss) { 1209 $html .= '<div style="page-break-before:always" />'; 1210 } 1211 1212 // open table again: <table> + <col> etc. 1213 $html .= $this->generateTableHeader($worksheet, false); 1214 $html .= '<tbody>' . PHP_EOL; 1215 } 1216 } 1217 1218 // Write row start 1219 if (!$this->useInlineCss) { 1220 $html .= ' <tr class="row' . $row . '">' . PHP_EOL; 1221 } else { 1222 $style = isset($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $row]) 1223 ? $this->assembleCSS($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $row]) : ''; 1224 1225 $html .= ' <tr style="' . $style . '">' . PHP_EOL; 1226 } 1227 1228 return $html; 1229 } 1230 1231 private function generateRowCellCss(Worksheet $worksheet, $cellAddress, $row, $columnNumber) 1232 { 1233 $cell = ($cellAddress > '') ? $worksheet->getCell($cellAddress) : ''; 1234 $coordinate = Coordinate::stringFromColumnIndex($columnNumber + 1) . ($row + 1); 1235 if (!$this->useInlineCss) { 1236 $cssClass = 'column' . $columnNumber; 1237 } else { 1238 $cssClass = []; 1239 // The statements below do nothing. 1240 // Commenting out the code rather than deleting it 1241 // in case someone can figure out what their intent was. 1242 //if ($cellType == 'th') { 1243 // if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' th.column' . $colNum])) { 1244 // $this->cssStyles['table.sheet' . $sheetIndex . ' th.column' . $colNum]; 1245 // } 1246 //} else { 1247 // if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' td.column' . $colNum])) { 1248 // $this->cssStyles['table.sheet' . $sheetIndex . ' td.column' . $colNum]; 1249 // } 1250 //} 1251 // End of mystery statements. 1252 } 1253 1254 return [$cell, $cssClass, $coordinate]; 1255 } 1256 1257 private function generateRowCellDataValueRich($cell, &$cellData): void 1258 { 1259 // Loop through rich text elements 1260 $elements = $cell->getValue()->getRichTextElements(); 1261 foreach ($elements as $element) { 1262 // Rich text start? 1263 if ($element instanceof Run) { 1264 $cellData .= '<span style="' . $this->assembleCSS($this->createCSSStyleFont($element->getFont())) . '">'; 1265 1266 $cellEnd = ''; 1267 if ($element->getFont()->getSuperscript()) { 1268 $cellData .= '<sup>'; 1269 $cellEnd = '</sup>'; 1270 } elseif ($element->getFont()->getSubscript()) { 1271 $cellData .= '<sub>'; 1272 $cellEnd = '</sub>'; 1273 } 1274 1275 // Convert UTF8 data to PCDATA 1276 $cellText = $element->getText(); 1277 $cellData .= htmlspecialchars($cellText, Settings::htmlEntityFlags()); 1278 1279 $cellData .= $cellEnd; 1280 1281 $cellData .= '</span>'; 1282 } else { 1283 // Convert UTF8 data to PCDATA 1284 $cellText = $element->getText(); 1285 $cellData .= htmlspecialchars($cellText, Settings::htmlEntityFlags()); 1286 } 1287 } 1288 } 1289 1290 private function generateRowCellDataValue(Worksheet $worksheet, $cell, &$cellData): void 1291 { 1292 if ($cell->getValue() instanceof RichText) { 1293 $this->generateRowCellDataValueRich($cell, $cellData); 1294 } else { 1295 $origData = $this->preCalculateFormulas ? $cell->getCalculatedValue() : $cell->getValue(); 1296 $formatCode = $worksheet->getParent()->getCellXfByIndex($cell->getXfIndex())->getNumberFormat()->getFormatCode(); 1297 if ($formatCode !== null) { 1298 $cellData = NumberFormat::toFormattedString( 1299 $origData, 1300 $formatCode, 1301 [$this, 'formatColor'] 1302 ); 1303 } 1304 1305 if ($cellData === $origData) { 1306 $cellData = htmlspecialchars($cellData ?? '', Settings::htmlEntityFlags()); 1307 } 1308 if ($worksheet->getParent()->getCellXfByIndex($cell->getXfIndex())->getFont()->getSuperscript()) { 1309 $cellData = '<sup>' . $cellData . '</sup>'; 1310 } elseif ($worksheet->getParent()->getCellXfByIndex($cell->getXfIndex())->getFont()->getSubscript()) { 1311 $cellData = '<sub>' . $cellData . '</sub>'; 1312 } 1313 } 1314 } 1315 1316 private function generateRowCellData(Worksheet $worksheet, $cell, &$cssClass, $cellType) 1317 { 1318 $cellData = ' '; 1319 if ($cell instanceof Cell) { 1320 $cellData = ''; 1321 // Don't know what this does, and no test cases. 1322 //if ($cell->getParent() === null) { 1323 // $cell->attach($worksheet); 1324 //} 1325 // Value 1326 $this->generateRowCellDataValue($worksheet, $cell, $cellData); 1327 1328 // Converts the cell content so that spaces occuring at beginning of each new line are replaced by 1329 // Example: " Hello\n to the world" is converted to " Hello\n to the world" 1330 $cellData = preg_replace('/(?m)(?:^|\\G) /', ' ', $cellData); 1331 1332 // convert newline "\n" to '<br>' 1333 $cellData = nl2br($cellData); 1334 1335 // Extend CSS class? 1336 if (!$this->useInlineCss) { 1337 $cssClass .= ' style' . $cell->getXfIndex(); 1338 $cssClass .= ' ' . $cell->getDataType(); 1339 } else { 1340 if ($cellType == 'th') { 1341 if (isset($this->cssStyles['th.style' . $cell->getXfIndex()])) { 1342 $cssClass = array_merge($cssClass, $this->cssStyles['th.style' . $cell->getXfIndex()]); 1343 } 1344 } else { 1345 if (isset($this->cssStyles['td.style' . $cell->getXfIndex()])) { 1346 $cssClass = array_merge($cssClass, $this->cssStyles['td.style' . $cell->getXfIndex()]); 1347 } 1348 } 1349 1350 // General horizontal alignment: Actual horizontal alignment depends on dataType 1351 $sharedStyle = $worksheet->getParent()->getCellXfByIndex($cell->getXfIndex()); 1352 if ( 1353 $sharedStyle->getAlignment()->getHorizontal() == Alignment::HORIZONTAL_GENERAL 1354 && isset($this->cssStyles['.' . $cell->getDataType()]['text-align']) 1355 ) { 1356 $cssClass['text-align'] = $this->cssStyles['.' . $cell->getDataType()]['text-align']; 1357 } 1358 } 1359 } else { 1360 // Use default borders for empty cell 1361 if (is_string($cssClass)) { 1362 $cssClass .= ' style0'; 1363 } 1364 } 1365 1366 return $cellData; 1367 } 1368 1369 private function generateRowIncludeCharts(Worksheet $worksheet, $coordinate) 1370 { 1371 return $this->includeCharts ? $this->writeChartInCell($worksheet, $coordinate) : ''; 1372 } 1373 1374 private function generateRowSpans($html, $rowSpan, $colSpan) 1375 { 1376 $html .= ($colSpan > 1) ? (' colspan="' . $colSpan . '"') : ''; 1377 $html .= ($rowSpan > 1) ? (' rowspan="' . $rowSpan . '"') : ''; 1378 1379 return $html; 1380 } 1381 1382 private function generateRowWriteCell(&$html, Worksheet $worksheet, $coordinate, $cellType, $cellData, $colSpan, $rowSpan, $cssClass, $colNum, $sheetIndex, $row): void 1383 { 1384 // Image? 1385 $htmlx = $this->writeImageInCell($worksheet, $coordinate); 1386 // Chart? 1387 $htmlx .= $this->generateRowIncludeCharts($worksheet, $coordinate); 1388 // Column start 1389 $html .= ' <' . $cellType; 1390 if (!$this->useInlineCss && !$this->isPdf) { 1391 $html .= ' class="' . $cssClass . '"'; 1392 if ($htmlx) { 1393 $html .= " style='position: relative;'"; 1394 } 1395 } else { 1396 //** Necessary redundant code for the sake of \PhpOffice\PhpSpreadsheet\Writer\Pdf ** 1397 // We must explicitly write the width of the <td> element because TCPDF 1398 // does not recognize e.g. <col style="width:42pt"> 1399 if ($this->useInlineCss) { 1400 $xcssClass = $cssClass; 1401 } else { 1402 $html .= ' class="' . $cssClass . '"'; 1403 $xcssClass = []; 1404 } 1405 $width = 0; 1406 $i = $colNum - 1; 1407 $e = $colNum + $colSpan - 1; 1408 while ($i++ < $e) { 1409 if (isset($this->columnWidths[$sheetIndex][$i])) { 1410 $width += $this->columnWidths[$sheetIndex][$i]; 1411 } 1412 } 1413 $xcssClass['width'] = $width . 'pt'; 1414 1415 // We must also explicitly write the height of the <td> element because TCPDF 1416 // does not recognize e.g. <tr style="height:50pt"> 1417 if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $row]['height'])) { 1418 $height = $this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $row]['height']; 1419 $xcssClass['height'] = $height; 1420 } 1421 //** end of redundant code ** 1422 1423 if ($htmlx) { 1424 $xcssClass['position'] = 'relative'; 1425 } 1426 $html .= ' style="' . $this->assembleCSS($xcssClass) . '"'; 1427 } 1428 $html = $this->generateRowSpans($html, $rowSpan, $colSpan); 1429 1430 $html .= '>'; 1431 $html .= $htmlx; 1432 1433 $html .= $this->writeComment($worksheet, $coordinate); 1434 1435 // Cell data 1436 $html .= $cellData; 1437 1438 // Column end 1439 $html .= '</' . $cellType . '>' . PHP_EOL; 1440 } 1441 1442 /** 1443 * Generate row. 1444 * 1445 * @param array $values Array containing cells in a row 1446 * @param int $row Row number (0-based) 1447 * @param string $cellType eg: 'td' 1448 * 1449 * @return string 1450 */ 1451 private function generateRow(Worksheet $worksheet, array $values, $row, $cellType) 1452 { 1453 // Sheet index 1454 $sheetIndex = $worksheet->getParent()->getIndex($worksheet); 1455 $html = $this->generateRowStart($worksheet, $sheetIndex, $row); 1456 1457 // Write cells 1458 $colNum = 0; 1459 foreach ($values as $cellAddress) { 1460 [$cell, $cssClass, $coordinate] = $this->generateRowCellCss($worksheet, $cellAddress, $row, $colNum); 1461 1462 $colSpan = 1; 1463 $rowSpan = 1; 1464 1465 // Cell Data 1466 $cellData = $this->generateRowCellData($worksheet, $cell, $cssClass, $cellType); 1467 1468 // Hyperlink? 1469 if ($worksheet->hyperlinkExists($coordinate) && !$worksheet->getHyperlink($coordinate)->isInternal()) { 1470 $cellData = '<a href="' . htmlspecialchars($worksheet->getHyperlink($coordinate)->getUrl(), Settings::htmlEntityFlags()) . '" title="' . htmlspecialchars($worksheet->getHyperlink($coordinate)->getTooltip(), Settings::htmlEntityFlags()) . '">' . $cellData . '</a>'; 1471 } 1472 1473 // Should the cell be written or is it swallowed by a rowspan or colspan? 1474 $writeCell = !(isset($this->isSpannedCell[$worksheet->getParent()->getIndex($worksheet)][$row + 1][$colNum]) 1475 && $this->isSpannedCell[$worksheet->getParent()->getIndex($worksheet)][$row + 1][$colNum]); 1476 1477 // Colspan and Rowspan 1478 $colspan = 1; 1479 $rowspan = 1; 1480 if (isset($this->isBaseCell[$worksheet->getParent()->getIndex($worksheet)][$row + 1][$colNum])) { 1481 $spans = $this->isBaseCell[$worksheet->getParent()->getIndex($worksheet)][$row + 1][$colNum]; 1482 $rowSpan = $spans['rowspan']; 1483 $colSpan = $spans['colspan']; 1484 1485 // Also apply style from last cell in merge to fix borders - 1486 // relies on !important for non-none border declarations in createCSSStyleBorder 1487 $endCellCoord = Coordinate::stringFromColumnIndex($colNum + $colSpan) . ($row + $rowSpan); 1488 if (!$this->useInlineCss) { 1489 $cssClass .= ' style' . $worksheet->getCell($endCellCoord)->getXfIndex(); 1490 } 1491 } 1492 1493 // Write 1494 if ($writeCell) { 1495 $this->generateRowWriteCell($html, $worksheet, $coordinate, $cellType, $cellData, $colSpan, $rowSpan, $cssClass, $colNum, $sheetIndex, $row); 1496 } 1497 1498 // Next column 1499 ++$colNum; 1500 } 1501 1502 // Write row end 1503 $html .= ' </tr>' . PHP_EOL; 1504 1505 // Return 1506 return $html; 1507 } 1508 1509 /** 1510 * Takes array where of CSS properties / values and converts to CSS string. 1511 * 1512 * @return string 1513 */ 1514 private function assembleCSS(array $values = []) 1515 { 1516 $pairs = []; 1517 foreach ($values as $property => $value) { 1518 $pairs[] = $property . ':' . $value; 1519 } 1520 $string = implode('; ', $pairs); 1521 1522 return $string; 1523 } 1524 1525 /** 1526 * Get images root. 1527 * 1528 * @return string 1529 */ 1530 public function getImagesRoot() 1531 { 1532 return $this->imagesRoot; 1533 } 1534 1535 /** 1536 * Set images root. 1537 * 1538 * @param string $imagesRoot 1539 * 1540 * @return $this 1541 */ 1542 public function setImagesRoot($imagesRoot) 1543 { 1544 $this->imagesRoot = $imagesRoot; 1545 1546 return $this; 1547 } 1548 1549 /** 1550 * Get embed images. 1551 * 1552 * @return bool 1553 */ 1554 public function getEmbedImages() 1555 { 1556 return $this->embedImages; 1557 } 1558 1559 /** 1560 * Set embed images. 1561 * 1562 * @param bool $embedImages 1563 * 1564 * @return $this 1565 */ 1566 public function setEmbedImages($embedImages) 1567 { 1568 $this->embedImages = $embedImages; 1569 1570 return $this; 1571 } 1572 1573 /** 1574 * Get use inline CSS? 1575 * 1576 * @return bool 1577 */ 1578 public function getUseInlineCss() 1579 { 1580 return $this->useInlineCss; 1581 } 1582 1583 /** 1584 * Set use inline CSS? 1585 * 1586 * @param bool $useInlineCss 1587 * 1588 * @return $this 1589 */ 1590 public function setUseInlineCss($useInlineCss) 1591 { 1592 $this->useInlineCss = $useInlineCss; 1593 1594 return $this; 1595 } 1596 1597 /** 1598 * Get use embedded CSS? 1599 * 1600 * @return bool 1601 * 1602 * @codeCoverageIgnore 1603 * 1604 * @deprecated no longer used 1605 */ 1606 public function getUseEmbeddedCSS() 1607 { 1608 return $this->useEmbeddedCSS; 1609 } 1610 1611 /** 1612 * Set use embedded CSS? 1613 * 1614 * @param bool $useEmbeddedCSS 1615 * 1616 * @return $this 1617 * 1618 * @codeCoverageIgnore 1619 * 1620 * @deprecated no longer used 1621 */ 1622 public function setUseEmbeddedCSS($useEmbeddedCSS) 1623 { 1624 $this->useEmbeddedCSS = $useEmbeddedCSS; 1625 1626 return $this; 1627 } 1628 1629 /** 1630 * Add color to formatted string as inline style. 1631 * 1632 * @param string $value Plain formatted value without color 1633 * @param string $format Format code 1634 * 1635 * @return string 1636 */ 1637 public function formatColor($value, $format) 1638 { 1639 // Color information, e.g. [Red] is always at the beginning 1640 $color = null; // initialize 1641 $matches = []; 1642 1643 $color_regex = '/^\\[[a-zA-Z]+\\]/'; 1644 if (preg_match($color_regex, $format, $matches)) { 1645 $color = str_replace(['[', ']'], '', $matches[0]); 1646 $color = strtolower($color); 1647 } 1648 1649 // convert to PCDATA 1650 $result = htmlspecialchars($value, Settings::htmlEntityFlags()); 1651 1652 // color span tag 1653 if ($color !== null) { 1654 $result = '<span style="color:' . $color . '">' . $result . '</span>'; 1655 } 1656 1657 return $result; 1658 } 1659 1660 /** 1661 * Calculate information about HTML colspan and rowspan which is not always the same as Excel's. 1662 */ 1663 private function calculateSpans(): void 1664 { 1665 if ($this->spansAreCalculated) { 1666 return; 1667 } 1668 // Identify all cells that should be omitted in HTML due to cell merge. 1669 // In HTML only the upper-left cell should be written and it should have 1670 // appropriate rowspan / colspan attribute 1671 $sheetIndexes = $this->sheetIndex !== null ? 1672 [$this->sheetIndex] : range(0, $this->spreadsheet->getSheetCount() - 1); 1673 1674 foreach ($sheetIndexes as $sheetIndex) { 1675 $sheet = $this->spreadsheet->getSheet($sheetIndex); 1676 1677 $candidateSpannedRow = []; 1678 1679 // loop through all Excel merged cells 1680 foreach ($sheet->getMergeCells() as $cells) { 1681 [$cells] = Coordinate::splitRange($cells); 1682 $first = $cells[0]; 1683 $last = $cells[1]; 1684 1685 [$fc, $fr] = Coordinate::indexesFromString($first); 1686 $fc = $fc - 1; 1687 1688 [$lc, $lr] = Coordinate::indexesFromString($last); 1689 $lc = $lc - 1; 1690 1691 // loop through the individual cells in the individual merge 1692 $r = $fr - 1; 1693 while ($r++ < $lr) { 1694 // also, flag this row as a HTML row that is candidate to be omitted 1695 $candidateSpannedRow[$r] = $r; 1696 1697 $c = $fc - 1; 1698 while ($c++ < $lc) { 1699 if (!($c == $fc && $r == $fr)) { 1700 // not the upper-left cell (should not be written in HTML) 1701 $this->isSpannedCell[$sheetIndex][$r][$c] = [ 1702 'baseCell' => [$fr, $fc], 1703 ]; 1704 } else { 1705 // upper-left is the base cell that should hold the colspan/rowspan attribute 1706 $this->isBaseCell[$sheetIndex][$r][$c] = [ 1707 'xlrowspan' => $lr - $fr + 1, // Excel rowspan 1708 'rowspan' => $lr - $fr + 1, // HTML rowspan, value may change 1709 'xlcolspan' => $lc - $fc + 1, // Excel colspan 1710 'colspan' => $lc - $fc + 1, // HTML colspan, value may change 1711 ]; 1712 } 1713 } 1714 } 1715 } 1716 1717 $this->calculateSpansOmitRows($sheet, $sheetIndex, $candidateSpannedRow); 1718 1719 // TODO: Same for columns 1720 } 1721 1722 // We have calculated the spans 1723 $this->spansAreCalculated = true; 1724 } 1725 1726 private function calculateSpansOmitRows($sheet, $sheetIndex, $candidateSpannedRow): void 1727 { 1728 // Identify which rows should be omitted in HTML. These are the rows where all the cells 1729 // participate in a merge and the where base cells are somewhere above. 1730 $countColumns = Coordinate::columnIndexFromString($sheet->getHighestColumn()); 1731 foreach ($candidateSpannedRow as $rowIndex) { 1732 if (isset($this->isSpannedCell[$sheetIndex][$rowIndex])) { 1733 if (count($this->isSpannedCell[$sheetIndex][$rowIndex]) == $countColumns) { 1734 $this->isSpannedRow[$sheetIndex][$rowIndex] = $rowIndex; 1735 } 1736 } 1737 } 1738 1739 // For each of the omitted rows we found above, the affected rowspans should be subtracted by 1 1740 if (isset($this->isSpannedRow[$sheetIndex])) { 1741 foreach ($this->isSpannedRow[$sheetIndex] as $rowIndex) { 1742 $adjustedBaseCells = []; 1743 $c = -1; 1744 $e = $countColumns - 1; 1745 while ($c++ < $e) { 1746 $baseCell = $this->isSpannedCell[$sheetIndex][$rowIndex][$c]['baseCell']; 1747 1748 if (!in_array($baseCell, $adjustedBaseCells)) { 1749 // subtract rowspan by 1 1750 --$this->isBaseCell[$sheetIndex][$baseCell[0]][$baseCell[1]]['rowspan']; 1751 $adjustedBaseCells[] = $baseCell; 1752 } 1753 } 1754 } 1755 } 1756 } 1757 1758 /** 1759 * Write a comment in the same format as LibreOffice. 1760 * 1761 * @see https://github.com/LibreOffice/core/blob/9fc9bf3240f8c62ad7859947ab8a033ac1fe93fa/sc/source/filter/html/htmlexp.cxx#L1073-L1092 1762 * 1763 * @param string $coordinate 1764 * 1765 * @return string 1766 */ 1767 private function writeComment(Worksheet $worksheet, $coordinate) 1768 { 1769 $result = ''; 1770 if (!$this->isPdf && isset($worksheet->getComments()[$coordinate])) { 1771 $sanitizer = new HTMLPurifier(); 1772 $cachePath = File::sysGetTempDir() . '/phpsppur'; 1773 if (is_dir($cachePath) || mkdir($cachePath)) { 1774 $sanitizer->config->set('Cache.SerializerPath', $cachePath); 1775 } 1776 $sanitizedString = $sanitizer->purify($worksheet->getComment($coordinate)->getText()->getPlainText()); 1777 if ($sanitizedString !== '') { 1778 $result .= '<a class="comment-indicator"></a>'; 1779 $result .= '<div class="comment">' . nl2br($sanitizedString) . '</div>'; 1780 $result .= PHP_EOL; 1781 } 1782 } 1783 1784 return $result; 1785 } 1786 1787 public function getOrientation(): ?string 1788 { 1789 return null; 1790 } 1791 1792 /** 1793 * Generate @page declarations. 1794 * 1795 * @param bool $generateSurroundingHTML 1796 * 1797 * @return string 1798 */ 1799 private function generatePageDeclarations($generateSurroundingHTML) 1800 { 1801 // Ensure that Spans have been calculated? 1802 $this->calculateSpans(); 1803 1804 // Fetch sheets 1805 $sheets = []; 1806 if ($this->sheetIndex === null) { 1807 $sheets = $this->spreadsheet->getAllSheets(); 1808 } else { 1809 $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex); 1810 } 1811 1812 // Construct HTML 1813 $htmlPage = $generateSurroundingHTML ? ('<style type="text/css">' . PHP_EOL) : ''; 1814 1815 // Loop all sheets 1816 $sheetId = 0; 1817 foreach ($sheets as $worksheet) { 1818 $htmlPage .= "@page page$sheetId { "; 1819 $left = StringHelper::formatNumber($worksheet->getPageMargins()->getLeft()) . 'in; '; 1820 $htmlPage .= 'margin-left: ' . $left; 1821 $right = StringHelper::FormatNumber($worksheet->getPageMargins()->getRight()) . 'in; '; 1822 $htmlPage .= 'margin-right: ' . $right; 1823 $top = StringHelper::FormatNumber($worksheet->getPageMargins()->getTop()) . 'in; '; 1824 $htmlPage .= 'margin-top: ' . $top; 1825 $bottom = StringHelper::FormatNumber($worksheet->getPageMargins()->getBottom()) . 'in; '; 1826 $htmlPage .= 'margin-bottom: ' . $bottom; 1827 $orientation = $this->getOrientation() ?? $worksheet->getPageSetup()->getOrientation(); 1828 if ($orientation === \PhpOffice\PhpSpreadsheet\Worksheet\PageSetup::ORIENTATION_LANDSCAPE) { 1829 $htmlPage .= 'size: landscape; '; 1830 } elseif ($orientation === \PhpOffice\PhpSpreadsheet\Worksheet\PageSetup::ORIENTATION_PORTRAIT) { 1831 $htmlPage .= 'size: portrait; '; 1832 } 1833 $htmlPage .= "}\n"; 1834 ++$sheetId; 1835 } 1836 $htmlPage .= <<<EOF 1837 .navigation {page-break-after: always;} 1838 .scrpgbrk, div + div {page-break-before: always;} 1839 @media screen { 1840 .gridlines td {border: 1px solid black;} 1841 .gridlines th {border: 1px solid black;} 1842 body>div {margin-top: 5px;} 1843 body>div:first-child {margin-top: 0;} 1844 .scrpgbrk {margin-top: 1px;} 1845 } 1846 @media print { 1847 .gridlinesp td {border: 1px solid black;} 1848 .gridlinesp th {border: 1px solid black;} 1849 .navigation {display: none;} 1850 } 1851 1852 EOF; 1853 $htmlPage .= $generateSurroundingHTML ? ('</style>' . PHP_EOL) : ''; 1854 1855 return $htmlPage; 1856 } 1857 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body