Completed
Push — master ( 4b5c92...896769 )
by Adrien
16:06 queued 09:43
created

Html::generateTableHeader()   B

Complexity

Conditions 11
Paths 60

Size

Total Lines 40
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 11.2682

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 11
eloc 25
c 1
b 0
f 0
nc 60
nop 1
dl 0
loc 40
rs 7.3166
ccs 20
cts 23
cp 0.8696
crap 11.2682

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
     * 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)
141
    {
142 23
        $this->spreadsheet = $spreadsheet;
143 23
        $this->defaultFont = $this->spreadsheet->getDefaultStyle()->getFont();
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)
219
    {
220 18
        switch ($hAlign) {
221
            case Alignment::HORIZONTAL_GENERAL:
222 18
                return false;
223
            case Alignment::HORIZONTAL_LEFT:
224 4
                return 'left';
225
            case Alignment::HORIZONTAL_RIGHT:
226 4
                return 'right';
227
            case Alignment::HORIZONTAL_CENTER:
228
            case Alignment::HORIZONTAL_CENTER_CONTINUOUS:
229 1
                return 'center';
230
            case Alignment::HORIZONTAL_JUSTIFY:
231 4
                return 'justify';
232
            default:
233
                return false;
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)
351
    {
352
        // Construct HTML
353 18
        $properties = $this->spreadsheet->getProperties();
354 18
        $html = '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">' . PHP_EOL;
355 18
        $html .= '<html>' . PHP_EOL;
356 18
        $html .= '  <head>' . PHP_EOL;
357 18
        $html .= '      <meta http-equiv="Content-Type" content="text/html; charset=utf-8">' . PHP_EOL;
358 18
        $html .= '      <meta name="generator" content="PhpSpreadsheet, https://github.com/PHPOffice/PhpSpreadsheet">' . PHP_EOL;
359 18
        if ($properties->getTitle() > '') {
360 18
            $html .= '      <title>' . htmlspecialchars($properties->getTitle()) . '</title>' . PHP_EOL;
361
        }
362 18
        if ($properties->getCreator() > '') {
363 18
            $html .= '      <meta name="author" content="' . htmlspecialchars($properties->getCreator()) . '" />' . PHP_EOL;
364
        }
365 18
        if ($properties->getTitle() > '') {
366 18
            $html .= '      <meta name="title" content="' . htmlspecialchars($properties->getTitle()) . '" />' . PHP_EOL;
367
        }
368 18
        if ($properties->getDescription() > '') {
369 5
            $html .= '      <meta name="description" content="' . htmlspecialchars($properties->getDescription()) . '" />' . PHP_EOL;
370
        }
371 18
        if ($properties->getSubject() > '') {
372 6
            $html .= '      <meta name="subject" content="' . htmlspecialchars($properties->getSubject()) . '" />' . PHP_EOL;
373
        }
374 18
        if ($properties->getKeywords() > '') {
375 6
            $html .= '      <meta name="keywords" content="' . htmlspecialchars($properties->getKeywords()) . '" />' . PHP_EOL;
376
        }
377 18
        if ($properties->getCategory() > '') {
378 6
            $html .= '      <meta name="category" content="' . htmlspecialchars($properties->getCategory()) . '" />' . PHP_EOL;
379
        }
380 18
        if ($properties->getCompany() > '') {
381 18
            $html .= '      <meta name="company" content="' . htmlspecialchars($properties->getCompany()) . '" />' . PHP_EOL;
382
        }
383 18
        if ($properties->getManager() > '') {
384
            $html .= '      <meta name="manager" content="' . htmlspecialchars($properties->getManager()) . '" />' . PHP_EOL;
385
        }
386
387 18
        if ($pIncludeStyles) {
388 12
            $html .= $this->generateStyles(true);
389
        }
390
391 18
        $html .= '  </head>' . PHP_EOL;
392 18
        $html .= '' . PHP_EOL;
393 18
        $html .= '  <body>' . PHP_EOL;
394
395 18
        return $html;
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);
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...
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? (&lt;style&gt; and &lt;/style&gt;)
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());
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

1058
        $borderStyle = $this->mapBorderStyle(/** @scrutinizer ignore-type */ $pStyle->getBorderStyle());
Loading history...
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()
1087
    {
1088
        // Construct HTML
1089 18
        $html = '';
1090 18
        $html .= '  </body>' . PHP_EOL;
1091 18
        $html .= '</html>' . PHP_EOL;
1092
1093 18
        return $html;
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 = '&nbsp;';
1224
1225
            // Cell
1226 18
            if ($cell instanceof Cell) {
1227 18
                $cellData = '';
1228 18
                if ($cell->getParent() === null) {
1229
                    $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

1229
                    $cell->attach(/** @scrutinizer ignore-type */ $pSheet);
Loading history...
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 &nbsp;
1284
                // Example: "  Hello\n to the world" is converted to "&nbsp;&nbsp;Hello\n&nbsp;to the world"
1285 18
                $cellData = preg_replace('/(?m)(?:^|\\G) /', '&nbsp;', $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()]);
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

1297
                            $cssClass = array_merge(/** @scrutinizer ignore-type */ $cssClass, $this->cssStyles['th.style' . $cell->getXfIndex()]);
Loading history...
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;
0 ignored issues
show
Unused Code introduced by
The assignment to $colspan is dead and can be removed.
Loading history...
1326 18
            $rowspan = 1;
0 ignored issues
show
Unused Code introduced by
The assignment to $rowspan is dead and can be removed.
Loading history...
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
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

1345
                    $html .= ' class="' . /** @scrutinizer ignore-type */ $cssClass . '"';
Loading history...
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
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

1368
                    $html .= ' style="' . $this->assembleCSS(/** @scrutinizer ignore-type */ $cssClass) . '"';
Loading history...
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);
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

1569
                [$cells] = Coordinate::splitRange(/** @scrutinizer ignore-type */ $cells);
Loading history...
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)
1676
    {
1677 18
        $result = '';
1678 18
        if (!$this->isPdf && isset($pSheet->getComments()[$coordinate])) {
1679 7
            $result .= '<a class="comment-indicator"></a>';
1680 7
            $result .= '<div class="comment">' . nl2br($pSheet->getComment($coordinate)->getText()->getPlainText()) . '</div>';
1681 7
            $result .= PHP_EOL;
1682
        }
1683
1684 18
        return $result;
1685
    }
1686
}
1687