Passed
Push — master ( 8a1cb0...dbc877 )
by Adrien
27:24 queued 19:37
created

Html::generateHTMLHeader()   F

Complexity

Conditions 11
Paths 1024

Size

Total Lines 46
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 30
CRAP Score 11.004

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 11
eloc 30
c 1
b 0
f 0
nc 1024
nop 1
dl 0
loc 46
ccs 30
cts 31
cp 0.9677
crap 11.004
rs 3.15

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Writer;
4
5
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
6
use PhpOffice\PhpSpreadsheet\Cell\Cell;
7
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
8
use PhpOffice\PhpSpreadsheet\Chart\Chart;
9
use PhpOffice\PhpSpreadsheet\RichText\RichText;
10
use PhpOffice\PhpSpreadsheet\RichText\Run;
11
use PhpOffice\PhpSpreadsheet\Shared\Drawing as SharedDrawing;
12
use PhpOffice\PhpSpreadsheet\Shared\File;
13
use PhpOffice\PhpSpreadsheet\Shared\Font as SharedFont;
14
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
15
use PhpOffice\PhpSpreadsheet\Spreadsheet;
16
use PhpOffice\PhpSpreadsheet\Style\Alignment;
17
use PhpOffice\PhpSpreadsheet\Style\Border;
18
use PhpOffice\PhpSpreadsheet\Style\Borders;
19
use PhpOffice\PhpSpreadsheet\Style\Fill;
20
use PhpOffice\PhpSpreadsheet\Style\Font;
21
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
22
use PhpOffice\PhpSpreadsheet\Style\Style;
23
use PhpOffice\PhpSpreadsheet\Worksheet\Drawing;
24
use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing;
25
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
26
use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException;
27
28
class Html extends BaseWriter
29
{
30
    /**
31
     * Spreadsheet object.
32
     *
33
     * @var Spreadsheet
34
     */
35
    protected $spreadsheet;
36
37
    /**
38
     * Sheet index to write.
39
     *
40
     * @var int
41
     */
42
    private $sheetIndex = 0;
43
44
    /**
45
     * Images root.
46
     *
47
     * @var string
48
     */
49
    private $imagesRoot = '';
50
51
    /**
52
     * embed images, or link to images.
53
     *
54
     * @var bool
55
     */
56
    private $embedImages = false;
57
58
    /**
59
     * Use inline CSS?
60
     *
61
     * @var bool
62
     */
63
    private $useInlineCss = false;
64
65
    /**
66
     * Array of CSS styles.
67
     *
68
     * @var array
69
     */
70
    private $cssStyles;
71
72
    /**
73
     * Array of column widths in points.
74
     *
75
     * @var array
76
     */
77
    private $columnWidths;
78
79
    /**
80
     * Default font.
81
     *
82
     * @var Font
83
     */
84
    private $defaultFont;
85
86
    /**
87
     * Flag whether spans have been calculated.
88
     *
89
     * @var bool
90
     */
91
    private $spansAreCalculated = false;
92
93
    /**
94
     * Excel cells that should not be written as HTML cells.
95
     *
96
     * @var array
97
     */
98
    private $isSpannedCell = [];
99
100
    /**
101
     * Excel cells that are upper-left corner in a cell merge.
102
     *
103
     * @var array
104
     */
105
    private $isBaseCell = [];
106
107
    /**
108
     * Excel rows that should not be written as HTML rows.
109
     *
110
     * @var array
111
     */
112
    private $isSpannedRow = [];
113
114
    /**
115
     * Is the current writer creating PDF?
116
     *
117
     * @var bool
118
     */
119
    protected $isPdf = false;
120
121
    /**
122
     * Generate the Navigation block.
123
     *
124
     * @var bool
125
     */
126
    private $generateSheetNavigationBlock = true;
127
128
    /**
129
     * Create a new HTML.
130
     *
131
     * @param Spreadsheet $spreadsheet
132
     */
133 18
    public function __construct(Spreadsheet $spreadsheet)
134
    {
135 18
        $this->spreadsheet = $spreadsheet;
136 18
        $this->defaultFont = $this->spreadsheet->getDefaultStyle()->getFont();
137 18
    }
138
139
    /**
140
     * Save Spreadsheet to file.
141
     *
142
     * @param string $pFilename
143
     *
144
     * @throws WriterException
145
     */
146 11
    public function save($pFilename)
147
    {
148
        // garbage collect
149 11
        $this->spreadsheet->garbageCollect();
150
151 11
        $saveDebugLog = Calculation::getInstance($this->spreadsheet)->getDebugLog()->getWriteDebugLog();
152 11
        Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog(false);
153 11
        $saveArrayReturnType = Calculation::getArrayReturnType();
154 11
        Calculation::setArrayReturnType(Calculation::RETURN_ARRAY_AS_VALUE);
155
156
        // Build CSS
157 11
        $this->buildCSS(!$this->useInlineCss);
158
159
        // Open file
160 11
        $fileHandle = fopen($pFilename, 'wb+');
161 11
        if ($fileHandle === false) {
162
            throw new WriterException("Could not open file $pFilename for writing.");
163
        }
164
165
        // Write headers
166 11
        fwrite($fileHandle, $this->generateHTMLHeader(!$this->useInlineCss));
167
168
        // Write navigation (tabs)
169 11
        if ((!$this->isPdf) && ($this->generateSheetNavigationBlock)) {
170 11
            fwrite($fileHandle, $this->generateNavigation());
171
        }
172
173
        // Write data
174 11
        fwrite($fileHandle, $this->generateSheetData());
175
176
        // Write footer
177 11
        fwrite($fileHandle, $this->generateHTMLFooter());
178
179
        // Close file
180 11
        fclose($fileHandle);
181
182 11
        Calculation::setArrayReturnType($saveArrayReturnType);
183 11
        Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog($saveDebugLog);
184 11
    }
185
186
    /**
187
     * Map VAlign.
188
     *
189
     * @param string $vAlign Vertical alignment
190
     *
191
     * @return string
192
     */
193 13
    private function mapVAlign($vAlign)
194
    {
195 13
        switch ($vAlign) {
196
            case Alignment::VERTICAL_BOTTOM:
197 13
                return 'bottom';
198
            case Alignment::VERTICAL_TOP:
199
                return 'top';
200
            case Alignment::VERTICAL_CENTER:
201
            case Alignment::VERTICAL_JUSTIFY:
202 3
                return 'middle';
203
            default:
204
                return 'baseline';
205
        }
206
    }
207
208
    /**
209
     * Map HAlign.
210
     *
211
     * @param string $hAlign Horizontal alignment
212
     *
213
     * @return false|string
214
     */
215 13
    private function mapHAlign($hAlign)
216
    {
217 13
        switch ($hAlign) {
218
            case Alignment::HORIZONTAL_GENERAL:
219 13
                return false;
220
            case Alignment::HORIZONTAL_LEFT:
221 3
                return 'left';
222
            case Alignment::HORIZONTAL_RIGHT:
223 3
                return 'right';
224
            case Alignment::HORIZONTAL_CENTER:
225
            case Alignment::HORIZONTAL_CENTER_CONTINUOUS:
226 1
                return 'center';
227
            case Alignment::HORIZONTAL_JUSTIFY:
228 3
                return 'justify';
229
            default:
230
                return false;
231
        }
232
    }
233
234
    /**
235
     * Map border style.
236
     *
237
     * @param int $borderStyle Sheet index
238
     *
239
     * @return string
240
     */
241 13
    private function mapBorderStyle($borderStyle)
242
    {
243 13
        switch ($borderStyle) {
244
            case Border::BORDER_NONE:
245 13
                return 'none';
246
            case Border::BORDER_DASHDOT:
247
                return '1px dashed';
248
            case Border::BORDER_DASHDOTDOT:
249
                return '1px dotted';
250
            case Border::BORDER_DASHED:
251
                return '1px dashed';
252
            case Border::BORDER_DOTTED:
253
                return '1px dotted';
254
            case Border::BORDER_DOUBLE:
255
                return '3px double';
256
            case Border::BORDER_HAIR:
257
                return '1px solid';
258
            case Border::BORDER_MEDIUM:
259
                return '2px solid';
260
            case Border::BORDER_MEDIUMDASHDOT:
261
                return '2px dashed';
262
            case Border::BORDER_MEDIUMDASHDOTDOT:
263
                return '2px dotted';
264
            case Border::BORDER_MEDIUMDASHED:
265
                return '2px dashed';
266
            case Border::BORDER_SLANTDASHDOT:
267
                return '2px dashed';
268
            case Border::BORDER_THICK:
269 3
                return '3px solid';
270
            case Border::BORDER_THIN:
271 3
                return '1px solid';
272
            default:
273
                // map others to thin
274
                return '1px solid';
275
        }
276
    }
277
278
    /**
279
     * Get sheet index.
280
     *
281
     * @return int
282
     */
283 3
    public function getSheetIndex()
284
    {
285 3
        return $this->sheetIndex;
286
    }
287
288
    /**
289
     * Set sheet index.
290
     *
291
     * @param int $pValue Sheet index
292
     *
293
     * @return HTML
294
     */
295
    public function setSheetIndex($pValue)
296
    {
297
        $this->sheetIndex = $pValue;
298
299
        return $this;
300
    }
301
302
    /**
303
     * Get sheet index.
304
     *
305
     * @return bool
306
     */
307
    public function getGenerateSheetNavigationBlock()
308
    {
309
        return $this->generateSheetNavigationBlock;
310
    }
311
312
    /**
313
     * Set sheet index.
314
     *
315
     * @param bool $pValue Flag indicating whether the sheet navigation block should be generated or not
316
     *
317
     * @return HTML
318
     */
319
    public function setGenerateSheetNavigationBlock($pValue)
320
    {
321
        $this->generateSheetNavigationBlock = (bool) $pValue;
322
323
        return $this;
324
    }
325
326
    /**
327
     * Write all sheets (resets sheetIndex to NULL).
328
     */
329
    public function writeAllSheets()
330
    {
331
        $this->sheetIndex = null;
332
333
        return $this;
334
    }
335
336
    /**
337
     * Generate HTML header.
338
     *
339
     * @param bool $pIncludeStyles Include styles?
340
     *
341
     * @throws WriterException
342
     *
343
     * @return string
344
     */
345 13
    public function generateHTMLHeader($pIncludeStyles = false)
346
    {
347
        // Construct HTML
348 13
        $properties = $this->spreadsheet->getProperties();
349 13
        $html = '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">' . PHP_EOL;
350 13
        $html .= '<html>' . PHP_EOL;
351 13
        $html .= '  <head>' . PHP_EOL;
352 13
        $html .= '      <meta http-equiv="Content-Type" content="text/html; charset=utf-8">' . PHP_EOL;
353 13
        $html .= '      <meta name="generator" content="PhpSpreadsheet, https://github.com/PHPOffice/PhpSpreadsheet">' . PHP_EOL;
354 13
        if ($properties->getTitle() > '') {
355 13
            $html .= '      <title>' . htmlspecialchars($properties->getTitle()) . '</title>' . PHP_EOL;
356
        }
357 13
        if ($properties->getCreator() > '') {
358 13
            $html .= '      <meta name="author" content="' . htmlspecialchars($properties->getCreator()) . '" />' . PHP_EOL;
359
        }
360 13
        if ($properties->getTitle() > '') {
361 13
            $html .= '      <meta name="title" content="' . htmlspecialchars($properties->getTitle()) . '" />' . PHP_EOL;
362
        }
363 13
        if ($properties->getDescription() > '') {
364 4
            $html .= '      <meta name="description" content="' . htmlspecialchars($properties->getDescription()) . '" />' . PHP_EOL;
365
        }
366 13
        if ($properties->getSubject() > '') {
367 5
            $html .= '      <meta name="subject" content="' . htmlspecialchars($properties->getSubject()) . '" />' . PHP_EOL;
368
        }
369 13
        if ($properties->getKeywords() > '') {
370 5
            $html .= '      <meta name="keywords" content="' . htmlspecialchars($properties->getKeywords()) . '" />' . PHP_EOL;
371
        }
372 13
        if ($properties->getCategory() > '') {
373 5
            $html .= '      <meta name="category" content="' . htmlspecialchars($properties->getCategory()) . '" />' . PHP_EOL;
374
        }
375 13
        if ($properties->getCompany() > '') {
376 13
            $html .= '      <meta name="company" content="' . htmlspecialchars($properties->getCompany()) . '" />' . PHP_EOL;
377
        }
378 13
        if ($properties->getManager() > '') {
379
            $html .= '      <meta name="manager" content="' . htmlspecialchars($properties->getManager()) . '" />' . PHP_EOL;
380
        }
381
382 13
        if ($pIncludeStyles) {
383 11
            $html .= $this->generateStyles(true);
384
        }
385
386 13
        $html .= '  </head>' . PHP_EOL;
387 13
        $html .= '' . PHP_EOL;
388 13
        $html .= '  <body>' . PHP_EOL;
389
390 13
        return $html;
391
    }
392
393
    /**
394
     * Generate sheet data.
395
     *
396
     * @throws WriterException
397
     *
398
     * @return string
399
     */
400 13
    public function generateSheetData()
401
    {
402
        // Ensure that Spans have been calculated?
403 13
        if ($this->sheetIndex !== null || !$this->spansAreCalculated) {
404 13
            $this->calculateSpans();
405
        }
406
407
        // Fetch sheets
408 13
        $sheets = [];
409 13
        if ($this->sheetIndex === null) {
410
            $sheets = $this->spreadsheet->getAllSheets();
411
        } else {
412 13
            $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex);
413
        }
414
415
        // Construct HTML
416 13
        $html = '';
417
418
        // Loop all sheets
419 13
        $sheetId = 0;
420 13
        foreach ($sheets as $sheet) {
421
            // Write table header
422 13
            $html .= $this->generateTableHeader($sheet);
423
424
            // Get worksheet dimension
425 13
            $dimension = explode(':', $sheet->calculateWorksheetDimension());
426 13
            $dimension[0] = Coordinate::coordinateFromString($dimension[0]);
427 13
            $dimension[0][0] = Coordinate::columnIndexFromString($dimension[0][0]);
428 13
            $dimension[1] = Coordinate::coordinateFromString($dimension[1]);
429 13
            $dimension[1][0] = Coordinate::columnIndexFromString($dimension[1][0]);
430
431
            // row min,max
432 13
            $rowMin = $dimension[0][1];
433 13
            $rowMax = $dimension[1][1];
434
435
            // calculate start of <tbody>, <thead>
436 13
            $tbodyStart = $rowMin;
437 13
            $theadStart = $theadEnd = 0; // default: no <thead>    no </thead>
438 13
            if ($sheet->getPageSetup()->isRowsToRepeatAtTopSet()) {
439
                $rowsToRepeatAtTop = $sheet->getPageSetup()->getRowsToRepeatAtTop();
440
441
                // we can only support repeating rows that start at top row
442
                if ($rowsToRepeatAtTop[0] == 1) {
443
                    $theadStart = $rowsToRepeatAtTop[0];
444
                    $theadEnd = $rowsToRepeatAtTop[1];
445
                    $tbodyStart = $rowsToRepeatAtTop[1] + 1;
446
                }
447
            }
448
449
            // Loop through cells
450 13
            $row = $rowMin - 1;
451 13
            while ($row++ < $rowMax) {
452
                // <thead> ?
453 13
                if ($row == $theadStart) {
454
                    $html .= '        <thead>' . PHP_EOL;
455
                    $cellType = 'th';
456
                }
457
458
                // <tbody> ?
459 13
                if ($row == $tbodyStart) {
460 13
                    $html .= '        <tbody>' . PHP_EOL;
461 13
                    $cellType = 'td';
462
                }
463
464
                // Write row if there are HTML table cells in it
465 13
                if (!isset($this->isSpannedRow[$sheet->getParent()->getIndex($sheet)][$row])) {
466
                    // Start a new rowData
467 13
                    $rowData = [];
468
                    // Loop through columns
469 13
                    $column = $dimension[0][0];
470 13
                    while ($column <= $dimension[1][0]) {
471
                        // Cell exists?
472 13
                        if ($sheet->cellExistsByColumnAndRow($column, $row)) {
473 13
                            $rowData[$column] = Coordinate::stringFromColumnIndex($column) . $row;
474
                        } else {
475 5
                            $rowData[$column] = '';
476
                        }
477 13
                        ++$column;
478
                    }
479 13
                    $html .= $this->generateRow($sheet, $rowData, $row - 1, $cellType);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $cellType does not seem to be defined for all execution paths leading up to this point.
Loading history...
480
                }
481
482
                // </thead> ?
483 13
                if ($row == $theadEnd) {
484
                    $html .= '        </thead>' . PHP_EOL;
485
                }
486
            }
487 13
            $html .= $this->extendRowsForChartsAndImages($sheet, $row);
488
489
            // Close table body.
490 13
            $html .= '        </tbody>' . PHP_EOL;
491
492
            // Write table footer
493 13
            $html .= $this->generateTableFooter();
494
495
            // Writing PDF?
496 13
            if ($this->isPdf) {
497 3
                if ($this->sheetIndex === null && $sheetId + 1 < $this->spreadsheet->getSheetCount()) {
498
                    $html .= '<div style="page-break-before:always" />';
499
                }
500
            }
501
502
            // Next sheet
503 13
            ++$sheetId;
504
        }
505
506 13
        return $html;
507
    }
508
509
    /**
510
     * Generate sheet tabs.
511
     *
512
     * @throws WriterException
513
     *
514
     * @return string
515
     */
516 11
    public function generateNavigation()
517
    {
518
        // Fetch sheets
519 11
        $sheets = [];
520 11
        if ($this->sheetIndex === null) {
521
            $sheets = $this->spreadsheet->getAllSheets();
522
        } else {
523 11
            $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex);
524
        }
525
526
        // Construct HTML
527 11
        $html = '';
528
529
        // Only if there are more than 1 sheets
530 11
        if (count($sheets) > 1) {
531
            // Loop all sheets
532
            $sheetId = 0;
533
534
            $html .= '<ul class="navigation">' . PHP_EOL;
535
536
            foreach ($sheets as $sheet) {
537
                $html .= '  <li class="sheet' . $sheetId . '"><a href="#sheet' . $sheetId . '">' . $sheet->getTitle() . '</a></li>' . PHP_EOL;
538
                ++$sheetId;
539
            }
540
541
            $html .= '</ul>' . PHP_EOL;
542
        }
543
544 11
        return $html;
545
    }
546
547 13
    private function extendRowsForChartsAndImages(Worksheet $pSheet, $row)
548
    {
549 13
        $rowMax = $row;
550 13
        $colMax = 'A';
551 13
        if ($this->includeCharts) {
552
            foreach ($pSheet->getChartCollection() as $chart) {
553
                if ($chart instanceof Chart) {
554
                    $chartCoordinates = $chart->getTopLeftPosition();
555
                    $chartTL = Coordinate::coordinateFromString($chartCoordinates['cell']);
556
                    $chartCol = Coordinate::columnIndexFromString($chartTL[0]);
557
                    if ($chartTL[1] > $rowMax) {
558
                        $rowMax = $chartTL[1];
559
                        if ($chartCol > Coordinate::columnIndexFromString($colMax)) {
560
                            $colMax = $chartTL[0];
561
                        }
562
                    }
563
                }
564
            }
565
        }
566
567 13
        foreach ($pSheet->getDrawingCollection() as $drawing) {
568 4
            if ($drawing instanceof Drawing) {
569 3
                $imageTL = Coordinate::coordinateFromString($drawing->getCoordinates());
570 3
                $imageCol = Coordinate::columnIndexFromString($imageTL[0]);
571 3
                if ($imageTL[1] > $rowMax) {
572
                    $rowMax = $imageTL[1];
573
                    if ($imageCol > Coordinate::columnIndexFromString($colMax)) {
574
                        $colMax = $imageTL[0];
575
                    }
576
                }
577
            }
578
        }
579
580
        // Don't extend rows if not needed
581 13
        if ($row === $rowMax) {
582 13
            return '';
583
        }
584
585
        $html = '';
586
        ++$colMax;
587
588
        while ($row <= $rowMax) {
589
            $html .= '<tr>';
590
            for ($col = 'A'; $col != $colMax; ++$col) {
591
                $html .= '<td>';
592
                $html .= $this->writeImageInCell($pSheet, $col . $row);
593
                if ($this->includeCharts) {
594
                    $html .= $this->writeChartInCell($pSheet, $col . $row);
595
                }
596
                $html .= '</td>';
597
            }
598
            ++$row;
599
            $html .= '</tr>';
600
        }
601
602
        return $html;
603
    }
604
605
    /**
606
     * Generate image tag in cell.
607
     *
608
     * @param Worksheet $pSheet \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet
609
     * @param string $coordinates Cell coordinates
610
     *
611
     * @return string
612
     */
613 13
    private function writeImageInCell(Worksheet $pSheet, $coordinates)
614
    {
615
        // Construct HTML
616 13
        $html = '';
617
618
        // Write images
619 13
        foreach ($pSheet->getDrawingCollection() as $drawing) {
620 4
            if ($drawing instanceof Drawing) {
621 3
                if ($drawing->getCoordinates() == $coordinates) {
622 3
                    $filename = $drawing->getPath();
623
624
                    // Strip off eventual '.'
625 3
                    if (substr($filename, 0, 1) == '.') {
626
                        $filename = substr($filename, 1);
627
                    }
628
629
                    // Prepend images root
630 3
                    $filename = $this->getImagesRoot() . $filename;
631
632
                    // Strip off eventual '.'
633 3
                    if (substr($filename, 0, 1) == '.' && substr($filename, 0, 2) != './') {
634
                        $filename = substr($filename, 1);
635
                    }
636
637
                    // Convert UTF8 data to PCDATA
638 3
                    $filename = htmlspecialchars($filename);
639
640 3
                    $html .= PHP_EOL;
641 3
                    if ((!$this->embedImages) || ($this->isPdf)) {
642 3
                        $imageData = $filename;
643
                    } else {
644
                        $imageDetails = getimagesize($filename);
645
                        if ($fp = fopen($filename, 'rb', 0)) {
646
                            $picture = '';
647
                            while (!feof($fp)) {
648
                                $picture .= fread($fp, 1024);
649
                            }
650
                            fclose($fp);
651
                            // base64 encode the binary data, then break it
652
                            // into chunks according to RFC 2045 semantics
653
                            $base64 = chunk_split(base64_encode($picture));
654
                            $imageData = 'data:' . $imageDetails['mime'] . ';base64,' . $base64;
655
                        } else {
656
                            $imageData = $filename;
657
                        }
658
                    }
659
660 3
                    $html .= '<div style="position: relative;">';
661
                    $html .= '<img style="position: absolute; z-index: 1; left: ' .
662 3
                        $drawing->getOffsetX() . 'px; top: ' . $drawing->getOffsetY() . 'px; width: ' .
663 3
                        $drawing->getWidth() . 'px; height: ' . $drawing->getHeight() . 'px;" src="' .
664 3
                        $imageData . '" border="0" />';
665 3
                    $html .= '</div>';
666
                }
667 1
            } elseif ($drawing instanceof MemoryDrawing) {
668 1
                if ($drawing->getCoordinates() != $coordinates) {
669
                    continue;
670
                }
671 1
                ob_start(); //  Let's start output buffering.
672 1
                imagepng($drawing->getImageResource()); //  This will normally output the image, but because of ob_start(), it won't.
673 1
                $contents = ob_get_contents(); //  Instead, output above is saved to $contents
674 1
                ob_end_clean(); //  End the output buffer.
675
676 1
                $dataUri = 'data:image/jpeg;base64,' . base64_encode($contents);
677
678
                //  Because of the nature of tables, width is more important than height.
679
                //  max-width: 100% ensures that image doesnt overflow containing cell
680
                //  width: X sets width of supplied image.
681
                //  As a result, images bigger than cell will be contained and images smaller will not get stretched
682 1
                $html .= '<img src="' . $dataUri . '" style="max-width:100%;width:' . $drawing->getWidth() . 'px;" />';
683
            }
684
        }
685
686 13
        return $html;
687
    }
688
689
    /**
690
     * Generate chart tag in cell.
691
     *
692
     * @param Worksheet $pSheet \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet
693
     * @param string $coordinates Cell coordinates
694
     *
695
     * @return string
696
     */
697
    private function writeChartInCell(Worksheet $pSheet, $coordinates)
698
    {
699
        // Construct HTML
700
        $html = '';
701
702
        // Write charts
703
        foreach ($pSheet->getChartCollection() as $chart) {
704
            if ($chart instanceof Chart) {
705
                $chartCoordinates = $chart->getTopLeftPosition();
706
                if ($chartCoordinates['cell'] == $coordinates) {
707
                    $chartFileName = File::sysGetTempDir() . '/' . uniqid('', true) . '.png';
708
                    if (!$chart->render($chartFileName)) {
709
                        return;
710
                    }
711
712
                    $html .= PHP_EOL;
713
                    $imageDetails = getimagesize($chartFileName);
714
                    if ($fp = fopen($chartFileName, 'rb', 0)) {
715
                        $picture = fread($fp, filesize($chartFileName));
716
                        fclose($fp);
717
                        // base64 encode the binary data, then break it
718
                        // into chunks according to RFC 2045 semantics
719
                        $base64 = chunk_split(base64_encode($picture));
720
                        $imageData = 'data:' . $imageDetails['mime'] . ';base64,' . $base64;
721
722
                        $html .= '<div style="position: relative;">';
723
                        $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;
724
                        $html .= '</div>';
725
726
                        unlink($chartFileName);
727
                    }
728
                }
729
            }
730
        }
731
732
        // Return
733
        return $html;
734
    }
735
736
    /**
737
     * Generate CSS styles.
738
     *
739
     * @param bool $generateSurroundingHTML Generate surrounding HTML tags? (&lt;style&gt; and &lt;/style&gt;)
740
     *
741
     * @throws WriterException
742
     *
743
     * @return string
744
     */
745 11
    public function generateStyles($generateSurroundingHTML = true)
746
    {
747
        // Build CSS
748 11
        $css = $this->buildCSS($generateSurroundingHTML);
749
750
        // Construct HTML
751 11
        $html = '';
752
753
        // Start styles
754 11
        if ($generateSurroundingHTML) {
755 11
            $html .= '    <style type="text/css">' . PHP_EOL;
756 11
            $html .= '      html { ' . $this->assembleCSS($css['html']) . ' }' . PHP_EOL;
757
        }
758
759
        // Write all other styles
760 11
        foreach ($css as $styleName => $styleDefinition) {
761 11
            if ($styleName != 'html') {
762 11
                $html .= '      ' . $styleName . ' { ' . $this->assembleCSS($styleDefinition) . ' }' . PHP_EOL;
763
            }
764
        }
765
766
        // End styles
767 11
        if ($generateSurroundingHTML) {
768 11
            $html .= '    </style>' . PHP_EOL;
769
        }
770
771
        // Return
772 11
        return $html;
773
    }
774
775
    /**
776
     * Build CSS styles.
777
     *
778
     * @param bool $generateSurroundingHTML Generate surrounding HTML style? (html { })
779
     *
780
     * @throws WriterException
781
     *
782
     * @return array
783
     */
784 13
    public function buildCSS($generateSurroundingHTML = true)
785
    {
786
        // Cached?
787 13
        if ($this->cssStyles !== null) {
788 11
            return $this->cssStyles;
789
        }
790
791
        // Ensure that spans have been calculated
792 13
        if (!$this->spansAreCalculated) {
793 13
            $this->calculateSpans();
794
        }
795
796
        // Construct CSS
797 13
        $css = [];
798
799
        // Start styles
800 13
        if ($generateSurroundingHTML) {
801
            // html { }
802 13
            $css['html']['font-family'] = 'Calibri, Arial, Helvetica, sans-serif';
803 13
            $css['html']['font-size'] = '11pt';
804 13
            $css['html']['background-color'] = 'white';
805
        }
806
807
        // CSS for comments as found in LibreOffice
808 13
        $css['a.comment-indicator:hover + div.comment'] = [
809
            'background' => '#ffd',
810
            'position' => 'absolute',
811
            'display' => 'block',
812
            'border' => '1px solid black',
813
            'padding' => '0.5em',
814
        ];
815
816 13
        $css['a.comment-indicator'] = [
817
            'background' => 'red',
818
            'display' => 'inline-block',
819
            'border' => '1px solid black',
820
            'width' => '0.5em',
821
            'height' => '0.5em',
822
        ];
823
824 13
        $css['div.comment']['display'] = 'none';
825
826
        // table { }
827 13
        $css['table']['border-collapse'] = 'collapse';
828 13
        if (!$this->isPdf) {
829 11
            $css['table']['page-break-after'] = 'always';
830
        }
831
832
        // .gridlines td { }
833 13
        $css['.gridlines td']['border'] = '1px dotted black';
834 13
        $css['.gridlines th']['border'] = '1px dotted black';
835
836
        // .b {}
837 13
        $css['.b']['text-align'] = 'center'; // BOOL
838
839
        // .e {}
840 13
        $css['.e']['text-align'] = 'center'; // ERROR
841
842
        // .f {}
843 13
        $css['.f']['text-align'] = 'right'; // FORMULA
844
845
        // .inlineStr {}
846 13
        $css['.inlineStr']['text-align'] = 'left'; // INLINE
847
848
        // .n {}
849 13
        $css['.n']['text-align'] = 'right'; // NUMERIC
850
851
        // .s {}
852 13
        $css['.s']['text-align'] = 'left'; // STRING
853
854
        // Calculate cell style hashes
855 13
        foreach ($this->spreadsheet->getCellXfCollection() as $index => $style) {
856 13
            $css['td.style' . $index] = $this->createCSSStyle($style);
857 13
            $css['th.style' . $index] = $this->createCSSStyle($style);
858
        }
859
860
        // Fetch sheets
861 13
        $sheets = [];
862 13
        if ($this->sheetIndex === null) {
863
            $sheets = $this->spreadsheet->getAllSheets();
864
        } else {
865 13
            $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex);
866
        }
867
868
        // Build styles per sheet
869 13
        foreach ($sheets as $sheet) {
870
            // Calculate hash code
871 13
            $sheetIndex = $sheet->getParent()->getIndex($sheet);
872
873
            // Build styles
874
            // Calculate column widths
875 13
            $sheet->calculateColumnWidths();
876
877
            // col elements, initialize
878 13
            $highestColumnIndex = Coordinate::columnIndexFromString($sheet->getHighestColumn()) - 1;
879 13
            $column = -1;
880 13
            while ($column++ < $highestColumnIndex) {
881 13
                $this->columnWidths[$sheetIndex][$column] = 42; // approximation
882 13
                $css['table.sheet' . $sheetIndex . ' col.col' . $column]['width'] = '42pt';
883
            }
884
885
            // col elements, loop through columnDimensions and set width
886 13
            foreach ($sheet->getColumnDimensions() as $columnDimension) {
887 4
                if (($width = SharedDrawing::cellDimensionToPixels($columnDimension->getWidth(), $this->defaultFont)) >= 0) {
888 4
                    $width = SharedDrawing::pixelsToPoints($width);
889 4
                    $column = Coordinate::columnIndexFromString($columnDimension->getColumnIndex()) - 1;
890 4
                    $this->columnWidths[$sheetIndex][$column] = $width;
891 4
                    $css['table.sheet' . $sheetIndex . ' col.col' . $column]['width'] = $width . 'pt';
892
893 4
                    if ($columnDimension->getVisible() === false) {
894
                        $css['table.sheet' . $sheetIndex . ' col.col' . $column]['visibility'] = 'collapse';
895
                        $css['table.sheet' . $sheetIndex . ' col.col' . $column]['*display'] = 'none'; // target IE6+7
896
                    }
897
                }
898
            }
899
900
            // Default row height
901 13
            $rowDimension = $sheet->getDefaultRowDimension();
902
903
            // table.sheetN tr { }
904 13
            $css['table.sheet' . $sheetIndex . ' tr'] = [];
905
906 13
            if ($rowDimension->getRowHeight() == -1) {
907 13
                $pt_height = SharedFont::getDefaultRowHeightByFont($this->spreadsheet->getDefaultStyle()->getFont());
908
            } else {
909
                $pt_height = $rowDimension->getRowHeight();
910
            }
911 13
            $css['table.sheet' . $sheetIndex . ' tr']['height'] = $pt_height . 'pt';
912 13
            if ($rowDimension->getVisible() === false) {
913
                $css['table.sheet' . $sheetIndex . ' tr']['display'] = 'none';
914
                $css['table.sheet' . $sheetIndex . ' tr']['visibility'] = 'hidden';
915
            }
916
917
            // Calculate row heights
918 13
            foreach ($sheet->getRowDimensions() as $rowDimension) {
919 2
                $row = $rowDimension->getRowIndex() - 1;
920
921
                // table.sheetN tr.rowYYYYYY { }
922 2
                $css['table.sheet' . $sheetIndex . ' tr.row' . $row] = [];
923
924 2
                if ($rowDimension->getRowHeight() == -1) {
925 2
                    $pt_height = SharedFont::getDefaultRowHeightByFont($this->spreadsheet->getDefaultStyle()->getFont());
926
                } else {
927 1
                    $pt_height = $rowDimension->getRowHeight();
928
                }
929 2
                $css['table.sheet' . $sheetIndex . ' tr.row' . $row]['height'] = $pt_height . 'pt';
930 2
                if ($rowDimension->getVisible() === false) {
931
                    $css['table.sheet' . $sheetIndex . ' tr.row' . $row]['display'] = 'none';
932
                    $css['table.sheet' . $sheetIndex . ' tr.row' . $row]['visibility'] = 'hidden';
933
                }
934
            }
935
        }
936
937
        // Cache
938 13
        if ($this->cssStyles === null) {
939 13
            $this->cssStyles = $css;
940
        }
941
942
        // Return
943 13
        return $css;
944
    }
945
946
    /**
947
     * Create CSS style.
948
     *
949
     * @param Style $pStyle
950
     *
951
     * @return array
952
     */
953 13
    private function createCSSStyle(Style $pStyle)
954
    {
955
        // Create CSS
956 13
        $css = array_merge(
957 13
            $this->createCSSStyleAlignment($pStyle->getAlignment()),
958 13
            $this->createCSSStyleBorders($pStyle->getBorders()),
959 13
            $this->createCSSStyleFont($pStyle->getFont()),
960 13
            $this->createCSSStyleFill($pStyle->getFill())
961
        );
962
963
        // Return
964 13
        return $css;
965
    }
966
967
    /**
968
     * Create CSS style (\PhpOffice\PhpSpreadsheet\Style\Alignment).
969
     *
970
     * @param Alignment $pStyle \PhpOffice\PhpSpreadsheet\Style\Alignment
971
     *
972
     * @return array
973
     */
974 13
    private function createCSSStyleAlignment(Alignment $pStyle)
975
    {
976
        // Construct CSS
977 13
        $css = [];
978
979
        // Create CSS
980 13
        $css['vertical-align'] = $this->mapVAlign($pStyle->getVertical());
981 13
        if ($textAlign = $this->mapHAlign($pStyle->getHorizontal())) {
982 4
            $css['text-align'] = $textAlign;
983 4
            if (in_array($textAlign, ['left', 'right'])) {
984 3
                $css['padding-' . $textAlign] = (string) ((int) $pStyle->getIndent() * 9) . 'px';
985
            }
986
        }
987
988 13
        return $css;
989
    }
990
991
    /**
992
     * Create CSS style (\PhpOffice\PhpSpreadsheet\Style\Font).
993
     *
994
     * @param Font $pStyle
995
     *
996
     * @return array
997
     */
998 13
    private function createCSSStyleFont(Font $pStyle)
999
    {
1000
        // Construct CSS
1001 13
        $css = [];
1002
1003
        // Create CSS
1004 13
        if ($pStyle->getBold()) {
1005 4
            $css['font-weight'] = 'bold';
1006
        }
1007 13
        if ($pStyle->getUnderline() != Font::UNDERLINE_NONE && $pStyle->getStrikethrough()) {
1008
            $css['text-decoration'] = 'underline line-through';
1009 13
        } elseif ($pStyle->getUnderline() != Font::UNDERLINE_NONE) {
1010 3
            $css['text-decoration'] = 'underline';
1011 13
        } elseif ($pStyle->getStrikethrough()) {
1012
            $css['text-decoration'] = 'line-through';
1013
        }
1014 13
        if ($pStyle->getItalic()) {
1015 3
            $css['font-style'] = 'italic';
1016
        }
1017
1018 13
        $css['color'] = '#' . $pStyle->getColor()->getRGB();
1019 13
        $css['font-family'] = '\'' . $pStyle->getName() . '\'';
1020 13
        $css['font-size'] = $pStyle->getSize() . 'pt';
1021
1022 13
        return $css;
1023
    }
1024
1025
    /**
1026
     * Create CSS style (Borders).
1027
     *
1028
     * @param Borders $pStyle Borders
1029
     *
1030
     * @return array
1031
     */
1032 13
    private function createCSSStyleBorders(Borders $pStyle)
1033
    {
1034
        // Construct CSS
1035 13
        $css = [];
1036
1037
        // Create CSS
1038 13
        $css['border-bottom'] = $this->createCSSStyleBorder($pStyle->getBottom());
1039 13
        $css['border-top'] = $this->createCSSStyleBorder($pStyle->getTop());
1040 13
        $css['border-left'] = $this->createCSSStyleBorder($pStyle->getLeft());
1041 13
        $css['border-right'] = $this->createCSSStyleBorder($pStyle->getRight());
1042
1043 13
        return $css;
1044
    }
1045
1046
    /**
1047
     * Create CSS style (Border).
1048
     *
1049
     * @param Border $pStyle Border
1050
     *
1051
     * @return string
1052
     */
1053 13
    private function createCSSStyleBorder(Border $pStyle)
1054
    {
1055
        //    Create CSS - add !important to non-none border styles for merged cells
1056 13
        $borderStyle = $this->mapBorderStyle($pStyle->getBorderStyle());
0 ignored issues
show
Bug introduced by
$pStyle->getBorderStyle() of type string is incompatible with the type integer expected by parameter $borderStyle of PhpOffice\PhpSpreadsheet...\Html::mapBorderStyle(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1056
        $borderStyle = $this->mapBorderStyle(/** @scrutinizer ignore-type */ $pStyle->getBorderStyle());
Loading history...
1057 13
        $css = $borderStyle . ' #' . $pStyle->getColor()->getRGB() . (($borderStyle == 'none') ? '' : ' !important');
1058
1059 13
        return $css;
1060
    }
1061
1062
    /**
1063
     * Create CSS style (Fill).
1064
     *
1065
     * @param Fill $pStyle Fill
1066
     *
1067
     * @return array
1068
     */
1069 13
    private function createCSSStyleFill(Fill $pStyle)
1070
    {
1071
        // Construct HTML
1072 13
        $css = [];
1073
1074
        // Create CSS
1075 13
        $value = $pStyle->getFillType() == Fill::FILL_NONE ?
1076 13
            'white' : '#' . $pStyle->getStartColor()->getRGB();
1077 13
        $css['background-color'] = $value;
1078
1079 13
        return $css;
1080
    }
1081
1082
    /**
1083
     * Generate HTML footer.
1084
     */
1085 13
    public function generateHTMLFooter()
1086
    {
1087
        // Construct HTML
1088 13
        $html = '';
1089 13
        $html .= '  </body>' . PHP_EOL;
1090 13
        $html .= '</html>' . PHP_EOL;
1091
1092 13
        return $html;
1093
    }
1094
1095
    /**
1096
     * Generate table header.
1097
     *
1098
     * @param Worksheet $pSheet The worksheet for the table we are writing
1099
     *
1100
     * @return string
1101
     */
1102 13
    private function generateTableHeader($pSheet)
1103
    {
1104 13
        $sheetIndex = $pSheet->getParent()->getIndex($pSheet);
1105
1106
        // Construct HTML
1107 13
        $html = '';
1108 13
        $html .= $this->setMargins($pSheet);
1109
1110 13
        if (!$this->useInlineCss) {
1111 11
            $gridlines = $pSheet->getShowGridlines() ? ' gridlines' : '';
1112 11
            $html .= '    <table border="0" cellpadding="0" cellspacing="0" id="sheet' . $sheetIndex . '" class="sheet' . $sheetIndex . $gridlines . '">' . PHP_EOL;
1113
        } else {
1114 3
            $style = isset($this->cssStyles['table']) ?
1115 3
                $this->assembleCSS($this->cssStyles['table']) : '';
1116
1117 3
            if ($this->isPdf && $pSheet->getShowGridlines()) {
1118 1
                $html .= '    <table border="1" cellpadding="1" id="sheet' . $sheetIndex . '" cellspacing="1" style="' . $style . '">' . PHP_EOL;
1119
            } else {
1120 2
                $html .= '    <table border="0" cellpadding="1" id="sheet' . $sheetIndex . '" cellspacing="0" style="' . $style . '">' . PHP_EOL;
1121
            }
1122
        }
1123
1124
        // Write <col> elements
1125 13
        $highestColumnIndex = Coordinate::columnIndexFromString($pSheet->getHighestColumn()) - 1;
1126 13
        $i = -1;
1127 13
        while ($i++ < $highestColumnIndex) {
1128 13
            if (!$this->isPdf) {
1129 11
                if (!$this->useInlineCss) {
1130 11
                    $html .= '        <col class="col' . $i . '">' . PHP_EOL;
1131
                } else {
1132
                    $style = isset($this->cssStyles['table.sheet' . $sheetIndex . ' col.col' . $i]) ?
1133
                        $this->assembleCSS($this->cssStyles['table.sheet' . $sheetIndex . ' col.col' . $i]) : '';
1134
                    $html .= '        <col style="' . $style . '">' . PHP_EOL;
1135
                }
1136
            }
1137
        }
1138
1139 13
        return $html;
1140
    }
1141
1142
    /**
1143
     * Generate table footer.
1144
     */
1145 13
    private function generateTableFooter()
1146
    {
1147 13
        $html = '    </table>' . PHP_EOL;
1148
1149 13
        return $html;
1150
    }
1151
1152
    /**
1153
     * Generate row.
1154
     *
1155
     * @param Worksheet $pSheet \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet
1156
     * @param array $pValues Array containing cells in a row
1157
     * @param int $pRow Row number (0-based)
1158
     * @param string $cellType eg: 'td'
1159
     *
1160
     * @throws WriterException
1161
     *
1162
     * @return string
1163
     */
1164 13
    private function generateRow(Worksheet $pSheet, array $pValues, $pRow, $cellType)
1165
    {
1166
        // Construct HTML
1167 13
        $html = '';
1168
1169
        // Sheet index
1170 13
        $sheetIndex = $pSheet->getParent()->getIndex($pSheet);
1171
1172
        // Dompdf and breaks
1173 13
        if ($this->isPdf && count($pSheet->getBreaks()) > 0) {
1174
            $breaks = $pSheet->getBreaks();
1175
1176
            // check if a break is needed before this row
1177
            if (isset($breaks['A' . $pRow])) {
1178
                // close table: </table>
1179
                $html .= $this->generateTableFooter();
1180
1181
                // insert page break
1182
                $html .= '<div style="page-break-before:always" />';
1183
1184
                // open table again: <table> + <col> etc.
1185
                $html .= $this->generateTableHeader($pSheet);
1186
            }
1187
        }
1188
1189
        // Write row start
1190 13
        if (!$this->useInlineCss) {
1191 11
            $html .= '          <tr class="row' . $pRow . '">' . PHP_EOL;
1192
        } else {
1193 3
            $style = isset($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $pRow])
1194 3
                ? $this->assembleCSS($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $pRow]) : '';
1195
1196 3
            $html .= '          <tr style="' . $style . '">' . PHP_EOL;
1197
        }
1198
1199
        // Write cells
1200 13
        $colNum = 0;
1201 13
        foreach ($pValues as $cellAddress) {
1202 13
            $cell = ($cellAddress > '') ? $pSheet->getCell($cellAddress) : '';
1203 13
            $coordinate = Coordinate::stringFromColumnIndex($colNum + 1) . ($pRow + 1);
1204 13
            if (!$this->useInlineCss) {
1205 11
                $cssClass = 'column' . $colNum;
1206
            } else {
1207 3
                $cssClass = [];
1208 3
                if ($cellType == 'th') {
1209
                    if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' th.column' . $colNum])) {
1210
                        $this->cssStyles['table.sheet' . $sheetIndex . ' th.column' . $colNum];
1211
                    }
1212
                } else {
1213 3
                    if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' td.column' . $colNum])) {
1214
                        $this->cssStyles['table.sheet' . $sheetIndex . ' td.column' . $colNum];
1215
                    }
1216
                }
1217
            }
1218 13
            $colSpan = 1;
1219 13
            $rowSpan = 1;
1220
1221
            // initialize
1222 13
            $cellData = '&nbsp;';
1223
1224
            // Cell
1225 13
            if ($cell instanceof Cell) {
1226 13
                $cellData = '';
1227 13
                if ($cell->getParent() === null) {
1228
                    $cell->attach($pSheet);
0 ignored issues
show
Bug introduced by
$pSheet of type PhpOffice\PhpSpreadsheet\Worksheet\Worksheet is incompatible with the type PhpOffice\PhpSpreadsheet\Collection\Cells expected by parameter $parent of PhpOffice\PhpSpreadsheet\Cell\Cell::attach(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1228
                    $cell->attach(/** @scrutinizer ignore-type */ $pSheet);
Loading history...
1229
                }
1230
                // Value
1231 13
                if ($cell->getValue() instanceof RichText) {
1232
                    // Loop through rich text elements
1233 4
                    $elements = $cell->getValue()->getRichTextElements();
1234 4
                    foreach ($elements as $element) {
1235
                        // Rich text start?
1236 4
                        if ($element instanceof Run) {
1237 4
                            $cellData .= '<span style="' . $this->assembleCSS($this->createCSSStyleFont($element->getFont())) . '">';
1238
1239 4
                            if ($element->getFont()->getSuperscript()) {
1240
                                $cellData .= '<sup>';
1241 4
                            } elseif ($element->getFont()->getSubscript()) {
1242
                                $cellData .= '<sub>';
1243
                            }
1244
                        }
1245
1246
                        // Convert UTF8 data to PCDATA
1247 4
                        $cellText = $element->getText();
1248 4
                        $cellData .= htmlspecialchars($cellText);
1249
1250 4
                        if ($element instanceof Run) {
1251 4
                            if ($element->getFont()->getSuperscript()) {
1252
                                $cellData .= '</sup>';
1253 4
                            } elseif ($element->getFont()->getSubscript()) {
1254
                                $cellData .= '</sub>';
1255
                            }
1256
1257 4
                            $cellData .= '</span>';
1258
                        }
1259
                    }
1260
                } else {
1261 13
                    if ($this->preCalculateFormulas) {
1262 13
                        $cellData = NumberFormat::toFormattedString(
1263 13
                            $cell->getCalculatedValue(),
1264 13
                            $pSheet->getParent()->getCellXfByIndex($cell->getXfIndex())->getNumberFormat()->getFormatCode(),
1265 13
                            [$this, 'formatColor']
1266
                        );
1267
                    } else {
1268
                        $cellData = NumberFormat::toFormattedString(
1269
                            $cell->getValue(),
1270
                            $pSheet->getParent()->getCellXfByIndex($cell->getXfIndex())->getNumberFormat()->getFormatCode(),
1271
                            [$this, 'formatColor']
1272
                        );
1273
                    }
1274 13
                    $cellData = htmlspecialchars($cellData);
1275 13
                    if ($pSheet->getParent()->getCellXfByIndex($cell->getXfIndex())->getFont()->getSuperscript()) {
1276
                        $cellData = '<sup>' . $cellData . '</sup>';
1277 13
                    } elseif ($pSheet->getParent()->getCellXfByIndex($cell->getXfIndex())->getFont()->getSubscript()) {
1278
                        $cellData = '<sub>' . $cellData . '</sub>';
1279
                    }
1280
                }
1281
1282
                // Converts the cell content so that spaces occuring at beginning of each new line are replaced by &nbsp;
1283
                // Example: "  Hello\n to the world" is converted to "&nbsp;&nbsp;Hello\n&nbsp;to the world"
1284 13
                $cellData = preg_replace('/(?m)(?:^|\\G) /', '&nbsp;', $cellData);
1285
1286
                // convert newline "\n" to '<br>'
1287 13
                $cellData = nl2br($cellData);
1288
1289
                // Extend CSS class?
1290 13
                if (!$this->useInlineCss) {
1291 11
                    $cssClass .= ' style' . $cell->getXfIndex();
1292 11
                    $cssClass .= ' ' . $cell->getDataType();
1293
                } else {
1294 3
                    if ($cellType == 'th') {
1295
                        if (isset($this->cssStyles['th.style' . $cell->getXfIndex()])) {
1296
                            $cssClass = array_merge($cssClass, $this->cssStyles['th.style' . $cell->getXfIndex()]);
0 ignored issues
show
Bug introduced by
$cssClass of type string is incompatible with the type array expected by parameter $array1 of array_merge(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1296
                            $cssClass = array_merge(/** @scrutinizer ignore-type */ $cssClass, $this->cssStyles['th.style' . $cell->getXfIndex()]);
Loading history...
1297
                        }
1298
                    } else {
1299 3
                        if (isset($this->cssStyles['td.style' . $cell->getXfIndex()])) {
1300 3
                            $cssClass = array_merge($cssClass, $this->cssStyles['td.style' . $cell->getXfIndex()]);
1301
                        }
1302
                    }
1303
1304
                    // General horizontal alignment: Actual horizontal alignment depends on dataType
1305 3
                    $sharedStyle = $pSheet->getParent()->getCellXfByIndex($cell->getXfIndex());
1306 3
                    if ($sharedStyle->getAlignment()->getHorizontal() == Alignment::HORIZONTAL_GENERAL
1307 3
                        && isset($this->cssStyles['.' . $cell->getDataType()]['text-align'])
1308
                    ) {
1309 3
                        $cssClass['text-align'] = $this->cssStyles['.' . $cell->getDataType()]['text-align'];
1310
                    }
1311
                }
1312
            }
1313
1314
            // Hyperlink?
1315 13
            if ($pSheet->hyperlinkExists($coordinate) && !$pSheet->getHyperlink($coordinate)->isInternal()) {
1316 3
                $cellData = '<a href="' . htmlspecialchars($pSheet->getHyperlink($coordinate)->getUrl()) . '" title="' . htmlspecialchars($pSheet->getHyperlink($coordinate)->getTooltip()) . '">' . $cellData . '</a>';
1317
            }
1318
1319
            // Should the cell be written or is it swallowed by a rowspan or colspan?
1320 13
            $writeCell = !(isset($this->isSpannedCell[$pSheet->getParent()->getIndex($pSheet)][$pRow + 1][$colNum])
1321 13
                && $this->isSpannedCell[$pSheet->getParent()->getIndex($pSheet)][$pRow + 1][$colNum]);
1322
1323
            // Colspan and Rowspan
1324 13
            $colspan = 1;
0 ignored issues
show
Unused Code introduced by
The assignment to $colspan is dead and can be removed.
Loading history...
1325 13
            $rowspan = 1;
0 ignored issues
show
Unused Code introduced by
The assignment to $rowspan is dead and can be removed.
Loading history...
1326 13
            if (isset($this->isBaseCell[$pSheet->getParent()->getIndex($pSheet)][$pRow + 1][$colNum])) {
1327 5
                $spans = $this->isBaseCell[$pSheet->getParent()->getIndex($pSheet)][$pRow + 1][$colNum];
1328 5
                $rowSpan = $spans['rowspan'];
1329 5
                $colSpan = $spans['colspan'];
1330
1331
                //    Also apply style from last cell in merge to fix borders -
1332
                //        relies on !important for non-none border declarations in createCSSStyleBorder
1333 5
                $endCellCoord = Coordinate::stringFromColumnIndex($colNum + $colSpan) . ($pRow + $rowSpan);
1334 5
                if (!$this->useInlineCss) {
1335 3
                    $cssClass .= ' style' . $pSheet->getCell($endCellCoord)->getXfIndex();
1336
                }
1337
            }
1338
1339
            // Write
1340 13
            if ($writeCell) {
1341
                // Column start
1342 13
                $html .= '            <' . $cellType;
1343 13
                if (!$this->useInlineCss) {
1344 11
                    $html .= ' class="' . $cssClass . '"';
1 ignored issue
show
Bug introduced by
Are you sure $cssClass of type array|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1344
                    $html .= ' class="' . /** @scrutinizer ignore-type */ $cssClass . '"';
Loading history...
1345
                } else {
1346
                    //** Necessary redundant code for the sake of \PhpOffice\PhpSpreadsheet\Writer\Pdf **
1347
                    // We must explicitly write the width of the <td> element because TCPDF
1348
                    // does not recognize e.g. <col style="width:42pt">
1349 3
                    $width = 0;
1350 3
                    $i = $colNum - 1;
1351 3
                    $e = $colNum + $colSpan - 1;
1352 3
                    while ($i++ < $e) {
1353 3
                        if (isset($this->columnWidths[$sheetIndex][$i])) {
1354 3
                            $width += $this->columnWidths[$sheetIndex][$i];
1355
                        }
1356
                    }
1357 3
                    $cssClass['width'] = $width . 'pt';
1358
1359
                    // We must also explicitly write the height of the <td> element because TCPDF
1360
                    // does not recognize e.g. <tr style="height:50pt">
1361 3
                    if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $pRow]['height'])) {
1362 1
                        $height = $this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $pRow]['height'];
1363 1
                        $cssClass['height'] = $height;
1364
                    }
1365
                    //** end of redundant code **
1366
1367 3
                    $html .= ' style="' . $this->assembleCSS($cssClass) . '"';
1 ignored issue
show
Bug introduced by
It seems like $cssClass can also be of type string; however, parameter $pValue of PhpOffice\PhpSpreadsheet...ter\Html::assembleCSS() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1367
                    $html .= ' style="' . $this->assembleCSS(/** @scrutinizer ignore-type */ $cssClass) . '"';
Loading history...
1368
                }
1369 13
                if ($colSpan > 1) {
1370 5
                    $html .= ' colspan="' . $colSpan . '"';
1371
                }
1372 13
                if ($rowSpan > 1) {
1373
                    $html .= ' rowspan="' . $rowSpan . '"';
1374
                }
1375 13
                $html .= '>';
1376
1377 13
                $html .= $this->writeComment($pSheet, $coordinate);
1378
1379
                // Image?
1380 13
                $html .= $this->writeImageInCell($pSheet, $coordinate);
1381
1382
                // Chart?
1383 13
                if ($this->includeCharts) {
1384
                    $html .= $this->writeChartInCell($pSheet, $coordinate);
1385
                }
1386
1387
                // Cell data
1388 13
                $html .= $cellData;
1389
1390
                // Column end
1391 13
                $html .= '</' . $cellType . '>' . PHP_EOL;
1392
            }
1393
1394
            // Next column
1395 13
            ++$colNum;
1396
        }
1397
1398
        // Write row end
1399 13
        $html .= '          </tr>' . PHP_EOL;
1400
1401
        // Return
1402 13
        return $html;
1403
    }
1404
1405
    /**
1406
     * Takes array where of CSS properties / values and converts to CSS string.
1407
     *
1408
     * @param array $pValue
1409
     *
1410
     * @return string
1411
     */
1412 13
    private function assembleCSS(array $pValue = [])
1413
    {
1414 13
        $pairs = [];
1415 13
        foreach ($pValue as $property => $value) {
1416 13
            $pairs[] = $property . ':' . $value;
1417
        }
1418 13
        $string = implode('; ', $pairs);
1419
1420 13
        return $string;
1421
    }
1422
1423
    /**
1424
     * Get images root.
1425
     *
1426
     * @return string
1427
     */
1428 3
    public function getImagesRoot()
1429
    {
1430 3
        return $this->imagesRoot;
1431
    }
1432
1433
    /**
1434
     * Set images root.
1435
     *
1436
     * @param string $pValue
1437
     *
1438
     * @return HTML
1439
     */
1440
    public function setImagesRoot($pValue)
1441
    {
1442
        $this->imagesRoot = $pValue;
1443
1444
        return $this;
1445
    }
1446
1447
    /**
1448
     * Get embed images.
1449
     *
1450
     * @return bool
1451
     */
1452
    public function getEmbedImages()
1453
    {
1454
        return $this->embedImages;
1455
    }
1456
1457
    /**
1458
     * Set embed images.
1459
     *
1460
     * @param bool $pValue
1461
     *
1462
     * @return HTML
1463
     */
1464
    public function setEmbedImages($pValue)
1465
    {
1466
        $this->embedImages = $pValue;
1467
1468
        return $this;
1469
    }
1470
1471
    /**
1472
     * Get use inline CSS?
1473
     *
1474
     * @return bool
1475
     */
1476
    public function getUseInlineCss()
1477
    {
1478
        return $this->useInlineCss;
1479
    }
1480
1481
    /**
1482
     * Set use inline CSS?
1483
     *
1484
     * @param bool $pValue
1485
     *
1486
     * @return HTML
1487
     */
1488 7
    public function setUseInlineCss($pValue)
1489
    {
1490 7
        $this->useInlineCss = $pValue;
1491
1492 7
        return $this;
1493
    }
1494
1495
    /**
1496
     * Add color to formatted string as inline style.
1497
     *
1498
     * @param string $pValue Plain formatted value without color
1499
     * @param string $pFormat Format code
1500
     *
1501
     * @return string
1502
     */
1503 3
    public function formatColor($pValue, $pFormat)
1504
    {
1505
        // Color information, e.g. [Red] is always at the beginning
1506 3
        $color = null; // initialize
1507 3
        $matches = [];
1508
1509 3
        $color_regex = '/^\\[[a-zA-Z]+\\]/';
1510 3
        if (preg_match($color_regex, $pFormat, $matches)) {
1511
            $color = str_replace(['[', ']'], '', $matches[0]);
1512
            $color = strtolower($color);
1513
        }
1514
1515
        // convert to PCDATA
1516 3
        $value = htmlspecialchars($pValue);
1517
1518
        // color span tag
1519 3
        if ($color !== null) {
1520
            $value = '<span style="color:' . $color . '">' . $value . '</span>';
1521
        }
1522
1523 3
        return $value;
1524
    }
1525
1526
    /**
1527
     * Calculate information about HTML colspan and rowspan which is not always the same as Excel's.
1528
     */
1529 13
    private function calculateSpans()
1530
    {
1531
        // Identify all cells that should be omitted in HTML due to cell merge.
1532
        // In HTML only the upper-left cell should be written and it should have
1533
        //   appropriate rowspan / colspan attribute
1534 13
        $sheetIndexes = $this->sheetIndex !== null ?
1535 13
            [$this->sheetIndex] : range(0, $this->spreadsheet->getSheetCount() - 1);
1536
1537 13
        foreach ($sheetIndexes as $sheetIndex) {
1538 13
            $sheet = $this->spreadsheet->getSheet($sheetIndex);
1539
1540 13
            $candidateSpannedRow = [];
1541
1542
            // loop through all Excel merged cells
1543 13
            foreach ($sheet->getMergeCells() as $cells) {
1544 5
                list($cells) = Coordinate::splitRange($cells);
0 ignored issues
show
Bug introduced by
$cells of type array is incompatible with the type string expected by parameter $pRange of PhpOffice\PhpSpreadsheet...oordinate::splitRange(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1544
                list($cells) = Coordinate::splitRange(/** @scrutinizer ignore-type */ $cells);
Loading history...
1545 5
                $first = $cells[0];
1546 5
                $last = $cells[1];
1547
1548 5
                list($fc, $fr) = Coordinate::coordinateFromString($first);
1549 5
                $fc = Coordinate::columnIndexFromString($fc) - 1;
1550
1551 5
                list($lc, $lr) = Coordinate::coordinateFromString($last);
1552 5
                $lc = Coordinate::columnIndexFromString($lc) - 1;
1553
1554
                // loop through the individual cells in the individual merge
1555 5
                $r = $fr - 1;
1556 5
                while ($r++ < $lr) {
1557
                    // also, flag this row as a HTML row that is candidate to be omitted
1558 5
                    $candidateSpannedRow[$r] = $r;
1559
1560 5
                    $c = $fc - 1;
1561 5
                    while ($c++ < $lc) {
1562 5
                        if (!($c == $fc && $r == $fr)) {
1563
                            // not the upper-left cell (should not be written in HTML)
1564 5
                            $this->isSpannedCell[$sheetIndex][$r][$c] = [
1565 5
                                'baseCell' => [$fr, $fc],
1566
                            ];
1567
                        } else {
1568
                            // upper-left is the base cell that should hold the colspan/rowspan attribute
1569 5
                            $this->isBaseCell[$sheetIndex][$r][$c] = [
1570 5
                                'xlrowspan' => $lr - $fr + 1, // Excel rowspan
1571 5
                                'rowspan' => $lr - $fr + 1, // HTML rowspan, value may change
1572 5
                                'xlcolspan' => $lc - $fc + 1, // Excel colspan
1573 5
                                'colspan' => $lc - $fc + 1, // HTML colspan, value may change
1574
                            ];
1575
                        }
1576
                    }
1577
                }
1578
            }
1579
1580
            // Identify which rows should be omitted in HTML. These are the rows where all the cells
1581
            //   participate in a merge and the where base cells are somewhere above.
1582 13
            $countColumns = Coordinate::columnIndexFromString($sheet->getHighestColumn());
1583 13
            foreach ($candidateSpannedRow as $rowIndex) {
1584 5
                if (isset($this->isSpannedCell[$sheetIndex][$rowIndex])) {
1585 5
                    if (count($this->isSpannedCell[$sheetIndex][$rowIndex]) == $countColumns) {
1586 3
                        $this->isSpannedRow[$sheetIndex][$rowIndex] = $rowIndex;
1587
                    }
1588
                }
1589
            }
1590
1591
            // For each of the omitted rows we found above, the affected rowspans should be subtracted by 1
1592 13
            if (isset($this->isSpannedRow[$sheetIndex])) {
1593 3
                foreach ($this->isSpannedRow[$sheetIndex] as $rowIndex) {
1594 3
                    $adjustedBaseCells = [];
1595 3
                    $c = -1;
1596 3
                    $e = $countColumns - 1;
1597 3
                    while ($c++ < $e) {
1598 3
                        $baseCell = $this->isSpannedCell[$sheetIndex][$rowIndex][$c]['baseCell'];
1599
1600 3
                        if (!in_array($baseCell, $adjustedBaseCells)) {
1601
                            // subtract rowspan by 1
1602 3
                            --$this->isBaseCell[$sheetIndex][$baseCell[0]][$baseCell[1]]['rowspan'];
1603 3
                            $adjustedBaseCells[] = $baseCell;
1604
                        }
1605
                    }
1606
                }
1607
            }
1608
1609
            // TODO: Same for columns
1610
        }
1611
1612
        // We have calculated the spans
1613 13
        $this->spansAreCalculated = true;
1614 13
    }
1615
1616 13
    private function setMargins(Worksheet $pSheet)
1617
    {
1618 13
        $htmlPage = '@page { ';
1619 13
        $htmlBody = 'body { ';
1620
1621 13
        $left = StringHelper::formatNumber($pSheet->getPageMargins()->getLeft()) . 'in; ';
1622 13
        $htmlPage .= 'margin-left: ' . $left;
1623 13
        $htmlBody .= 'margin-left: ' . $left;
1624 13
        $right = StringHelper::formatNumber($pSheet->getPageMargins()->getRight()) . 'in; ';
1625 13
        $htmlPage .= 'margin-right: ' . $right;
1626 13
        $htmlBody .= 'margin-right: ' . $right;
1627 13
        $top = StringHelper::formatNumber($pSheet->getPageMargins()->getTop()) . 'in; ';
1628 13
        $htmlPage .= 'margin-top: ' . $top;
1629 13
        $htmlBody .= 'margin-top: ' . $top;
1630 13
        $bottom = StringHelper::formatNumber($pSheet->getPageMargins()->getBottom()) . 'in; ';
1631 13
        $htmlPage .= 'margin-bottom: ' . $bottom;
1632 13
        $htmlBody .= 'margin-bottom: ' . $bottom;
1633
1634 13
        $htmlPage .= "}\n";
1635 13
        $htmlBody .= "}\n";
1636
1637 13
        return "<style>\n" . $htmlPage . $htmlBody . "</style>\n";
1638
    }
1639
1640
    /**
1641
     * Write a comment in the same format as LibreOffice.
1642
     *
1643
     * @see https://github.com/LibreOffice/core/blob/9fc9bf3240f8c62ad7859947ab8a033ac1fe93fa/sc/source/filter/html/htmlexp.cxx#L1073-L1092
1644
     *
1645
     * @param Worksheet $pSheet
1646
     * @param string $coordinate
1647
     *
1648
     * @return string
1649
     */
1650 13
    private function writeComment(Worksheet $pSheet, $coordinate)
1651
    {
1652 13
        $result = '';
1653 13
        if (!$this->isPdf && isset($pSheet->getComments()[$coordinate])) {
1654 7
            $result .= '<a class="comment-indicator"></a>';
1655 7
            $result .= '<div class="comment">' . nl2br($pSheet->getComment($coordinate)->getText()->getPlainText()) . '</div>';
1656 7
            $result .= PHP_EOL;
1657
        }
1658
1659 13
        return $result;
1660
    }
1661
}
1662