Completed
Push — master ( a79865...f1a019 )
by Adrien
09:59
created

Html::buildCSS()   F

Complexity

Conditions 18
Paths 10305

Size

Total Lines 160
Code Lines 82

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 62
CRAP Score 18.4838

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 18
eloc 82
c 2
b 0
f 0
nc 10305
nop 1
dl 0
loc 160
ccs 62
cts 70
cp 0.8857
crap 18.4838
rs 0.7

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

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

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

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

1349
                    $html .= ' class="' . /** @scrutinizer ignore-type */ $cssClass . '"';
Loading history...
1350
                } else {
1351
                    //** Necessary redundant code for the sake of \PhpOffice\PhpSpreadsheet\Writer\Pdf **
1352
                    // We must explicitly write the width of the <td> element because TCPDF
1353
                    // does not recognize e.g. <col style="width:42pt">
1354 4
                    $width = 0;
1355 4
                    $i = $colNum - 1;
1356 4
                    $e = $colNum + $colSpan - 1;
1357 4
                    while ($i++ < $e) {
1358 4
                        if (isset($this->columnWidths[$sheetIndex][$i])) {
1359 4
                            $width += $this->columnWidths[$sheetIndex][$i];
1360
                        }
1361
                    }
1362 4
                    $cssClass['width'] = $width . 'pt';
1363
1364
                    // We must also explicitly write the height of the <td> element because TCPDF
1365
                    // does not recognize e.g. <tr style="height:50pt">
1366 4
                    if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $pRow]['height'])) {
1367 1
                        $height = $this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $pRow]['height'];
1368 1
                        $cssClass['height'] = $height;
1369
                    }
1370
                    //** end of redundant code **
1371
1372 4
                    $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

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

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