PluginProbe
wpDataTables – WordPress Data Table, Dynamic Tables & Table Charts Plugin / 6.5.1.7
wpDataTables – WordPress Data Table, Dynamic Tables & Table Charts Plugin v6.5.1.7
6.5.1.7 6.5.1.6 6.5.1.5 6.5.1.4 6.5.1.3 6.5.1.2 6.5.1.1 6.5.0.9 6.5.0.8 6.5.0.7 6.5.0.6 trunk 3.4.2.40 3.4.2.41 3.4.2.42 3.4.2.43 3.4.2.44 3.4.2.45 3.4.2.46 3.4.2.47 3.4.2.48 3.4.2.49 3.4.2.50 6.3.2 6.3.3.1 All 47 releases
wpdatatables / lib / phpoffice / phpspreadsheet / src / PhpSpreadsheet / Writer / Html.php

Html.php in wpDataTables – WordPress Data Table, Dynamic Tables & Table Charts Plugin 6.5.1.7, at lib/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Html.php

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