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