PluginProbe
Visualizer – Tables & Charts Manager with Built-in AI Generator / 3.5.0
Visualizer – Tables & Charts Manager with Built-in AI Generator v3.5.0
4.0.8 4.0.7 4.0.6 4.0.5 4.0.4 4.0.3 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.1 3.1.2 3.1.3 3.10.0 3.10.1 3.10.10 3.10.11 3.10.12 3.10.13 3.10.14 3.10.15 3.10.2 3.10.3 All 149 releases
visualizer / vendor / phpoffice / phpspreadsheet / src / PhpSpreadsheet / Writer / Html.php

Html.php in Visualizer – Tables & Charts Manager with Built-in AI Generator 3.5.0, at vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Html.php

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