Failed Conditions
Push — master ( 4e6d68...8ea48e )
by Adrien
11:30 queued 01:31
created

Html::generateHTMLHeader()   F

Complexity

Conditions 11
Paths 1024

Size

Total Lines 46
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 30
CRAP Score 11.004

Importance

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

How to fix   Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Writer;
4
5
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
6
use PhpOffice\PhpSpreadsheet\Cell\Cell;
7
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
8
use PhpOffice\PhpSpreadsheet\Chart\Chart;
9
use PhpOffice\PhpSpreadsheet\RichText\RichText;
10
use PhpOffice\PhpSpreadsheet\RichText\Run;
11
use PhpOffice\PhpSpreadsheet\Shared\Drawing as SharedDrawing;
12
use PhpOffice\PhpSpreadsheet\Shared\File;
13
use PhpOffice\PhpSpreadsheet\Shared\Font as SharedFont;
14
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
15
use PhpOffice\PhpSpreadsheet\Spreadsheet;
16
use PhpOffice\PhpSpreadsheet\Style\Alignment;
17
use PhpOffice\PhpSpreadsheet\Style\Border;
18
use PhpOffice\PhpSpreadsheet\Style\Borders;
19
use PhpOffice\PhpSpreadsheet\Style\Fill;
20
use PhpOffice\PhpSpreadsheet\Style\Font;
21
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
22
use PhpOffice\PhpSpreadsheet\Style\Style;
23
use PhpOffice\PhpSpreadsheet\Worksheet\Drawing;
24
use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing;
25
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
26
use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException;
27
28
class Html extends BaseWriter
29
{
30
    /**
31
     * Spreadsheet object.
32
     *
33
     * @var Spreadsheet
34
     */
35
    protected $spreadsheet;
36
37
    /**
38
     * Sheet index to write.
39
     *
40
     * @var int
41
     */
42
    private $sheetIndex = 0;
43
44
    /**
45
     * Images root.
46
     *
47
     * @var string
48
     */
49
    private $imagesRoot = '';
50
51
    /**
52
     * embed images, or link to images.
53
     *
54
     * @var bool
55
     */
56
    private $embedImages = false;
57
58
    /**
59
     * Use inline CSS?
60
     *
61
     * @var bool
62
     */
63
    private $useInlineCss = false;
64
65
    /**
66
     * 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 15
    public function __construct(Spreadsheet $spreadsheet)
141
    {
142 15
        $this->spreadsheet = $spreadsheet;
143 15
        $this->defaultFont = $this->spreadsheet->getDefaultStyle()->getFont();
144 15
    }
145
146
    /**
147
     * Save Spreadsheet to file.
148
     *
149
     * @param string $pFilename
150
     *
151
     * @throws WriterException
152
     */
153 10
    public function save($pFilename)
154
    {
155
        // garbage collect
156 10
        $this->spreadsheet->garbageCollect();
157
158 10
        $saveDebugLog = Calculation::getInstance($this->spreadsheet)->getDebugLog()->getWriteDebugLog();
159 10
        Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog(false);
160 10
        $saveArrayReturnType = Calculation::getArrayReturnType();
161 10
        Calculation::setArrayReturnType(Calculation::RETURN_ARRAY_AS_VALUE);
162
163
        // Build CSS
164 10
        $this->buildCSS(!$this->useInlineCss);
165
166
        // Open file
167 10
        $fileHandle = fopen($pFilename, 'wb+');
168 10
        if ($fileHandle === false) {
169
            throw new WriterException("Could not open file $pFilename for writing.");
170
        }
171
172
        // Write headers
173 10
        fwrite($fileHandle, $this->generateHTMLHeader(!$this->useInlineCss));
174
175
        // Write navigation (tabs)
176 10
        if ((!$this->isPdf) && ($this->generateSheetNavigationBlock)) {
177 10
            fwrite($fileHandle, $this->generateNavigation());
178
        }
179
180
        // Write data
181 10
        fwrite($fileHandle, $this->generateSheetData());
182
183
        // Write footer
184 10
        fwrite($fileHandle, $this->generateHTMLFooter());
185
186
        // Close file
187 10
        fclose($fileHandle);
188
189 10
        Calculation::setArrayReturnType($saveArrayReturnType);
190 10
        Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog($saveDebugLog);
191 10
    }
192
193
    /**
194
     * Map VAlign.
195
     *
196
     * @param string $vAlign Vertical alignment
197
     *
198
     * @return string
199
     */
200 10
    private function mapVAlign($vAlign)
201
    {
202 10
        switch ($vAlign) {
203
            case Alignment::VERTICAL_BOTTOM:
204 10
                return 'bottom';
205
            case Alignment::VERTICAL_TOP:
206
                return 'top';
207
            case Alignment::VERTICAL_CENTER:
208
            case Alignment::VERTICAL_JUSTIFY:
209 1
                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 10
    private function mapHAlign($hAlign)
223
    {
224 10
        switch ($hAlign) {
225
            case Alignment::HORIZONTAL_GENERAL:
226 10
                return false;
227
            case Alignment::HORIZONTAL_LEFT:
228 1
                return 'left';
229
            case Alignment::HORIZONTAL_RIGHT:
230 1
                return 'right';
231
            case Alignment::HORIZONTAL_CENTER:
232
            case Alignment::HORIZONTAL_CENTER_CONTINUOUS:
233
                return 'center';
234
            case Alignment::HORIZONTAL_JUSTIFY:
235 1
                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 10
    private function mapBorderStyle($borderStyle)
249
    {
250 10
        switch ($borderStyle) {
251
            case Border::BORDER_NONE:
252 10
                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 1
                return '3px solid';
277
            case Border::BORDER_THIN:
278 1
                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
    public function getSheetIndex()
291
    {
292
        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 10
    public function generateHTMLHeader($pIncludeStyles = false)
355
    {
356
        // Construct HTML
357 10
        $properties = $this->spreadsheet->getProperties();
358 10
        $html = '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">' . PHP_EOL;
359 10
        $html .= '<html>' . PHP_EOL;
360 10
        $html .= '  <head>' . PHP_EOL;
361 10
        $html .= '      <meta http-equiv="Content-Type" content="text/html; charset=utf-8">' . PHP_EOL;
362 10
        $html .= '      <meta name="generator" content="PhpSpreadsheet, https://github.com/PHPOffice/PhpSpreadsheet">' . PHP_EOL;
363 10
        if ($properties->getTitle() > '') {
364 10
            $html .= '      <title>' . htmlspecialchars($properties->getTitle()) . '</title>' . PHP_EOL;
365
        }
366 10
        if ($properties->getCreator() > '') {
367 10
            $html .= '      <meta name="author" content="' . htmlspecialchars($properties->getCreator()) . '" />' . PHP_EOL;
368
        }
369 10
        if ($properties->getTitle() > '') {
370 10
            $html .= '      <meta name="title" content="' . htmlspecialchars($properties->getTitle()) . '" />' . PHP_EOL;
371
        }
372 10
        if ($properties->getDescription() > '') {
373 2
            $html .= '      <meta name="description" content="' . htmlspecialchars($properties->getDescription()) . '" />' . PHP_EOL;
374
        }
375 10
        if ($properties->getSubject() > '') {
376 2
            $html .= '      <meta name="subject" content="' . htmlspecialchars($properties->getSubject()) . '" />' . PHP_EOL;
377
        }
378 10
        if ($properties->getKeywords() > '') {
379 2
            $html .= '      <meta name="keywords" content="' . htmlspecialchars($properties->getKeywords()) . '" />' . PHP_EOL;
380
        }
381 10
        if ($properties->getCategory() > '') {
382 2
            $html .= '      <meta name="category" content="' . htmlspecialchars($properties->getCategory()) . '" />' . PHP_EOL;
383
        }
384 10
        if ($properties->getCompany() > '') {
385 10
            $html .= '      <meta name="company" content="' . htmlspecialchars($properties->getCompany()) . '" />' . PHP_EOL;
386
        }
387 10
        if ($properties->getManager() > '') {
388
            $html .= '      <meta name="manager" content="' . htmlspecialchars($properties->getManager()) . '" />' . PHP_EOL;
389
        }
390
391 10
        if ($pIncludeStyles) {
392 10
            $html .= $this->generateStyles(true);
393
        }
394
395 10
        $html .= '  </head>' . PHP_EOL;
396 10
        $html .= '' . PHP_EOL;
397 10
        $html .= '  <body>' . PHP_EOL;
398
399 10
        return $html;
400
    }
401
402
    /**
403
     * Generate sheet data.
404
     *
405
     * @throws WriterException
406
     *
407
     * @return string
408
     */
409 10
    public function generateSheetData()
410
    {
411
        // Ensure that Spans have been calculated?
412 10
        if ($this->sheetIndex !== null || !$this->spansAreCalculated) {
413 10
            $this->calculateSpans();
414
        }
415
416
        // Fetch sheets
417 10
        $sheets = [];
418 10
        if ($this->sheetIndex === null) {
419
            $sheets = $this->spreadsheet->getAllSheets();
420
        } else {
421 10
            $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex);
422
        }
423
424
        // Construct HTML
425 10
        $html = '';
426
427
        // Loop all sheets
428 10
        $sheetId = 0;
429 10
        foreach ($sheets as $sheet) {
430
            // Write table header
431 10
            $html .= $this->generateTableHeader($sheet);
432
433
            // Get worksheet dimension
434 10
            $dimension = explode(':', $sheet->calculateWorksheetDimension());
435 10
            $dimension[0] = Coordinate::coordinateFromString($dimension[0]);
436 10
            $dimension[0][0] = Coordinate::columnIndexFromString($dimension[0][0]);
437 10
            $dimension[1] = Coordinate::coordinateFromString($dimension[1]);
438 10
            $dimension[1][0] = Coordinate::columnIndexFromString($dimension[1][0]);
439
440
            // row min,max
441 10
            $rowMin = $dimension[0][1];
442 10
            $rowMax = $dimension[1][1];
443
444
            // calculate start of <tbody>, <thead>
445 10
            $tbodyStart = $rowMin;
446 10
            $theadStart = $theadEnd = 0; // default: no <thead>    no </thead>
447 10
            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 10
            $row = $rowMin - 1;
460 10
            while ($row++ < $rowMax) {
461
                // <thead> ?
462 10
                if ($row == $theadStart) {
463
                    $html .= '        <thead>' . PHP_EOL;
464
                    $cellType = 'th';
465
                }
466
467
                // <tbody> ?
468 10
                if ($row == $tbodyStart) {
469 10
                    $html .= '        <tbody>' . PHP_EOL;
470 10
                    $cellType = 'td';
471
                }
472
473
                // Write row if there are HTML table cells in it
474 10
                if (!isset($this->isSpannedRow[$sheet->getParent()->getIndex($sheet)][$row])) {
475
                    // Start a new rowData
476 10
                    $rowData = [];
477
                    // Loop through columns
478 10
                    $column = $dimension[0][0];
479 10
                    while ($column <= $dimension[1][0]) {
480
                        // Cell exists?
481 10
                        if ($sheet->cellExistsByColumnAndRow($column, $row)) {
482 10
                            $rowData[$column] = Coordinate::stringFromColumnIndex($column) . $row;
483
                        } else {
484 3
                            $rowData[$column] = '';
485
                        }
486 10
                        ++$column;
487
                    }
488 10
                    $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 10
                if ($row == $theadEnd) {
493
                    $html .= '        </thead>' . PHP_EOL;
494
                }
495
            }
496 10
            $html .= $this->extendRowsForChartsAndImages($sheet, $row);
497
498
            // Close table body.
499 10
            $html .= '        </tbody>' . PHP_EOL;
500
501
            // Write table footer
502 10
            $html .= $this->generateTableFooter();
503
504
            // Writing PDF?
505 10
            if ($this->isPdf) {
506
                if ($this->sheetIndex === null && $sheetId + 1 < $this->spreadsheet->getSheetCount()) {
507
                    $html .= '<div style="page-break-before:always" />';
508
                }
509
            }
510
511
            // Next sheet
512 10
            ++$sheetId;
513
        }
514
515 10
        return $html;
516
    }
517
518
    /**
519
     * Generate sheet tabs.
520
     *
521
     * @throws WriterException
522
     *
523
     * @return string
524
     */
525 10
    public function generateNavigation()
526
    {
527
        // Fetch sheets
528 10
        $sheets = [];
529 10
        if ($this->sheetIndex === null) {
530
            $sheets = $this->spreadsheet->getAllSheets();
531
        } else {
532 10
            $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex);
533
        }
534
535
        // Construct HTML
536 10
        $html = '';
537
538
        // Only if there are more than 1 sheets
539 10
        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 10
        return $html;
554
    }
555
556 10
    private function extendRowsForChartsAndImages(Worksheet $pSheet, $row)
557
    {
558 10
        $rowMax = $row;
559 10
        $colMax = 'A';
560 10
        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 10
        foreach ($pSheet->getDrawingCollection() as $drawing) {
577 2
            if ($drawing instanceof Drawing) {
578 1
                $imageTL = Coordinate::coordinateFromString($drawing->getCoordinates());
579 1
                $imageCol = Coordinate::columnIndexFromString($imageTL[0]);
580 1
                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 10
        if ($row === $rowMax) {
591 10
            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 10
    private function writeImageInCell(Worksheet $pSheet, $coordinates)
623
    {
624
        // Construct HTML
625 10
        $html = '';
626
627
        // Write images
628 10
        foreach ($pSheet->getDrawingCollection() as $drawing) {
629 2
            if ($drawing instanceof Drawing) {
630 1
                if ($drawing->getCoordinates() == $coordinates) {
631 1
                    $filename = $drawing->getPath();
632
633
                    // Strip off eventual '.'
634 1
                    if (substr($filename, 0, 1) == '.') {
635
                        $filename = substr($filename, 1);
636
                    }
637
638
                    // Prepend images root
639 1
                    $filename = $this->getImagesRoot() . $filename;
640
641
                    // Strip off eventual '.'
642 1
                    if (substr($filename, 0, 1) == '.' && substr($filename, 0, 2) != './') {
643
                        $filename = substr($filename, 1);
644
                    }
645
646
                    // Convert UTF8 data to PCDATA
647 1
                    $filename = htmlspecialchars($filename);
648
649 1
                    $html .= PHP_EOL;
650 1
                    if ((!$this->embedImages) || ($this->isPdf)) {
651 1
                        $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 1
                    $html .= '<div style="position: relative;">';
670
                    $html .= '<img style="position: absolute; z-index: 1; left: ' .
671 1
                        $drawing->getOffsetX() . 'px; top: ' . $drawing->getOffsetY() . 'px; width: ' .
672 1
                        $drawing->getWidth() . 'px; height: ' . $drawing->getHeight() . 'px;" src="' .
673 1
                        $imageData . '" border="0" />';
674 1
                    $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 10
        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 10
    public function generateStyles($generateSurroundingHTML = true)
755
    {
756
        // Build CSS
757 10
        $css = $this->buildCSS($generateSurroundingHTML);
758
759
        // Construct HTML
760 10
        $html = '';
761
762
        // Start styles
763 10
        if ($generateSurroundingHTML) {
764 10
            $html .= '    <style type="text/css">' . PHP_EOL;
765 10
            $html .= '      html { ' . $this->assembleCSS($css['html']) . ' }' . PHP_EOL;
766
        }
767
768
        // Write all other styles
769 10
        foreach ($css as $styleName => $styleDefinition) {
770 10
            if ($styleName != 'html') {
771 10
                $html .= '      ' . $styleName . ' { ' . $this->assembleCSS($styleDefinition) . ' }' . PHP_EOL;
772
            }
773
        }
774
775
        // End styles
776 10
        if ($generateSurroundingHTML) {
777 10
            $html .= '    </style>' . PHP_EOL;
778
        }
779
780
        // Return
781 10
        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 10
    public function buildCSS($generateSurroundingHTML = true)
794
    {
795
        // Cached?
796 10
        if ($this->cssStyles !== null) {
797 10
            return $this->cssStyles;
798
        }
799
800
        // Ensure that spans have been calculated
801 10
        if (!$this->spansAreCalculated) {
802 10
            $this->calculateSpans();
803
        }
804
805
        // Construct CSS
806 10
        $css = [];
807
808
        // Start styles
809 10
        if ($generateSurroundingHTML) {
810
            // html { }
811 10
            $css['html']['font-family'] = 'Calibri, Arial, Helvetica, sans-serif';
812 10
            $css['html']['font-size'] = '11pt';
813 10
            $css['html']['background-color'] = 'white';
814
        }
815
816
        // CSS for comments as found in LibreOffice
817 10
        $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 10
        $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 10
        $css['div.comment']['display'] = 'none';
834
835
        // table { }
836 10
        $css['table']['border-collapse'] = 'collapse';
837 10
        if (!$this->isPdf) {
838 10
            $css['table']['page-break-after'] = 'always';
839
        }
840
841
        // .gridlines td { }
842 10
        $css['.gridlines td']['border'] = '1px dotted black';
843 10
        $css['.gridlines th']['border'] = '1px dotted black';
844
845
        // .b {}
846 10
        $css['.b']['text-align'] = 'center'; // BOOL
847
848
        // .e {}
849 10
        $css['.e']['text-align'] = 'center'; // ERROR
850
851
        // .f {}
852 10
        $css['.f']['text-align'] = 'right'; // FORMULA
853
854
        // .inlineStr {}
855 10
        $css['.inlineStr']['text-align'] = 'left'; // INLINE
856
857
        // .n {}
858 10
        $css['.n']['text-align'] = 'right'; // NUMERIC
859
860
        // .s {}
861 10
        $css['.s']['text-align'] = 'left'; // STRING
862
863
        // Calculate cell style hashes
864 10
        foreach ($this->spreadsheet->getCellXfCollection() as $index => $style) {
865 10
            $css['td.style' . $index] = $this->createCSSStyle($style);
866 10
            $css['th.style' . $index] = $this->createCSSStyle($style);
867
        }
868
869
        // Fetch sheets
870 10
        $sheets = [];
871 10
        if ($this->sheetIndex === null) {
872
            $sheets = $this->spreadsheet->getAllSheets();
873
        } else {
874 10
            $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex);
875
        }
876
877
        // Build styles per sheet
878 10
        foreach ($sheets as $sheet) {
879
            // Calculate hash code
880 10
            $sheetIndex = $sheet->getParent()->getIndex($sheet);
881
882
            // Build styles
883
            // Calculate column widths
884 10
            $sheet->calculateColumnWidths();
885
886
            // col elements, initialize
887 10
            $highestColumnIndex = Coordinate::columnIndexFromString($sheet->getHighestColumn()) - 1;
888 10
            $column = -1;
889 10
            while ($column++ < $highestColumnIndex) {
890 10
                $this->columnWidths[$sheetIndex][$column] = 42; // approximation
891 10
                $css['table.sheet' . $sheetIndex . ' col.col' . $column]['width'] = '42pt';
892
            }
893
894
            // col elements, loop through columnDimensions and set width
895 10
            foreach ($sheet->getColumnDimensions() as $columnDimension) {
896 1
                if (($width = SharedDrawing::cellDimensionToPixels($columnDimension->getWidth(), $this->defaultFont)) >= 0) {
897 1
                    $width = SharedDrawing::pixelsToPoints($width);
898 1
                    $column = Coordinate::columnIndexFromString($columnDimension->getColumnIndex()) - 1;
899 1
                    $this->columnWidths[$sheetIndex][$column] = $width;
900 1
                    $css['table.sheet' . $sheetIndex . ' col.col' . $column]['width'] = $width . 'pt';
901
902 1
                    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 10
            $rowDimension = $sheet->getDefaultRowDimension();
911
912
            // table.sheetN tr { }
913 10
            $css['table.sheet' . $sheetIndex . ' tr'] = [];
914
915 10
            if ($rowDimension->getRowHeight() == -1) {
916 10
                $pt_height = SharedFont::getDefaultRowHeightByFont($this->spreadsheet->getDefaultStyle()->getFont());
917
            } else {
918
                $pt_height = $rowDimension->getRowHeight();
919
            }
920 10
            $css['table.sheet' . $sheetIndex . ' tr']['height'] = $pt_height . 'pt';
921 10
            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 10
            foreach ($sheet->getRowDimensions() as $rowDimension) {
928 1
                $row = $rowDimension->getRowIndex() - 1;
929
930
                // table.sheetN tr.rowYYYYYY { }
931 1
                $css['table.sheet' . $sheetIndex . ' tr.row' . $row] = [];
932
933 1
                if ($rowDimension->getRowHeight() == -1) {
934 1
                    $pt_height = SharedFont::getDefaultRowHeightByFont($this->spreadsheet->getDefaultStyle()->getFont());
935
                } else {
936
                    $pt_height = $rowDimension->getRowHeight();
937
                }
938 1
                $css['table.sheet' . $sheetIndex . ' tr.row' . $row]['height'] = $pt_height . 'pt';
939 1
                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 10
        if ($this->cssStyles === null) {
948 10
            $this->cssStyles = $css;
949
        }
950
951
        // Return
952 10
        return $css;
953
    }
954
955
    /**
956
     * Create CSS style.
957
     *
958
     * @param Style $pStyle
959
     *
960
     * @return array
961
     */
962 10
    private function createCSSStyle(Style $pStyle)
963
    {
964
        // Create CSS
965 10
        return array_merge(
966 10
            $this->createCSSStyleAlignment($pStyle->getAlignment()),
967 10
            $this->createCSSStyleBorders($pStyle->getBorders()),
968 10
            $this->createCSSStyleFont($pStyle->getFont()),
969 10
            $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 10
    private function createCSSStyleAlignment(Alignment $pStyle)
981
    {
982
        // Construct CSS
983 10
        $css = [];
984
985
        // Create CSS
986 10
        $css['vertical-align'] = $this->mapVAlign($pStyle->getVertical());
987 10
        if ($textAlign = $this->mapHAlign($pStyle->getHorizontal())) {
988 1
            $css['text-align'] = $textAlign;
989 1
            if (in_array($textAlign, ['left', 'right'])) {
990 1
                $css['padding-' . $textAlign] = (string) ((int) $pStyle->getIndent() * 9) . 'px';
991
            }
992
        }
993
994 10
        return $css;
995
    }
996
997
    /**
998
     * Create CSS style (\PhpOffice\PhpSpreadsheet\Style\Font).
999
     *
1000
     * @param Font $pStyle
1001
     *
1002
     * @return array
1003
     */
1004 10
    private function createCSSStyleFont(Font $pStyle)
1005
    {
1006
        // Construct CSS
1007 10
        $css = [];
1008
1009
        // Create CSS
1010 10
        if ($pStyle->getBold()) {
1011 1
            $css['font-weight'] = 'bold';
1012
        }
1013 10
        if ($pStyle->getUnderline() != Font::UNDERLINE_NONE && $pStyle->getStrikethrough()) {
1014
            $css['text-decoration'] = 'underline line-through';
1015 10
        } elseif ($pStyle->getUnderline() != Font::UNDERLINE_NONE) {
1016 1
            $css['text-decoration'] = 'underline';
1017 10
        } elseif ($pStyle->getStrikethrough()) {
1018
            $css['text-decoration'] = 'line-through';
1019
        }
1020 10
        if ($pStyle->getItalic()) {
1021 1
            $css['font-style'] = 'italic';
1022
        }
1023
1024 10
        $css['color'] = '#' . $pStyle->getColor()->getRGB();
1025 10
        $css['font-family'] = '\'' . $pStyle->getName() . '\'';
1026 10
        $css['font-size'] = $pStyle->getSize() . 'pt';
1027
1028 10
        return $css;
1029
    }
1030
1031
    /**
1032
     * Create CSS style (Borders).
1033
     *
1034
     * @param Borders $pStyle Borders
1035
     *
1036
     * @return array
1037
     */
1038 10
    private function createCSSStyleBorders(Borders $pStyle)
1039
    {
1040
        // Construct CSS
1041 10
        $css = [];
1042
1043
        // Create CSS
1044 10
        $css['border-bottom'] = $this->createCSSStyleBorder($pStyle->getBottom());
1045 10
        $css['border-top'] = $this->createCSSStyleBorder($pStyle->getTop());
1046 10
        $css['border-left'] = $this->createCSSStyleBorder($pStyle->getLeft());
1047 10
        $css['border-right'] = $this->createCSSStyleBorder($pStyle->getRight());
1048
1049 10
        return $css;
1050
    }
1051
1052
    /**
1053
     * Create CSS style (Border).
1054
     *
1055
     * @param Border $pStyle Border
1056
     *
1057
     * @return string
1058
     */
1059 10
    private function createCSSStyleBorder(Border $pStyle)
1060
    {
1061
        //    Create CSS - add !important to non-none border styles for merged cells
1062 10
        $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 10
        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 10
    private function createCSSStyleFill(Fill $pStyle)
1075
    {
1076
        // Construct HTML
1077 10
        $css = [];
1078
1079
        // Create CSS
1080 10
        $value = $pStyle->getFillType() == Fill::FILL_NONE ?
1081 10
            'white' : '#' . $pStyle->getStartColor()->getRGB();
1082 10
        $css['background-color'] = $value;
1083
1084 10
        return $css;
1085
    }
1086
1087
    /**
1088
     * Generate HTML footer.
1089
     */
1090 10
    public function generateHTMLFooter()
1091
    {
1092
        // Construct HTML
1093 10
        $html = '';
1094 10
        $html .= '  </body>' . PHP_EOL;
1095 10
        $html .= '</html>' . PHP_EOL;
1096
1097 10
        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 10
    private function generateTableHeader($pSheet)
1108
    {
1109 10
        $sheetIndex = $pSheet->getParent()->getIndex($pSheet);
1110
1111
        // Construct HTML
1112 10
        $html = '';
1113 10
        if ($this->useEmbeddedCSS) {
1114 10
            $html .= $this->setMargins($pSheet);
1115
        }
1116
1117 10
        if (!$this->useInlineCss) {
1118 10
            $gridlines = $pSheet->getShowGridlines() ? ' gridlines' : '';
1119 10
            $html .= '    <table border="0" cellpadding="0" cellspacing="0" id="sheet' . $sheetIndex . '" class="sheet' . $sheetIndex . $gridlines . '">' . PHP_EOL;
1120
        } else {
1121
            $style = isset($this->cssStyles['table']) ?
1122
                $this->assembleCSS($this->cssStyles['table']) : '';
1123
1124
            if ($this->isPdf && $pSheet->getShowGridlines()) {
1125
                $html .= '    <table border="1" cellpadding="1" id="sheet' . $sheetIndex . '" cellspacing="1" style="' . $style . '">' . PHP_EOL;
1126
            } else {
1127
                $html .= '    <table border="0" cellpadding="1" id="sheet' . $sheetIndex . '" cellspacing="0" style="' . $style . '">' . PHP_EOL;
1128
            }
1129
        }
1130
1131
        // Write <col> elements
1132 10
        $highestColumnIndex = Coordinate::columnIndexFromString($pSheet->getHighestColumn()) - 1;
1133 10
        $i = -1;
1134 10
        while ($i++ < $highestColumnIndex) {
1135 10
            if (!$this->isPdf) {
1136 10
                if (!$this->useInlineCss) {
1137 10
                    $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 10
        return $html;
1147
    }
1148
1149
    /**
1150
     * Generate table footer.
1151
     */
1152 10
    private function generateTableFooter()
1153
    {
1154 10
        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 10
    private function generateRow(Worksheet $pSheet, array $pValues, $pRow, $cellType)
1170
    {
1171
        // Construct HTML
1172 10
        $html = '';
1173
1174
        // Sheet index
1175 10
        $sheetIndex = $pSheet->getParent()->getIndex($pSheet);
1176
1177
        // Dompdf and breaks
1178 10
        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 10
        if (!$this->useInlineCss) {
1196 10
            $html .= '          <tr class="row' . $pRow . '">' . PHP_EOL;
1197
        } else {
1198
            $style = isset($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $pRow])
1199
                ? $this->assembleCSS($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $pRow]) : '';
1200
1201
            $html .= '          <tr style="' . $style . '">' . PHP_EOL;
1202
        }
1203
1204
        // Write cells
1205 10
        $colNum = 0;
1206 10
        foreach ($pValues as $cellAddress) {
1207 10
            $cell = ($cellAddress > '') ? $pSheet->getCell($cellAddress) : '';
1208 10
            $coordinate = Coordinate::stringFromColumnIndex($colNum + 1) . ($pRow + 1);
1209 10
            if (!$this->useInlineCss) {
1210 10
                $cssClass = 'column' . $colNum;
1211
            } else {
1212
                $cssClass = [];
1213
                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
                    if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' td.column' . $colNum])) {
1219
                        $this->cssStyles['table.sheet' . $sheetIndex . ' td.column' . $colNum];
1220
                    }
1221
                }
1222
            }
1223 10
            $colSpan = 1;
1224 10
            $rowSpan = 1;
1225
1226
            // initialize
1227 10
            $cellData = '&nbsp;';
1228
1229
            // Cell
1230 10
            if ($cell instanceof Cell) {
1231 10
                $cellData = '';
1232 10
                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 10
                if ($cell->getValue() instanceof RichText) {
1237
                    // Loop through rich text elements
1238 1
                    $elements = $cell->getValue()->getRichTextElements();
1239 1
                    foreach ($elements as $element) {
1240
                        // Rich text start?
1241 1
                        if ($element instanceof Run) {
1242 1
                            $cellData .= '<span style="' . $this->assembleCSS($this->createCSSStyleFont($element->getFont())) . '">';
1243
1244 1
                            if ($element->getFont()->getSuperscript()) {
1245
                                $cellData .= '<sup>';
1246 1
                            } elseif ($element->getFont()->getSubscript()) {
1247
                                $cellData .= '<sub>';
1248
                            }
1249
                        }
1250
1251
                        // Convert UTF8 data to PCDATA
1252 1
                        $cellText = $element->getText();
1253 1
                        $cellData .= htmlspecialchars($cellText);
1254
1255 1
                        if ($element instanceof Run) {
1256 1
                            if ($element->getFont()->getSuperscript()) {
1257
                                $cellData .= '</sup>';
1258 1
                            } elseif ($element->getFont()->getSubscript()) {
1259
                                $cellData .= '</sub>';
1260
                            }
1261
1262 1
                            $cellData .= '</span>';
1263
                        }
1264
                    }
1265
                } else {
1266 10
                    if ($this->preCalculateFormulas) {
1267 10
                        $cellData = NumberFormat::toFormattedString(
1268 10
                            $cell->getCalculatedValue(),
1269 10
                            $pSheet->getParent()->getCellXfByIndex($cell->getXfIndex())->getNumberFormat()->getFormatCode(),
1270 10
                            [$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 10
                    $cellData = htmlspecialchars($cellData);
1280 10
                    if ($pSheet->getParent()->getCellXfByIndex($cell->getXfIndex())->getFont()->getSuperscript()) {
1281
                        $cellData = '<sup>' . $cellData . '</sup>';
1282 10
                    } 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 10
                $cellData = preg_replace('/(?m)(?:^|\\G) /', '&nbsp;', $cellData);
1290
1291
                // convert newline "\n" to '<br>'
1292 10
                $cellData = nl2br($cellData);
1293
1294
                // Extend CSS class?
1295 10
                if (!$this->useInlineCss) {
1296 10
                    $cssClass .= ' style' . $cell->getXfIndex();
1297 10
                    $cssClass .= ' ' . $cell->getDataType();
1298
                } else {
1299
                    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
                        if (isset($this->cssStyles['td.style' . $cell->getXfIndex()])) {
1305
                            $cssClass = array_merge($cssClass, $this->cssStyles['td.style' . $cell->getXfIndex()]);
1306
                        }
1307
                    }
1308
1309
                    // General horizontal alignment: Actual horizontal alignment depends on dataType
1310
                    $sharedStyle = $pSheet->getParent()->getCellXfByIndex($cell->getXfIndex());
1311
                    if ($sharedStyle->getAlignment()->getHorizontal() == Alignment::HORIZONTAL_GENERAL
1312
                        && isset($this->cssStyles['.' . $cell->getDataType()]['text-align'])
1313
                    ) {
1314
                        $cssClass['text-align'] = $this->cssStyles['.' . $cell->getDataType()]['text-align'];
1315
                    }
1316
                }
1317
            }
1318
1319
            // Hyperlink?
1320 10
            if ($pSheet->hyperlinkExists($coordinate) && !$pSheet->getHyperlink($coordinate)->isInternal()) {
1321 1
                $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 10
            $writeCell = !(isset($this->isSpannedCell[$pSheet->getParent()->getIndex($pSheet)][$pRow + 1][$colNum])
1326 10
                && $this->isSpannedCell[$pSheet->getParent()->getIndex($pSheet)][$pRow + 1][$colNum]);
1327
1328
            // Colspan and Rowspan
1329 10
            $colspan = 1;
0 ignored issues
show
Unused Code introduced by
The assignment to $colspan is dead and can be removed.
Loading history...
1330 10
            $rowspan = 1;
0 ignored issues
show
Unused Code introduced by
The assignment to $rowspan is dead and can be removed.
Loading history...
1331 10
            if (isset($this->isBaseCell[$pSheet->getParent()->getIndex($pSheet)][$pRow + 1][$colNum])) {
1332 2
                $spans = $this->isBaseCell[$pSheet->getParent()->getIndex($pSheet)][$pRow + 1][$colNum];
1333 2
                $rowSpan = $spans['rowspan'];
1334 2
                $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 2
                $endCellCoord = Coordinate::stringFromColumnIndex($colNum + $colSpan) . ($pRow + $rowSpan);
1339 2
                if (!$this->useInlineCss) {
1340 2
                    $cssClass .= ' style' . $pSheet->getCell($endCellCoord)->getXfIndex();
1341
                }
1342
            }
1343
1344
            // Write
1345 10
            if ($writeCell) {
1346
                // Column start
1347 10
                $html .= '            <' . $cellType;
1348 10
                if (!$this->useInlineCss) {
1349 10
                    $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
                    $width = 0;
1355
                    $i = $colNum - 1;
1356
                    $e = $colNum + $colSpan - 1;
1357
                    while ($i++ < $e) {
1358
                        if (isset($this->columnWidths[$sheetIndex][$i])) {
1359
                            $width += $this->columnWidths[$sheetIndex][$i];
1360
                        }
1361
                    }
1362
                    $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
                    if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $pRow]['height'])) {
1367
                        $height = $this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $pRow]['height'];
1368
                        $cssClass['height'] = $height;
1369
                    }
1370
                    //** end of redundant code **
1371
1372
                    $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 10
                if ($colSpan > 1) {
1375 2
                    $html .= ' colspan="' . $colSpan . '"';
1376
                }
1377 10
                if ($rowSpan > 1) {
1378
                    $html .= ' rowspan="' . $rowSpan . '"';
1379
                }
1380 10
                $html .= '>';
1381
1382 10
                $html .= $this->writeComment($pSheet, $coordinate);
1383
1384
                // Image?
1385 10
                $html .= $this->writeImageInCell($pSheet, $coordinate);
1386
1387
                // Chart?
1388 10
                if ($this->includeCharts) {
1389
                    $html .= $this->writeChartInCell($pSheet, $coordinate);
1390
                }
1391
1392
                // Cell data
1393 10
                $html .= $cellData;
1394
1395
                // Column end
1396 10
                $html .= '</' . $cellType . '>' . PHP_EOL;
1397
            }
1398
1399
            // Next column
1400 10
            ++$colNum;
1401
        }
1402
1403
        // Write row end
1404 10
        $html .= '          </tr>' . PHP_EOL;
1405
1406
        // Return
1407 10
        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 10
    private function assembleCSS(array $pValue = [])
1418
    {
1419 10
        $pairs = [];
1420 10
        foreach ($pValue as $property => $value) {
1421 10
            $pairs[] = $property . ':' . $value;
1422
        }
1423 10
        $string = implode('; ', $pairs);
1424
1425 10
        return $string;
1426
    }
1427
1428
    /**
1429
     * Get images root.
1430
     *
1431
     * @return string
1432
     */
1433 1
    public function getImagesRoot()
1434
    {
1435 1
        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 4
    public function setUseInlineCss($pValue)
1494
    {
1495 4
        $this->useInlineCss = $pValue;
1496
1497 4
        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 1
    public function formatColor($pValue, $pFormat)
1533
    {
1534
        // Color information, e.g. [Red] is always at the beginning
1535 1
        $color = null; // initialize
1536 1
        $matches = [];
1537
1538 1
        $color_regex = '/^\\[[a-zA-Z]+\\]/';
1539 1
        if (preg_match($color_regex, $pFormat, $matches)) {
1540
            $color = str_replace(['[', ']'], '', $matches[0]);
1541
            $color = strtolower($color);
1542
        }
1543
1544
        // convert to PCDATA
1545 1
        $value = htmlspecialchars($pValue);
1546
1547
        // color span tag
1548 1
        if ($color !== null) {
1549
            $value = '<span style="color:' . $color . '">' . $value . '</span>';
1550
        }
1551
1552 1
        return $value;
1553
    }
1554
1555
    /**
1556
     * Calculate information about HTML colspan and rowspan which is not always the same as Excel's.
1557
     */
1558 10
    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 10
        $sheetIndexes = $this->sheetIndex !== null ?
1564 10
            [$this->sheetIndex] : range(0, $this->spreadsheet->getSheetCount() - 1);
1565
1566 10
        foreach ($sheetIndexes as $sheetIndex) {
1567 10
            $sheet = $this->spreadsheet->getSheet($sheetIndex);
1568
1569 10
            $candidateSpannedRow = [];
1570
1571
            // loop through all Excel merged cells
1572 10
            foreach ($sheet->getMergeCells() as $cells) {
1573 2
                [$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 2
                $first = $cells[0];
1575 2
                $last = $cells[1];
1576
1577 2
                [$fc, $fr] = Coordinate::coordinateFromString($first);
1578 2
                $fc = Coordinate::columnIndexFromString($fc) - 1;
1579
1580 2
                [$lc, $lr] = Coordinate::coordinateFromString($last);
1581 2
                $lc = Coordinate::columnIndexFromString($lc) - 1;
1582
1583
                // loop through the individual cells in the individual merge
1584 2
                $r = $fr - 1;
1585 2
                while ($r++ < $lr) {
1586
                    // also, flag this row as a HTML row that is candidate to be omitted
1587 2
                    $candidateSpannedRow[$r] = $r;
1588
1589 2
                    $c = $fc - 1;
1590 2
                    while ($c++ < $lc) {
1591 2
                        if (!($c == $fc && $r == $fr)) {
1592
                            // not the upper-left cell (should not be written in HTML)
1593 2
                            $this->isSpannedCell[$sheetIndex][$r][$c] = [
1594 2
                                'baseCell' => [$fr, $fc],
1595
                            ];
1596
                        } else {
1597
                            // upper-left is the base cell that should hold the colspan/rowspan attribute
1598 2
                            $this->isBaseCell[$sheetIndex][$r][$c] = [
1599 2
                                'xlrowspan' => $lr - $fr + 1, // Excel rowspan
1600 2
                                'rowspan' => $lr - $fr + 1, // HTML rowspan, value may change
1601 2
                                'xlcolspan' => $lc - $fc + 1, // Excel colspan
1602 2
                                '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 10
            $countColumns = Coordinate::columnIndexFromString($sheet->getHighestColumn());
1612 10
            foreach ($candidateSpannedRow as $rowIndex) {
1613 2
                if (isset($this->isSpannedCell[$sheetIndex][$rowIndex])) {
1614 2
                    if (count($this->isSpannedCell[$sheetIndex][$rowIndex]) == $countColumns) {
1615 1
                        $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 10
            if (isset($this->isSpannedRow[$sheetIndex])) {
1622 1
                foreach ($this->isSpannedRow[$sheetIndex] as $rowIndex) {
1623 1
                    $adjustedBaseCells = [];
1624 1
                    $c = -1;
1625 1
                    $e = $countColumns - 1;
1626 1
                    while ($c++ < $e) {
1627 1
                        $baseCell = $this->isSpannedCell[$sheetIndex][$rowIndex][$c]['baseCell'];
1628
1629 1
                        if (!in_array($baseCell, $adjustedBaseCells)) {
1630
                            // subtract rowspan by 1
1631 1
                            --$this->isBaseCell[$sheetIndex][$baseCell[0]][$baseCell[1]]['rowspan'];
1632 1
                            $adjustedBaseCells[] = $baseCell;
1633
                        }
1634
                    }
1635
                }
1636
            }
1637
1638
            // TODO: Same for columns
1639
        }
1640
1641
        // We have calculated the spans
1642 10
        $this->spansAreCalculated = true;
1643 10
    }
1644
1645 10
    private function setMargins(Worksheet $pSheet)
1646
    {
1647 10
        $htmlPage = '@page { ';
1648 10
        $htmlBody = 'body { ';
1649
1650 10
        $left = StringHelper::formatNumber($pSheet->getPageMargins()->getLeft()) . 'in; ';
1651 10
        $htmlPage .= 'margin-left: ' . $left;
1652 10
        $htmlBody .= 'margin-left: ' . $left;
1653 10
        $right = StringHelper::formatNumber($pSheet->getPageMargins()->getRight()) . 'in; ';
1654 10
        $htmlPage .= 'margin-right: ' . $right;
1655 10
        $htmlBody .= 'margin-right: ' . $right;
1656 10
        $top = StringHelper::formatNumber($pSheet->getPageMargins()->getTop()) . 'in; ';
1657 10
        $htmlPage .= 'margin-top: ' . $top;
1658 10
        $htmlBody .= 'margin-top: ' . $top;
1659 10
        $bottom = StringHelper::formatNumber($pSheet->getPageMargins()->getBottom()) . 'in; ';
1660 10
        $htmlPage .= 'margin-bottom: ' . $bottom;
1661 10
        $htmlBody .= 'margin-bottom: ' . $bottom;
1662
1663 10
        $htmlPage .= "}\n";
1664 10
        $htmlBody .= "}\n";
1665
1666 10
        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 10
    private function writeComment(Worksheet $pSheet, $coordinate)
1680
    {
1681 10
        $result = '';
1682 10
        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 10
        return $result;
1689
    }
1690
}
1691