Completed
Push — master ( 321dfc...a90bf8 )
by Adrien
16:05 queued 07:57
created

Html::generateSheetData()   B

Complexity

Conditions 10
Paths 10

Size

Total Lines 67
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 11.7193

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 10
eloc 35
c 1
b 0
f 0
nc 10
nop 0
dl 0
loc 67
rs 7.6666
ccs 23
cts 31
cp 0.7419
crap 11.7193

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

1099
        $borderStyle = $this->mapBorderStyle(/** @scrutinizer ignore-type */ $pStyle->getBorderStyle());
Loading history...
1100 18
1101 12
        return $borderStyle . ' #' . $pStyle->getColor()->getRGB() . (($borderStyle == 'none') ? '' : ' !important');
1102 12
    }
1103
1104 7
    /**
1105 7
     * Create CSS style (Fill).
1106
     *
1107 7
     * @param Fill $pStyle Fill
1108 4
     *
1109
     * @return array
1110 3
     */
1111
    private function createCSSStyleFill(Fill $pStyle)
1112
    {
1113
        // Construct HTML
1114
        $css = [];
1115 18
1116 18
        // Create CSS
1117 18
        $value = $pStyle->getFillType() == Fill::FILL_NONE ?
1118 18
            'white' : '#' . $pStyle->getStartColor()->getRGB();
1119 12
        $css['background-color'] = $value;
1120 12
1121
        return $css;
1122
    }
1123
1124
    /**
1125
     * Generate HTML footer.
1126
     */
1127
    public function generateHTMLFooter()
1128
    {
1129 18
        // Construct HTML
1130
        $html = '';
1131
        $html .= '  </body>' . PHP_EOL;
1132
        $html .= '</html>' . PHP_EOL;
1133
1134
        return $html;
1135 18
    }
1136
1137 18
    private function generateTableTagInline($pSheet, $id)
1138
    {
1139
        $style = isset($this->cssStyles['table']) ?
1140
            $this->assembleCSS($this->cssStyles['table']) : '';
1141
1142
        $prntgrid = $pSheet->getPrintGridlines();
1143
        $viewgrid = $this->isPdf ? $prntgrid : $pSheet->getShowGridlines();
1144
        if ($viewgrid && $prntgrid) {
1145
            $html = "    <table border='1' cellpadding='1' $id cellspacing='1' style='$style' class='gridlines gridlinesp'>" . PHP_EOL;
1146
        } elseif ($viewgrid) {
1147
            $html = "    <table border='0' cellpadding='0' $id cellspacing='0' style='$style' class='gridlines'>" . PHP_EOL;
1148
        } elseif ($prntgrid) {
1149
            $html = "    <table border='0' cellpadding='0' $id cellspacing='0' style='$style' class='gridlinesp'>" . PHP_EOL;
1150 18
        } else {
1151
            $html = "    <table border='0' cellpadding='1' $id cellspacing='0' style='$style'>" . PHP_EOL;
1152
        }
1153 18
1154
        return $html;
1155
    }
1156 18
1157
    private function generateTableTag($pSheet, $id, &$html, $sheetIndex): void
1158
    {
1159 18
        if (!$this->useInlineCss) {
1160
            $gridlines = $pSheet->getShowGridlines() ? ' gridlines' : '';
1161
            $gridlinesp = $pSheet->getPrintGridlines() ? ' gridlinesp' : '';
1162
            $html .= "    <table border='0' cellpadding='0' cellspacing='0' $id class='sheet$sheetIndex$gridlines$gridlinesp'>" . PHP_EOL;
1163
        } else {
1164
            $html .= $this->generateTableTagInline($pSheet, $id);
1165
        }
1166
    }
1167
1168
    /**
1169
     * Generate table header.
1170
     *
1171
     * @param Worksheet $pSheet The worksheet for the table we are writing
1172
     * @param bool   $showid whether or not to add id to table tag
1173
     *
1174
     * @return string
1175
     */
1176 18
    private function generateTableHeader($pSheet, $showid = true)
1177 12
    {
1178
        $sheetIndex = $pSheet->getParent()->getIndex($pSheet);
1179 7
1180 7
        // Construct HTML
1181
        $html = '';
1182 7
        $id = $showid ? "id='sheet$sheetIndex'" : '';
1183
        if ($showid) {
1184
            $html .= "<div style='page: page$sheetIndex'>\n";
1185
        //} elseif ($this->useInlineCss) {
1186 18
        //    $html .= "<div style='page-break-before: always' ></div>\n";
1187 18
        } else {
1188 18
            $html .= "<div style='page: page$sheetIndex' class='scrpgbrk'>\n";
1189 18
        }
1190 18
1191 12
        $this->generateTableTag($pSheet, $id, $html, $sheetIndex);
1192
1193 7
        // Write <col> elements
1194 7
        $highestColumnIndex = Coordinate::columnIndexFromString($pSheet->getHighestColumn()) - 1;
1195
        $i = -1;
1196
        while ($i++ < $highestColumnIndex) {
1197
            if (!$this->useInlineCss) {
1198
                $html .= '        <col class="col' . $i . '" />' . PHP_EOL;
1199 7
            } else {
1200
                $style = isset($this->cssStyles['table.sheet' . $sheetIndex . ' col.col' . $i]) ?
1201
                    $this->assembleCSS($this->cssStyles['table.sheet' . $sheetIndex . ' col.col' . $i]) : '';
1202
                $html .= '        <col style="' . $style . '" />' . PHP_EOL;
1203
            }
1204 18
        }
1205 18
1206
        return $html;
1207
    }
1208 18
1209
    /**
1210
     * Generate table footer.
1211 18
     */
1212 18
    private function generateTableFooter()
1213 18
    {
1214
        return '    </tbody></table>' . PHP_EOL . '</div>' . PHP_EOL;
1215
    }
1216
1217 18
    /**
1218
     * Generate row start.
1219 5
     *
1220 5
     * @param Worksheet $pSheet \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet
1221
     * @param int $sheetIndex Sheet index (0-based)
1222 5
     * @param int $pRow row number
1223 5
     *
1224
     * @return string
1225 5
     */
1226
    private function generateRowStart(Worksheet $pSheet, $sheetIndex, $pRow)
1227 5
    {
1228
        $html = '';
1229
        if (count($pSheet->getBreaks()) > 0) {
1230
            $breaks = $pSheet->getBreaks();
1231
1232
            // check if a break is needed before this row
1233 5
            if (isset($breaks['A' . $pRow])) {
1234 5
                // close table: </table>
1235
                $html .= $this->generateTableFooter();
1236 5
                if ($this->isPdf && $this->useInlineCss) {
1237 5
                    $html .= '<div style="page-break-before:always" />';
1238
                }
1239 5
1240
                // open table again: <table> + <col> etc.
1241
                $html .= $this->generateTableHeader($pSheet, false);
1242
                $html .= '<tbody>' . PHP_EOL;
1243 5
            }
1244
        }
1245
1246
        // Write row start
1247 18
        if (!$this->useInlineCss) {
1248 18
            $html .= '          <tr class="row' . $pRow . '">' . PHP_EOL;
1249 18
        } else {
1250 18
            $style = isset($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $pRow])
1251 18
                ? $this->assembleCSS($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $pRow]) : '';
1252
1253
            $html .= '          <tr style="' . $style . '">' . PHP_EOL;
1254
        }
1255
1256
        return $html;
1257
    }
1258
1259
    private function generateRowCellCss($pSheet, $cellAddress, $pRow, $colNum)
1260 18
    {
1261 18
        $cell = ($cellAddress > '') ? $pSheet->getCell($cellAddress) : '';
1262
        $coordinate = Coordinate::stringFromColumnIndex($colNum + 1) . ($pRow + 1);
1263 18
        if (!$this->useInlineCss) {
1264
            $cssClass = 'column' . $colNum;
1265
        } else {
1266
            $cssClass = [];
1267
            // The statements below do nothing.
1268
            // Commenting out the code rather than deleting it
1269
            // in case someone can figure out what their intent was.
1270 18
            //if ($cellType == 'th') {
1271
            //    if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' th.column' . $colNum])) {
1272
            //        $this->cssStyles['table.sheet' . $sheetIndex . ' th.column' . $colNum];
1273 18
            //    }
1274
            //} else {
1275
            //    if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' td.column' . $colNum])) {
1276 18
            //        $this->cssStyles['table.sheet' . $sheetIndex . ' td.column' . $colNum];
1277 12
            //    }
1278 12
            //}
1279
            // End of mystery statements.
1280 7
        }
1281
1282
        return [$cell, $cssClass, $coordinate];
1283
    }
1284
1285 7
    private function generateRowCellDataValueRich($cell, &$cellData): void
1286 7
    {
1287
        // Loop through rich text elements
1288
        $elements = $cell->getValue()->getRichTextElements();
1289
        foreach ($elements as $element) {
1290
            // Rich text start?
1291 7
            if ($element instanceof Run) {
1292 7
                $cellData .= '<span style="' . $this->assembleCSS($this->createCSSStyleFont($element->getFont())) . '">';
1293 7
1294
                $cellEnd = '';
1295 7
                if ($element->getFont()->getSuperscript()) {
1296
                    $cellData .= '<sup>';
1297
                    $cellEnd = '</sup>';
1298
                } elseif ($element->getFont()->getSubscript()) {
1299
                    $cellData .= '<sub>';
1300
                    $cellEnd = '</sub>';
1301 18
                }
1302 4
1303
                // Convert UTF8 data to PCDATA
1304
                $cellText = $element->getText();
1305
                $cellData .= htmlspecialchars($cellText);
1306 18
1307 18
                $cellData .= $cellEnd;
1308
1309
                $cellData .= '</span>';
1310 18
            } else {
1311 18
                // Convert UTF8 data to PCDATA
1312 18
                $cellText = $element->getText();
1313 6
                $cellData .= htmlspecialchars($cellText);
1314 6
            }
1315 6
        }
1316
    }
1317
1318
    private function generateRowCellDataValue($pSheet, $cell, &$cellData): void
1319 6
    {
1320 6
        if ($cell->getValue() instanceof RichText) {
1321 3
            $this->generateRowCellDataValueRich($cell, $cellData);
1322
        } else {
1323
            $origData = $this->preCalculateFormulas ? $cell->getCalculatedValue() : $cell->getValue();
1324
            $cellData = NumberFormat::toFormattedString(
1325
                $origData,
1326 18
                $pSheet->getParent()->getCellXfByIndex($cell->getXfIndex())->getNumberFormat()->getFormatCode(),
1327
                [$this, 'formatColor']
1328 18
            );
1329 18
            if ($cellData === $origData) {
1330 12
                $cellData = htmlspecialchars($cellData);
1331
            }
1332
            if ($pSheet->getParent()->getCellXfByIndex($cell->getXfIndex())->getFont()->getSuperscript()) {
1333
                $cellData = '<sup>' . $cellData . '</sup>';
1334
            } elseif ($pSheet->getParent()->getCellXfByIndex($cell->getXfIndex())->getFont()->getSubscript()) {
1335 7
                $cellData = '<sub>' . $cellData . '</sub>';
1336 7
            }
1337 7
        }
1338 7
    }
1339 7
1340 7
    private function generateRowCellData($pSheet, $cell, &$cssClass, $cellType)
1341
    {
1342
        $cellData = '&nbsp;';
1343 7
        if ($cell instanceof Cell) {
1344
            $cellData = '';
1345
            // Don't know what this does, and no test cases.
1346
            //if ($cell->getParent() === null) {
1347 7
            //    $cell->attach($pSheet);
1348 1
            //}
1349 1
            // Value
1350
            $this->generateRowCellDataValue($pSheet, $cell, $cellData);
1351
1352
            // Converts the cell content so that spaces occuring at beginning of each new line are replaced by &nbsp;
1353 7
            // Example: "  Hello\n to the world" is converted to "&nbsp;&nbsp;Hello\n&nbsp;to the world"
1354
            $cellData = preg_replace('/(?m)(?:^|\\G) /', '&nbsp;', $cellData);
1355 18
1356 6
            // convert newline "\n" to '<br>'
1357
            $cellData = nl2br($cellData);
1358 18
1359
            // Extend CSS class?
1360
            if (!$this->useInlineCss) {
1361 18
                $cssClass .= ' style' . $cell->getXfIndex();
1362
                $cssClass .= ' ' . $cell->getDataType();
1363 18
            } else {
1364
                if ($cellType == 'th') {
1365
                    if (isset($this->cssStyles['th.style' . $cell->getXfIndex()])) {
1366 18
                        $cssClass = array_merge($cssClass, $this->cssStyles['th.style' . $cell->getXfIndex()]);
1367
                    }
1368
                } else {
1369 18
                    if (isset($this->cssStyles['td.style' . $cell->getXfIndex()])) {
1370
                        $cssClass = array_merge($cssClass, $this->cssStyles['td.style' . $cell->getXfIndex()]);
1371
                    }
1372
                }
1373
1374 18
                // General horizontal alignment: Actual horizontal alignment depends on dataType
1375
                $sharedStyle = $pSheet->getParent()->getCellXfByIndex($cell->getXfIndex());
1376
                if ($sharedStyle->getAlignment()->getHorizontal() == Alignment::HORIZONTAL_GENERAL
1377 18
                    && isset($this->cssStyles['.' . $cell->getDataType()]['text-align'])
1378
                ) {
1379
                    $cssClass['text-align'] = $this->cssStyles['.' . $cell->getDataType()]['text-align'];
1380
                }
1381 18
            }
1382
        } else {
1383
            // Use default borders for empty cell
1384
            if (is_string($cssClass)) {
1385 18
                $cssClass .= ' style0';
1386
            }
1387
        }
1388 18
1389
        return $cellData;
1390
    }
1391
1392
    private function generateRowIncludeCharts($pSheet, $coordinate)
1393
    {
1394
        return $this->includeCharts ? $this->writeChartInCell($pSheet, $coordinate) : '';
1395
    }
1396
1397
    private function generateRowSpans($html, $rowSpan, $colSpan)
1398 18
    {
1399
        $html .= ($colSpan > 1) ? (' colspan="' . $colSpan . '"') : '';
1400 18
        $html .= ($rowSpan > 1) ? (' rowspan="' . $rowSpan . '"') : '';
1401 18
1402 18
        return $html;
1403
    }
1404 18
1405
    private function generateRowWriteCell(&$html, $pSheet, $coordinate, $cellType, $cellData, $colSpan, $rowSpan, $cssClass, $colNum, $sheetIndex, $pRow): void
1406 18
    {
1407
        // Image?
1408
        $htmlx = $this->writeImageInCell($pSheet, $coordinate);
1409
        // Chart?
1410
        $htmlx .= $this->generateRowIncludeCharts($pSheet, $coordinate);
1411
        // Column start
1412
        $html .= '            <' . $cellType;
1413
        if (!$this->useInlineCss && !$this->isPdf) {
1414 4
            $html .= ' class="' . $cssClass . '"';
1415
            if ($htmlx) {
1416 4
                $html .= " style='position: relative;'";
1417
            }
1418
        } else {
1419
            //** Necessary redundant code for the sake of \PhpOffice\PhpSpreadsheet\Writer\Pdf **
1420
            // We must explicitly write the width of the <td> element because TCPDF
1421
            // does not recognize e.g. <col style="width:42pt">
1422
            if ($this->useInlineCss) {
1423
                $xcssClass = $cssClass;
1424
            } else {
1425
                $html .= ' class="' . $cssClass . '"';
1426
                $xcssClass = [];
1427
            }
1428
            $width = 0;
1429
            $i = $colNum - 1;
1430
            $e = $colNum + $colSpan - 1;
1431
            while ($i++ < $e) {
1432
                if (isset($this->columnWidths[$sheetIndex][$i])) {
1433
                    $width += $this->columnWidths[$sheetIndex][$i];
1434
                }
1435
            }
1436
            $xcssClass['width'] = $width . 'pt';
1437
1438
            // We must also explicitly write the height of the <td> element because TCPDF
1439
            // does not recognize e.g. <tr style="height:50pt">
1440
            if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $pRow]['height'])) {
1441
                $height = $this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $pRow]['height'];
1442
                $xcssClass['height'] = $height;
1443
            }
1444
            //** end of redundant code **
1445
1446
            if ($htmlx) {
1447
                $xcssClass['position'] = 'relative';
1448
            }
1449
            $html .= ' style="' . $this->assembleCSS($xcssClass) . '"';
1450
        }
1451
        $html = $this->generateRowSpans($html, $rowSpan, $colSpan);
1452
1453
        $html .= '>';
1454
        $html .= $htmlx;
1455
1456
        $html .= $this->writeComment($pSheet, $coordinate);
1457
1458
        // Cell data
1459
        $html .= $cellData;
1460
1461
        // Column end
1462
        $html .= '</' . $cellType . '>' . PHP_EOL;
1463
    }
1464
1465
    /**
1466
     * Generate row.
1467
     *
1468
     * @param Worksheet $pSheet \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet
1469
     * @param array $pValues Array containing cells in a row
1470
     * @param int $pRow Row number (0-based)
1471
     * @param string $cellType eg: 'td'
1472
     *
1473
     * @return string
1474 11
     */
1475
    private function generateRow(Worksheet $pSheet, array $pValues, $pRow, $cellType)
1476 11
    {
1477
        // Sheet index
1478 11
        $sheetIndex = $pSheet->getParent()->getIndex($pSheet);
1479
        $html = $this->generateRowStart($pSheet, $sheetIndex, $pRow);
1480
1481
        // Write cells
1482
        $colNum = 0;
1483
        foreach ($pValues as $cellAddress) {
1484
            [$cell, $cssClass, $coordinate] = $this->generateRowCellCss($pSheet, $cellAddress, $pRow, $colNum);
1485
1486
            $colSpan = 1;
1487
            $rowSpan = 1;
1488
1489
            // Cell Data
1490
            $cellData = $this->generateRowCellData($pSheet, $cell, $cssClass, $cellType);
1491
1492
            // Hyperlink?
1493
            if ($pSheet->hyperlinkExists($coordinate) && !$pSheet->getHyperlink($coordinate)->isInternal()) {
1494
                $cellData = '<a href="' . htmlspecialchars($pSheet->getHyperlink($coordinate)->getUrl()) . '" title="' . htmlspecialchars($pSheet->getHyperlink($coordinate)->getTooltip()) . '">' . $cellData . '</a>';
1495
            }
1496
1497
            // Should the cell be written or is it swallowed by a rowspan or colspan?
1498
            $writeCell = !(isset($this->isSpannedCell[$pSheet->getParent()->getIndex($pSheet)][$pRow + 1][$colNum])
1499
                && $this->isSpannedCell[$pSheet->getParent()->getIndex($pSheet)][$pRow + 1][$colNum]);
1500
1501
            // Colspan and Rowspan
1502
            $colspan = 1;
0 ignored issues
show
Unused Code introduced by
The assignment to $colspan is dead and can be removed.
Loading history...
1503
            $rowspan = 1;
0 ignored issues
show
Unused Code introduced by
The assignment to $rowspan is dead and can be removed.
Loading history...
1504
            if (isset($this->isBaseCell[$pSheet->getParent()->getIndex($pSheet)][$pRow + 1][$colNum])) {
1505
                $spans = $this->isBaseCell[$pSheet->getParent()->getIndex($pSheet)][$pRow + 1][$colNum];
1506
                $rowSpan = $spans['rowspan'];
1507
                $colSpan = $spans['colspan'];
1508
1509
                //    Also apply style from last cell in merge to fix borders -
1510
                //        relies on !important for non-none border declarations in createCSSStyleBorder
1511
                $endCellCoord = Coordinate::stringFromColumnIndex($colNum + $colSpan) . ($pRow + $rowSpan);
1512
                if (!$this->useInlineCss) {
1513 4
                    $cssClass .= ' style' . $pSheet->getCell($endCellCoord)->getXfIndex();
1514
                }
1515
            }
1516 4
1517 4
            // Write
1518
            if ($writeCell) {
1519 4
                $this->generateRowWriteCell($html, $pSheet, $coordinate, $cellType, $cellData, $colSpan, $rowSpan, $cssClass, $colNum, $sheetIndex, $pRow);
1520 4
            }
1521
1522
            // Next column
1523
            ++$colNum;
1524
        }
1525
1526 4
        // Write row end
1527
        $html .= '          </tr>' . PHP_EOL;
1528
1529 4
        // Return
1530
        return $html;
1531
    }
1532
1533 4
    /**
1534
     * Takes array where of CSS properties / values and converts to CSS string.
1535
     *
1536
     * @return string
1537
     */
1538
    private function assembleCSS(array $pValue = [])
1539 18
    {
1540
        $pairs = [];
1541
        foreach ($pValue as $property => $value) {
1542
            $pairs[] = $property . ':' . $value;
1543
        }
1544 18
        $string = implode('; ', $pairs);
1545 18
1546
        return $string;
1547 18
    }
1548 18
1549
    /**
1550 18
     * Get images root.
1551
     *
1552
     * @return string
1553 18
     */
1554 6
    public function getImagesRoot()
1555 6
    {
1556 6
        return $this->imagesRoot;
1557
    }
1558 6
1559 6
    /**
1560
     * Set images root.
1561 6
     *
1562 6
     * @param string $pValue
1563
     *
1564
     * @return $this
1565 6
     */
1566 6
    public function setImagesRoot($pValue)
1567
    {
1568 6
        $this->imagesRoot = $pValue;
1569
1570 6
        return $this;
1571 6
    }
1572 6
1573
    /**
1574 6
     * Get embed images.
1575 6
     *
1576
     * @return bool
1577
     */
1578
    public function getEmbedImages()
1579 6
    {
1580 6
        return $this->embedImages;
1581 6
    }
1582 6
1583 6
    /**
1584
     * Set embed images.
1585
     *
1586
     * @param bool $pValue
1587
     *
1588
     * @return $this
1589
     */
1590
    public function setEmbedImages($pValue)
1591
    {
1592 18
        $this->embedImages = $pValue;
1593 18
1594 6
        return $this;
1595 6
    }
1596 4
1597
    /**
1598
     * Get use inline CSS?
1599
     *
1600
     * @return bool
1601
     */
1602 18
    public function getUseInlineCss()
1603 4
    {
1604 4
        return $this->useInlineCss;
1605 4
    }
1606 4
1607 4
    /**
1608 4
     * Set use inline CSS?
1609
     *
1610 4
     * @param bool $pValue
1611
     *
1612 4
     * @return $this
1613 4
     */
1614
    public function setUseInlineCss($pValue)
1615
    {
1616
        $this->useInlineCss = $pValue;
1617
1618
        return $this;
1619
    }
1620
1621
    /**
1622
     * Get use embedded CSS?
1623 18
     *
1624 18
     * @deprecated no longer used
1625
     *
1626 18
     * @return bool
1627
     *
1628 18
     * @codeCoverageIgnore
1629 18
     */
1630
    public function getUseEmbeddedCSS()
1631 18
    {
1632 18
        return $this->useEmbeddedCSS;
1633 18
    }
1634 18
1635 18
    /**
1636 18
     * Set use embedded CSS?
1637 18
     *
1638 18
     * @deprecated no longer used
1639 18
     *
1640 18
     * @param bool $pValue
1641 18
     *
1642 18
     * @return $this
1643
     *
1644 18
     * @codeCoverageIgnore
1645 18
     */
1646
    public function setUseEmbeddedCSS($pValue)
1647 18
    {
1648
        $this->useEmbeddedCSS = $pValue;
1649
1650
        return $this;
1651
    }
1652
1653
    /**
1654
     * Add color to formatted string as inline style.
1655
     *
1656
     * @param string $pValue Plain formatted value without color
1657
     * @param string $pFormat Format code
1658
     *
1659
     * @return string
1660 18
     */
1661
    public function formatColor($pValue, $pFormat)
1662 18
    {
1663 18
        // Color information, e.g. [Red] is always at the beginning
1664 7
        $color = null; // initialize
1665 7
        $matches = [];
1666 7
1667
        $color_regex = '/^\\[[a-zA-Z]+\\]/';
1668
        if (preg_match($color_regex, $pFormat, $matches)) {
1669 18
            $color = str_replace(['[', ']'], '', $matches[0]);
1670
            $color = strtolower($color);
1671
        }
1672
1673
        // convert to PCDATA
1674
        $value = htmlspecialchars($pValue);
1675
1676
        // color span tag
1677
        if ($color !== null) {
1678
            $value = '<span style="color:' . $color . '">' . $value . '</span>';
1679
        }
1680
1681
        return $value;
1682
    }
1683
1684
    /**
1685
     * Calculate information about HTML colspan and rowspan which is not always the same as Excel's.
1686
     */
1687
    private function calculateSpans(): void
1688
    {
1689
        if ($this->spansAreCalculated) {
1690
            return;
1691
        }
1692
        // Identify all cells that should be omitted in HTML due to cell merge.
1693
        // In HTML only the upper-left cell should be written and it should have
1694
        //   appropriate rowspan / colspan attribute
1695
        $sheetIndexes = $this->sheetIndex !== null ?
1696
            [$this->sheetIndex] : range(0, $this->spreadsheet->getSheetCount() - 1);
1697
1698
        foreach ($sheetIndexes as $sheetIndex) {
1699
            $sheet = $this->spreadsheet->getSheet($sheetIndex);
1700
1701
            $candidateSpannedRow = [];
1702
1703
            // loop through all Excel merged cells
1704
            foreach ($sheet->getMergeCells() as $cells) {
1705
                [$cells] = Coordinate::splitRange($cells);
1706
                $first = $cells[0];
1707
                $last = $cells[1];
1708
1709
                [$fc, $fr] = Coordinate::coordinateFromString($first);
1710
                $fc = Coordinate::columnIndexFromString($fc) - 1;
1711
1712
                [$lc, $lr] = Coordinate::coordinateFromString($last);
1713
                $lc = Coordinate::columnIndexFromString($lc) - 1;
1714
1715
                // loop through the individual cells in the individual merge
1716
                $r = $fr - 1;
1717
                while ($r++ < $lr) {
1718
                    // also, flag this row as a HTML row that is candidate to be omitted
1719
                    $candidateSpannedRow[$r] = $r;
1720
1721
                    $c = $fc - 1;
1722
                    while ($c++ < $lc) {
1723
                        if (!($c == $fc && $r == $fr)) {
1724
                            // not the upper-left cell (should not be written in HTML)
1725
                            $this->isSpannedCell[$sheetIndex][$r][$c] = [
1726
                                'baseCell' => [$fr, $fc],
1727
                            ];
1728
                        } else {
1729
                            // upper-left is the base cell that should hold the colspan/rowspan attribute
1730
                            $this->isBaseCell[$sheetIndex][$r][$c] = [
1731
                                'xlrowspan' => $lr - $fr + 1, // Excel rowspan
1732
                                'rowspan' => $lr - $fr + 1, // HTML rowspan, value may change
1733
                                'xlcolspan' => $lc - $fc + 1, // Excel colspan
1734
                                'colspan' => $lc - $fc + 1, // HTML colspan, value may change
1735
                            ];
1736
                        }
1737
                    }
1738
                }
1739
            }
1740
1741
            $this->calculateSpansOmitRows($sheet, $sheetIndex, $candidateSpannedRow);
1742
1743
            // TODO: Same for columns
1744
        }
1745
1746
        // We have calculated the spans
1747
        $this->spansAreCalculated = true;
1748
    }
1749
1750
    private function calculateSpansOmitRows($sheet, $sheetIndex, $candidateSpannedRow): void
1751
    {
1752
        // Identify which rows should be omitted in HTML. These are the rows where all the cells
1753
        //   participate in a merge and the where base cells are somewhere above.
1754
        $countColumns = Coordinate::columnIndexFromString($sheet->getHighestColumn());
1755
        foreach ($candidateSpannedRow as $rowIndex) {
1756
            if (isset($this->isSpannedCell[$sheetIndex][$rowIndex])) {
1757
                if (count($this->isSpannedCell[$sheetIndex][$rowIndex]) == $countColumns) {
1758
                    $this->isSpannedRow[$sheetIndex][$rowIndex] = $rowIndex;
1759
                }
1760
            }
1761
        }
1762
1763
        // For each of the omitted rows we found above, the affected rowspans should be subtracted by 1
1764
        if (isset($this->isSpannedRow[$sheetIndex])) {
1765
            foreach ($this->isSpannedRow[$sheetIndex] as $rowIndex) {
1766
                $adjustedBaseCells = [];
1767
                $c = -1;
1768
                $e = $countColumns - 1;
1769
                while ($c++ < $e) {
1770
                    $baseCell = $this->isSpannedCell[$sheetIndex][$rowIndex][$c]['baseCell'];
1771
1772
                    if (!in_array($baseCell, $adjustedBaseCells)) {
1773
                        // subtract rowspan by 1
1774
                        --$this->isBaseCell[$sheetIndex][$baseCell[0]][$baseCell[1]]['rowspan'];
1775
                        $adjustedBaseCells[] = $baseCell;
1776
                    }
1777
                }
1778
            }
1779
        }
1780
    }
1781
1782
    /**
1783
     * Write a comment in the same format as LibreOffice.
1784
     *
1785
     * @see https://github.com/LibreOffice/core/blob/9fc9bf3240f8c62ad7859947ab8a033ac1fe93fa/sc/source/filter/html/htmlexp.cxx#L1073-L1092
1786
     *
1787
     * @param string $coordinate
1788
     *
1789
     * @return string
1790
     */
1791
    private function writeComment(Worksheet $pSheet, $coordinate)
1792
    {
1793
        $result = '';
1794
        if (!$this->isPdf && isset($pSheet->getComments()[$coordinate])) {
1795
            $result .= '<a class="comment-indicator"></a>';
1796
            $result .= '<div class="comment">' . nl2br($pSheet->getComment($coordinate)->getText()->getPlainText()) . '</div>';
1797
            $result .= PHP_EOL;
1798
        }
1799
1800
        return $result;
1801
    }
1802
1803
    /**
1804
     * Generate @page declarations.
1805
     *
1806
     * @param bool $generateSurroundingHTML
1807
     *
1808
     * @return    string
1809
     */
1810
    private function generatePageDeclarations($generateSurroundingHTML)
1811
    {
1812
        // Ensure that Spans have been calculated?
1813
        $this->calculateSpans();
1814
1815
        // Fetch sheets
1816
        $sheets = [];
1817
        if ($this->sheetIndex === null) {
1818
            $sheets = $this->spreadsheet->getAllSheets();
1819
        } else {
1820
            $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex);
1821
        }
1822
1823
        // Construct HTML
1824
        $htmlPage = $generateSurroundingHTML ? ('<style type="text/css">' . PHP_EOL) : '';
1825
1826
        // Loop all sheets
1827
        $sheetId = 0;
1828
        foreach ($sheets as $pSheet) {
1829
            $htmlPage .= "@page page$sheetId { ";
1830
            $left = StringHelper::formatNumber($pSheet->getPageMargins()->getLeft()) . 'in; ';
1831
            $htmlPage .= 'margin-left: ' . $left;
1832
            $right = StringHelper::FormatNumber($pSheet->getPageMargins()->getRight()) . 'in; ';
1833
            $htmlPage .= 'margin-right: ' . $right;
1834
            $top = StringHelper::FormatNumber($pSheet->getPageMargins()->getTop()) . 'in; ';
1835
            $htmlPage .= 'margin-top: ' . $top;
1836
            $bottom = StringHelper::FormatNumber($pSheet->getPageMargins()->getBottom()) . 'in; ';
1837
            $htmlPage .= 'margin-bottom: ' . $bottom;
1838
            $orientation = $pSheet->getPageSetup()->getOrientation();
1839
            if ($orientation === \PhpOffice\PhpSpreadsheet\Worksheet\PageSetup::ORIENTATION_LANDSCAPE) {
1840
                $htmlPage .= 'size: landscape; ';
1841
            } elseif ($orientation === \PhpOffice\PhpSpreadsheet\Worksheet\PageSetup::ORIENTATION_PORTRAIT) {
1842
                $htmlPage .= 'size: portrait; ';
1843
            }
1844
            $htmlPage .= "}\n";
1845
            ++$sheetId;
1846
        }
1847
        $htmlPage .= <<<EOF
1848
.navigation {page-break-after: always;}
1849
.scrpgbrk, div + div {page-break-before: always;}
1850
@media screen {
1851
  .gridlines td {border: 1px solid black;}
1852
  .gridlines th {border: 1px solid black;}
1853
  body>div {margin-top: 5px;}
1854
  body>div:first-child {margin-top: 0;}
1855
  .scrpgbrk {margin-top: 1px;}
1856
}
1857
@media print {
1858
  .gridlinesp td {border: 1px solid black;}
1859
  .gridlinesp th {border: 1px solid black;}
1860
  .navigation {display: none;}
1861
}
1862
1863
EOF;
1864
        $htmlPage .= $generateSurroundingHTML ? ('</style>' . PHP_EOL) : '';
1865
1866
        return $htmlPage;
1867
    }
1868
}
1869