Passed
Push — master ( ed6627...cde292 )
by Owen
12:57
created

Html::generateRowWriteCell()   F

Complexity

Conditions 22
Paths 330

Size

Total Lines 90
Code Lines 47

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 46
CRAP Score 22

Importance

Changes 0
Metric Value
eloc 47
c 0
b 0
f 0
dl 0
loc 90
rs 1.7083
ccs 46
cts 46
cp 1
cc 22
nc 330
nop 11
crap 22

How to fix   Long Method    Complexity    Many Parameters   

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:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Writer;
4
5
use Composer\Pcre\Preg;
6
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
7
use PhpOffice\PhpSpreadsheet\Cell\Cell;
8
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
9
use PhpOffice\PhpSpreadsheet\Cell\DataType;
10
use PhpOffice\PhpSpreadsheet\Chart\Chart;
11
use PhpOffice\PhpSpreadsheet\Comment;
12
use PhpOffice\PhpSpreadsheet\Document\Properties;
13
use PhpOffice\PhpSpreadsheet\RichText\RichText;
14
use PhpOffice\PhpSpreadsheet\RichText\Run;
15
use PhpOffice\PhpSpreadsheet\Settings;
16
use PhpOffice\PhpSpreadsheet\Shared\Date;
17
use PhpOffice\PhpSpreadsheet\Shared\Drawing as SharedDrawing;
18
use PhpOffice\PhpSpreadsheet\Shared\File;
19
use PhpOffice\PhpSpreadsheet\Shared\Font as SharedFont;
20
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
21
use PhpOffice\PhpSpreadsheet\Spreadsheet;
22
use PhpOffice\PhpSpreadsheet\Style\Alignment;
23
use PhpOffice\PhpSpreadsheet\Style\Border;
24
use PhpOffice\PhpSpreadsheet\Style\Borders;
25
use PhpOffice\PhpSpreadsheet\Style\Fill;
26
use PhpOffice\PhpSpreadsheet\Style\Font;
27
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
28
use PhpOffice\PhpSpreadsheet\Style\Style;
29
use PhpOffice\PhpSpreadsheet\Worksheet\BaseDrawing;
30
use PhpOffice\PhpSpreadsheet\Worksheet\Drawing;
31
use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing;
32
use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup;
33
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
34
35
class Html extends BaseWriter
36
{
37
    private const DEFAULT_CELL_WIDTH_POINTS = 42;
38
39
    private const DEFAULT_CELL_WIDTH_PIXELS = 56;
40
41
    /**
42
     * Migration aid to tell if html tags will be treated as plaintext in comments.
43
     *     if (
44
     *         defined(
45
     *             \PhpOffice\PhpSpreadsheet\Writer\Html::class
46
     *             . '::COMMENT_HTML_TAGS_PLAINTEXT'
47
     *         )
48
     *     ) {
49
     *         new logic with styling in TextRun elements
50
     *     } else {
51
     *         old logic with styling via Html tags
52
     *     }.
53
     */
54
    public const COMMENT_HTML_TAGS_PLAINTEXT = true;
55
56
    /**
57
     * Spreadsheet object.
58
     */
59
    protected Spreadsheet $spreadsheet;
60
61
    /**
62
     * Sheet index to write.
63
     */
64
    private ?int $sheetIndex = 0;
65
66
    /**
67
     * Images root.
68
     */
69
    private string $imagesRoot = '';
70
71
    /**
72
     * embed images, or link to images.
73
     */
74
    protected bool $embedImages = false;
75
76
    /**
77
     * Use inline CSS?
78
     */
79
    private bool $useInlineCss = false;
80
81
    /**
82
     * Array of CSS styles.
83
     */
84
    private ?array $cssStyles = null;
85
86
    /**
87
     * Array of column widths in points.
88
     */
89
    private array $columnWidths;
90
91
    /**
92
     * Default font.
93
     */
94
    private Font $defaultFont;
95
96
    /**
97
     * Flag whether spans have been calculated.
98
     */
99
    private bool $spansAreCalculated = false;
100
101
    /**
102
     * Excel cells that should not be written as HTML cells.
103
     */
104
    private array $isSpannedCell = [];
105
106
    /**
107
     * Excel cells that are upper-left corner in a cell merge.
108
     */
109
    private array $isBaseCell = [];
110
111
    /**
112
     * Excel rows that should not be written as HTML rows.
113
     */
114
    private array $isSpannedRow = [];
115
116
    /**
117
     * Is the current writer creating PDF?
118
     */
119
    protected bool $isPdf = false;
120
121
    /**
122
     * Is the current writer creating mPDF?
123
     *
124
     * @deprecated 2.0.1 use instanceof Mpdf instead
125
     */
126
    protected bool $isMPdf = false;
127
128
    /**
129
     * Generate the Navigation block.
130
     */
131
    private bool $generateSheetNavigationBlock = true;
132
133
    /**
134
     * Callback for editing generated html.
135
     *
136
     * @var null|callable
137
     */
138
    private $editHtmlCallback;
139
140
    /** @var BaseDrawing[] */
141
    private $sheetDrawings;
142
143
    /** @var Chart[] */
144
    private $sheetCharts;
145
146
    private bool $betterBoolean = false;
147
148
    private string $getTrue = 'TRUE';
149
150
    private string $getFalse = 'FALSE';
151
152
    /**
153
     * Create a new HTML.
154
     */
155 540
    public function __construct(Spreadsheet $spreadsheet)
156
    {
157 540
        $this->spreadsheet = $spreadsheet;
158 540
        $this->defaultFont = $this->spreadsheet->getDefaultStyle()->getFont();
159 540
        $calc = Calculation::getInstance($this->spreadsheet);
160 540
        $this->getTrue = $calc->getTRUE();
161 540
        $this->getFalse = $calc->getFALSE();
162
    }
163
164
    /**
165
     * Save Spreadsheet to file.
166
     *
167
     * @param resource|string $filename
168
     */
169 458
    public function save($filename, int $flags = 0): void
170
    {
171 458
        $this->processFlags($flags);
172
173
        // Open file
174 458
        $this->openFileHandle($filename);
175
176
        // Write html
177 457
        fwrite($this->fileHandle, $this->generateHTMLAll());
178
179
        // Close file
180 457
        $this->maybeCloseFileHandle();
181
    }
182
183
    /**
184
     * Save Spreadsheet as html to variable.
185
     */
186 528
    public function generateHtmlAll(): string
187
    {
188 528
        $sheets = $this->generateSheetPrep();
189 528
        foreach ($sheets as $sheet) {
190 528
            $sheet->calculateArrays($this->preCalculateFormulas);
191
        }
192
        // garbage collect
193 528
        $this->spreadsheet->garbageCollect();
194
195 528
        $saveDebugLog = Calculation::getInstance($this->spreadsheet)->getDebugLog()->getWriteDebugLog();
196 528
        Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog(false);
197
198
        // Build CSS
199 528
        $this->buildCSS(!$this->useInlineCss);
200
201 528
        $html = '';
202
203
        // Write headers
204 528
        $html .= $this->generateHTMLHeader(!$this->useInlineCss);
205
206
        // Write navigation (tabs)
207 528
        if ((!$this->isPdf) && ($this->generateSheetNavigationBlock)) {
208 498
            $html .= $this->generateNavigation();
209
        }
210
211
        // Write data
212 528
        $html .= $this->generateSheetData();
213
214
        // Write footer
215 528
        $html .= $this->generateHTMLFooter();
216 528
        $callback = $this->editHtmlCallback;
217 528
        if ($callback) {
218 6
            $html = $callback($html);
219
        }
220
221 528
        Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog($saveDebugLog);
222
223 528
        return $html;
224
    }
225
226
    /**
227
     * Set a callback to edit the entire HTML.
228
     *
229
     * The callback must accept the HTML as string as first parameter,
230
     * and it must return the edited HTML as string.
231
     */
232 6
    public function setEditHtmlCallback(?callable $callback): void
233
    {
234 6
        $this->editHtmlCallback = $callback;
235
    }
236
237
    /**
238
     * Map VAlign.
239
     *
240
     * @param string $vAlign Vertical alignment
241
     */
242 530
    private function mapVAlign(string $vAlign): string
243
    {
244 530
        return Alignment::VERTICAL_ALIGNMENT_FOR_HTML[$vAlign] ?? '';
245
    }
246
247
    /**
248
     * Map HAlign.
249
     *
250
     * @param string $hAlign Horizontal alignment
251
     */
252 530
    private function mapHAlign(string $hAlign): string
253
    {
254 530
        return Alignment::HORIZONTAL_ALIGNMENT_FOR_HTML[$hAlign] ?? '';
255
    }
256
257
    const BORDER_NONE = 'none';
258
    const BORDER_ARR = [
259
        Border::BORDER_NONE => self::BORDER_NONE,
260
        Border::BORDER_DASHDOT => '1px dashed',
261
        Border::BORDER_DASHDOTDOT => '1px dotted',
262
        Border::BORDER_DASHED => '1px dashed',
263
        Border::BORDER_DOTTED => '1px dotted',
264
        Border::BORDER_DOUBLE => '3px double',
265
        Border::BORDER_HAIR => '1px solid',
266
        Border::BORDER_MEDIUM => '2px solid',
267
        Border::BORDER_MEDIUMDASHDOT => '2px dashed',
268
        Border::BORDER_MEDIUMDASHDOTDOT => '2px dotted',
269
        Border::BORDER_SLANTDASHDOT => '2px dashed',
270
        Border::BORDER_THICK => '3px solid',
271
    ];
272
273
    /**
274
     * Map border style.
275
     *
276
     * @param int|string $borderStyle Sheet index
277
     */
278 519
    private function mapBorderStyle($borderStyle): string
279
    {
280 519
        return self::BORDER_ARR[$borderStyle] ?? '1px solid';
281
    }
282
283
    /**
284
     * Get sheet index.
285
     */
286 17
    public function getSheetIndex(): ?int
287
    {
288 17
        return $this->sheetIndex;
289
    }
290
291
    /**
292
     * Set sheet index.
293
     *
294
     * @param int $sheetIndex Sheet index
295
     *
296
     * @return $this
297
     */
298 1
    public function setSheetIndex(int $sheetIndex): static
299
    {
300 1
        $this->sheetIndex = $sheetIndex;
301
302 1
        return $this;
303
    }
304
305
    /**
306
     * Get sheet index.
307
     */
308 1
    public function getGenerateSheetNavigationBlock(): bool
309
    {
310 1
        return $this->generateSheetNavigationBlock;
311
    }
312
313
    /**
314
     * Set sheet index.
315
     *
316
     * @param bool $generateSheetNavigationBlock Flag indicating whether the sheet navigation block should be generated or not
317
     *
318
     * @return $this
319
     */
320 1
    public function setGenerateSheetNavigationBlock(bool $generateSheetNavigationBlock): static
321
    {
322 1
        $this->generateSheetNavigationBlock = (bool) $generateSheetNavigationBlock;
323
324 1
        return $this;
325
    }
326
327
    /**
328
     * Write all sheets (resets sheetIndex to NULL).
329
     *
330
     * @return $this
331
     */
332 15
    public function writeAllSheets(): static
333
    {
334 15
        $this->sheetIndex = null;
335
336 15
        return $this;
337
    }
338
339 530
    private static function generateMeta(?string $val, string $desc): string
340
    {
341 530
        return ($val || $val === '0')
342 530
            ? ('      <meta name="' . $desc . '" content="' . htmlspecialchars($val, Settings::htmlEntityFlags()) . '" />' . PHP_EOL)
343 530
            : '';
344
    }
345
346
    public const BODY_LINE = '  <body>' . PHP_EOL;
347
348
    private const CUSTOM_TO_META = [
349
        Properties::PROPERTY_TYPE_BOOLEAN => 'bool',
350
        Properties::PROPERTY_TYPE_DATE => 'date',
351
        Properties::PROPERTY_TYPE_FLOAT => 'float',
352
        Properties::PROPERTY_TYPE_INTEGER => 'int',
353
        Properties::PROPERTY_TYPE_STRING => 'string',
354
    ];
355
356
    /**
357
     * Generate HTML header.
358
     *
359
     * @param bool $includeStyles Include styles?
360
     */
361 530
    public function generateHTMLHeader(bool $includeStyles = false): string
362
    {
363
        // Construct HTML
364 530
        $properties = $this->spreadsheet->getProperties();
365 530
        $html = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">' . PHP_EOL;
366 530
        $html .= '<html xmlns="http://www.w3.org/1999/xhtml">' . PHP_EOL;
367 530
        $html .= '  <head>' . PHP_EOL;
368 530
        $html .= '      <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />' . PHP_EOL;
369 530
        $html .= '      <meta name="generator" content="PhpSpreadsheet, https://github.com/PHPOffice/PhpSpreadsheet" />' . PHP_EOL;
370 530
        $title = $properties->getTitle();
371 530
        if ($title === '') {
372 11
            $title = $this->spreadsheet->getActiveSheet()->getTitle();
373
        }
374 530
        $html .= '      <title>' . htmlspecialchars($title, Settings::htmlEntityFlags()) . '</title>' . PHP_EOL;
375 530
        $html .= self::generateMeta($properties->getCreator(), 'author');
376 530
        $html .= self::generateMeta($properties->getTitle(), 'title');
377 530
        $html .= self::generateMeta($properties->getDescription(), 'description');
378 530
        $html .= self::generateMeta($properties->getSubject(), 'subject');
379 530
        $html .= self::generateMeta($properties->getKeywords(), 'keywords');
380 530
        $html .= self::generateMeta($properties->getCategory(), 'category');
381 530
        $html .= self::generateMeta($properties->getCompany(), 'company');
382 530
        $html .= self::generateMeta($properties->getManager(), 'manager');
383 530
        $html .= self::generateMeta($properties->getLastModifiedBy(), 'lastModifiedBy');
384 530
        $html .= self::generateMeta($properties->getViewport(), 'viewport');
385 530
        $date = Date::dateTimeFromTimestamp((string) $properties->getCreated());
386 530
        $date->setTimeZone(Date::getDefaultOrLocalTimeZone());
387 530
        $html .= self::generateMeta($date->format(DATE_W3C), 'created');
388 530
        $date = Date::dateTimeFromTimestamp((string) $properties->getModified());
389 530
        $date->setTimeZone(Date::getDefaultOrLocalTimeZone());
390 530
        $html .= self::generateMeta($date->format(DATE_W3C), 'modified');
391
392 530
        $customProperties = $properties->getCustomProperties();
393 530
        foreach ($customProperties as $customProperty) {
394 4
            $propertyValue = $properties->getCustomPropertyValue($customProperty);
395 4
            $propertyType = $properties->getCustomPropertyType($customProperty);
396 4
            $propertyQualifier = self::CUSTOM_TO_META[$propertyType] ?? null;
397 4
            if ($propertyQualifier !== null) {
398 4
                if ($propertyType === Properties::PROPERTY_TYPE_BOOLEAN) {
399 1
                    $propertyValue = $propertyValue ? '1' : '0';
400 4
                } elseif ($propertyType === Properties::PROPERTY_TYPE_DATE) {
401 1
                    $date = Date::dateTimeFromTimestamp((string) $propertyValue);
402 1
                    $date->setTimeZone(Date::getDefaultOrLocalTimeZone());
403 1
                    $propertyValue = $date->format(DATE_W3C);
404
                } else {
405 4
                    $propertyValue = (string) $propertyValue;
406
                }
407 4
                $html .= self::generateMeta($propertyValue, htmlspecialchars("custom.$propertyQualifier.$customProperty"));
408
            }
409
        }
410
411 530
        if (!empty($properties->getHyperlinkBase())) {
412 2
            $html .= '      <base href="' . htmlspecialchars($properties->getHyperlinkBase()) . '" />' . PHP_EOL;
413
        }
414
415 530
        $html .= $includeStyles ? $this->generateStyles(true) : $this->generatePageDeclarations(true);
416
417 530
        $html .= '  </head>' . PHP_EOL;
418 530
        $html .= '' . PHP_EOL;
419 530
        $html .= self::BODY_LINE;
420
421 530
        return $html;
422
    }
423
424
    /** @return Worksheet[] */
425 528
    private function generateSheetPrep(): array
426
    {
427
        // Fetch sheets
428 528
        if ($this->sheetIndex === null) {
429 14
            $sheets = $this->spreadsheet->getAllSheets();
430
        } else {
431 521
            $sheets = [$this->spreadsheet->getSheet($this->sheetIndex)];
432
        }
433
434 528
        return $sheets;
435
    }
436
437 528
    private function generateSheetStarts(Worksheet $sheet, int $rowMin): array
438
    {
439
        // calculate start of <tbody>, <thead>
440 528
        $tbodyStart = $rowMin;
441 528
        $theadStart = $theadEnd = 0; // default: no <thead>    no </thead>
442 528
        if ($sheet->getPageSetup()->isRowsToRepeatAtTopSet()) {
443 2
            $rowsToRepeatAtTop = $sheet->getPageSetup()->getRowsToRepeatAtTop();
444
445
            // we can only support repeating rows that start at top row
446 2
            if ($rowsToRepeatAtTop[0] == 1) {
447 2
                $theadStart = $rowsToRepeatAtTop[0];
448 2
                $theadEnd = $rowsToRepeatAtTop[1];
449 2
                $tbodyStart = $rowsToRepeatAtTop[1] + 1;
450
            }
451
        }
452
453 528
        return [$theadStart, $theadEnd, $tbodyStart];
454
    }
455
456 528
    private function generateSheetTags(int $row, int $theadStart, int $theadEnd, int $tbodyStart): array
457
    {
458
        // <thead> ?
459 528
        $startTag = ($row == $theadStart) ? ('        <thead>' . PHP_EOL) : '';
460 528
        if (!$startTag) {
461 528
            $startTag = ($row == $tbodyStart) ? ('        <tbody>' . PHP_EOL) : '';
462
        }
463 528
        $endTag = ($row == $theadEnd) ? ('        </thead>' . PHP_EOL) : '';
464 528
        $cellType = ($row >= $tbodyStart) ? 'td' : 'th';
465
466 528
        return [$cellType, $startTag, $endTag];
467
    }
468
469
    /**
470
     * Generate sheet data.
471
     */
472 528
    public function generateSheetData(): string
473
    {
474
        // Ensure that Spans have been calculated?
475 528
        $this->calculateSpans();
476 528
        $sheets = $this->generateSheetPrep();
477
478
        // Construct HTML
479 528
        $html = '';
480
481
        // Loop all sheets
482 528
        $sheetId = 0;
483 528
        foreach ($sheets as $sheet) {
484
            // Write table header
485 528
            $html .= $this->generateTableHeader($sheet);
486 528
            $this->sheetCharts = [];
487 528
            $this->sheetDrawings = [];
488
489
            // Get worksheet dimension
490 528
            [$min, $max] = explode(':', $sheet->calculateWorksheetDataDimension());
491 528
            [$minCol, $minRow, $minColString] = Coordinate::indexesFromString($min);
492 528
            [$maxCol, $maxRow] = Coordinate::indexesFromString($max);
493 528
            $this->extendRowsAndColumns($sheet, $maxCol, $maxRow);
494
495 528
            [$theadStart, $theadEnd, $tbodyStart] = $this->generateSheetStarts($sheet, $minRow);
496
497
            // Loop through cells
498 528
            $row = $minRow - 1;
499 528
            while ($row++ < $maxRow) {
500 528
                [$cellType, $startTag, $endTag] = $this->generateSheetTags($row, $theadStart, $theadEnd, $tbodyStart);
501 528
                $html .= $startTag;
502
503
                // Write row if there are HTML table cells in it
504 528
                if ($this->shouldGenerateRow($sheet, $row) && !isset($this->isSpannedRow[$sheet->getParentOrThrow()->getIndex($sheet)][$row])) {
505
                    // Start a new rowData
506 528
                    $rowData = [];
507
                    // Loop through columns
508 528
                    $column = $minCol;
509 528
                    $colStr = $minColString;
510 528
                    while ($column <= $maxCol) {
511
                        // Cell exists?
512 528
                        $cellAddress = Coordinate::stringFromColumnIndex($column) . $row;
513 528
                        if ($this->shouldGenerateColumn($sheet, $colStr)) {
514 528
                            $rowData[$column] = ($sheet->getCellCollection()->has($cellAddress)) ? $cellAddress : '';
515
                        }
516 528
                        ++$column;
517 528
                        ++$colStr;
518
                    }
519 528
                    $html .= $this->generateRow($sheet, $rowData, $row - 1, $cellType);
520
                }
521
522 528
                $html .= $endTag;
523
            }
524
525
            // Write table footer
526 528
            $html .= $this->generateTableFooter();
527
            // Writing PDF?
528 528
            if ($this->isPdf && $this->useInlineCss) {
529 7
                if ($this->sheetIndex === null && $sheetId + 1 < $this->spreadsheet->getSheetCount()) {
530 1
                    $html .= '<div style="page-break-before:always" ></div>';
531
                }
532
            }
533
534
            // Next sheet
535 528
            ++$sheetId;
536
        }
537
538 528
        return $html;
539
    }
540
541
    /**
542
     * Generate sheet tabs.
543
     */
544 498
    public function generateNavigation(): string
545
    {
546
        // Fetch sheets
547 498
        $sheets = [];
548 498
        if ($this->sheetIndex === null) {
549 8
            $sheets = $this->spreadsheet->getAllSheets();
550
        } else {
551 496
            $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex);
552
        }
553
554
        // Construct HTML
555 498
        $html = '';
556
557
        // Only if there are more than 1 sheets
558 498
        if (count($sheets) > 1) {
559
            // Loop all sheets
560 8
            $sheetId = 0;
561
562 8
            $html .= '<ul class="navigation">' . PHP_EOL;
563
564 8
            foreach ($sheets as $sheet) {
565 8
                $html .= '  <li class="sheet' . $sheetId . '"><a href="#sheet' . $sheetId . '">' . htmlspecialchars($sheet->getTitle()) . '</a></li>' . PHP_EOL;
566 8
                ++$sheetId;
567
            }
568
569 8
            $html .= '</ul>' . PHP_EOL;
570
        }
571
572 498
        return $html;
573
    }
574
575 528
    private function extendRowsAndColumns(Worksheet $worksheet, int &$colMax, int &$rowMax): void
576
    {
577 528
        if ($this->includeCharts) {
578 4
            foreach ($worksheet->getChartCollection() as $chart) {
579 4
                if ($chart instanceof Chart) {
580 4
                    $chartCoordinates = $chart->getTopLeftPosition();
581 4
                    $this->sheetCharts[$chartCoordinates['cell']] = $chart;
582 4
                    $chartTL = Coordinate::indexesFromString($chartCoordinates['cell']);
583 4
                    if ($chartTL[1] > $rowMax) {
584 2
                        $rowMax = $chartTL[1];
585
                    }
586 4
                    if ($chartTL[0] > $colMax) {
587 2
                        $colMax = $chartTL[0];
588
                    }
589
                }
590
            }
591
        }
592 528
        foreach ($worksheet->getDrawingCollection() as $drawing) {
593 26
            if ($drawing instanceof Drawing && $drawing->getPath() === '') {
594 2
                continue;
595
            }
596 25
            $imageTL = Coordinate::indexesFromString($drawing->getCoordinates());
597 25
            $this->sheetDrawings[$drawing->getCoordinates()] = $drawing;
598 25
            if ($imageTL[1] > $rowMax) {
599
                $rowMax = $imageTL[1];
600
            }
601 25
            if ($imageTL[0] > $colMax) {
602
                $colMax = $imageTL[0];
603
            }
604
        }
605
    }
606
607
    /**
608
     * Convert Windows file name to file protocol URL.
609
     *
610
     * @param string $filename file name on local system
611
     */
612 19
    public static function winFileToUrl(string $filename, bool $mpdf = false): string
613
    {
614
        // Windows filename
615 19
        if (substr($filename, 1, 2) === ':\\') {
616 1
            $protocol = $mpdf ? '' : 'file:///';
617 1
            $filename = $protocol . str_replace('\\', '/', $filename);
618
        }
619
620 19
        return $filename;
621
    }
622
623
    /**
624
     * Generate image tag in cell.
625
     *
626
     * @param string $coordinates Cell coordinates
627
     */
628 528
    private function writeImageInCell(string $coordinates): string
629
    {
630
        // Construct HTML
631 528
        $html = '';
632
633
        // Write images
634 528
        $drawing = $this->sheetDrawings[$coordinates] ?? null;
635 528
        if ($drawing !== null) {
636 25
            $opacity = '';
637 25
            $opacityValue = $drawing->getOpacity();
638 25
            if ($opacityValue !== null) {
639 3
                $opacityValue = $opacityValue / 100000;
640 3
                if ($opacityValue >= 0.0 && $opacityValue <= 1.0) {
641 3
                    $opacity = "opacity:$opacityValue; ";
642
                }
643
            }
644 25
            $filedesc = $drawing->getDescription();
645 25
            $filedesc = $filedesc ? htmlspecialchars($filedesc, ENT_QUOTES) : 'Embedded image';
646 25
            if ($drawing instanceof Drawing && $drawing->getPath() !== '') {
647 18
                $filename = $drawing->getPath();
648
649
                // Strip off eventual '.'
650 18
                $filename = Preg::replace('/^[.]/', '', $filename);
651
652
                // Prepend images root
653 18
                $filename = $this->getImagesRoot() . $filename;
654
655
                // Strip off eventual '.' if followed by non-/
656 18
                $filename = Preg::replace('@^[.]([^/])@', '$1', $filename);
657
658
                // Convert UTF8 data to PCDATA
659 18
                $filename = htmlspecialchars($filename, Settings::htmlEntityFlags());
660
661 18
                $html .= PHP_EOL;
662 18
                $imageData = self::winFileToUrl($filename, $this instanceof Pdf\Mpdf);
663
664 18
                if ($this->embedImages || str_starts_with($imageData, 'zip://')) {
665 11
                    $imageData = 'data:,';
666 11
                    $picture = @file_get_contents($filename);
667 11
                    if ($picture !== false) {
668 11
                        $mimeContentType = (string) @mime_content_type($filename);
669 11
                        if (str_starts_with($mimeContentType, 'image/')) {
670
                            // base64 encode the binary data
671 11
                            $base64 = base64_encode($picture);
672 11
                            $imageData = 'data:' . $mimeContentType . ';base64,' . $base64;
673
                        }
674
                    }
675
                }
676
677 18
                $html .= '<img style="' . $opacity . 'position: absolute; z-index: 1; left: '
678 18
                    . $drawing->getOffsetX() . 'px; top: ' . $drawing->getOffsetY() . 'px; width: '
679 18
                    . $drawing->getWidth() . 'px; height: ' . $drawing->getHeight() . 'px;" src="'
680 18
                    . $imageData . '" alt="' . $filedesc . '" />';
681 7
            } elseif ($drawing instanceof MemoryDrawing) {
682 7
                $imageResource = $drawing->getImageResource();
683 7
                if ($imageResource) {
684 7
                    ob_start(); //  Let's start output buffering.
685 7
                    imagepng($imageResource); //  This will normally output the image, but because of ob_start(), it won't.
686 7
                    $contents = (string) ob_get_contents(); //  Instead, output above is saved to $contents
687 7
                    ob_end_clean(); //  End the output buffer.
688
689 7
                    $dataUri = 'data:image/png;base64,' . base64_encode($contents);
690
691
                    //  Because of the nature of tables, width is more important than height.
692
                    //  max-width: 100% ensures that image doesnt overflow containing cell
693
                    //    However, PR #3535 broke test
694
                    //    25_In_memory_image, apparently because
695
                    //    of the use of max-with. In addition,
696
                    //    non-memory-drawings don't use max-width.
697
                    //    Its use here is suspect and is being eliminated.
698
                    //  width: X sets width of supplied image.
699
                    //  As a result, images bigger than cell will be contained and images smaller will not get stretched
700 7
                    $html .= '<img alt="' . $filedesc . '" src="' . $dataUri . '" style="' . $opacity . 'width:' . $drawing->getWidth() . 'px;left: '
701 7
                        . $drawing->getOffsetX() . 'px; top: ' . $drawing->getOffsetY() . 'px;position: absolute; z-index: 1;" />';
702
                }
703
            }
704
        }
705
706 528
        return $html;
707
    }
708
709
    /**
710
     * Generate chart tag in cell.
711
     * This code should be exercised by sample:
712
     * Chart/32_Chart_read_write_PDF.php.
713
     */
714 4
    private function writeChartInCell(Worksheet $worksheet, string $coordinates): string
715
    {
716
        // Construct HTML
717 4
        $html = '';
718
719
        // Write charts
720 4
        $chart = $this->sheetCharts[$coordinates] ?? null;
721 4
        if ($chart !== null) {
722 4
            $chartCoordinates = $chart->getTopLeftPosition();
723 4
            $chartFileName = File::sysGetTempDir() . '/' . uniqid('', true) . '.png';
724 4
            $renderedWidth = $chart->getRenderedWidth();
725 4
            $renderedHeight = $chart->getRenderedHeight();
726 4
            if ($renderedWidth === null || $renderedHeight === null) {
727 4
                $this->adjustRendererPositions($chart, $worksheet);
728
            }
729 4
            $title = $chart->getTitle();
730 4
            $caption = null;
731 4
            $filedesc = '';
732 4
            if ($title !== null) {
733 4
                $calculatedTitle = $title->getCalculatedTitle($worksheet->getParent());
734 4
                if ($calculatedTitle !== null) {
735 2
                    $caption = $title->getCaption();
736 2
                    $title->setCaption($calculatedTitle);
737
                }
738 4
                $filedesc = $title->getCaptionText($worksheet->getParent());
739
            }
740 4
            $renderSuccessful = $chart->render($chartFileName);
741 4
            $chart->setRenderedWidth($renderedWidth);
742 4
            $chart->setRenderedHeight($renderedHeight);
743 4
            if (isset($title, $caption)) {
744 2
                $title->setCaption($caption);
745
            }
746 4
            if (!$renderSuccessful) {
747
                return '';
748
            }
749
750 4
            $html .= PHP_EOL;
751 4
            $imageDetails = getimagesize($chartFileName) ?: ['', '', 'mime' => ''];
752
753 4
            $filedesc = $filedesc ? htmlspecialchars($filedesc, ENT_QUOTES) : 'Embedded chart';
754 4
            $picture = file_get_contents($chartFileName);
755 4
            unlink($chartFileName);
756 4
            if ($picture !== false) {
757 4
                $base64 = base64_encode($picture);
758 4
                $imageData = 'data:' . $imageDetails['mime'] . ';base64,' . $base64;
759
760 4
                $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;
761
            }
762
        }
763
764
        // Return
765 4
        return $html;
766
    }
767
768 4
    private function adjustRendererPositions(Chart $chart, Worksheet $sheet): void
769
    {
770 4
        $topLeft = $chart->getTopLeftPosition();
771 4
        $bottomRight = $chart->getBottomRightPosition();
772 4
        $tlCell = $topLeft['cell'];
773 4
        $brCell = $bottomRight['cell'];
774 4
        if ($tlCell !== '' && $brCell !== '') {
775 4
            $tlCoordinate = Coordinate::indexesFromString($tlCell);
776 4
            $brCoordinate = Coordinate::indexesFromString($brCell);
777 4
            $totalHeight = 0.0;
778 4
            $totalWidth = 0.0;
779 4
            $defaultRowHeight = $sheet->getDefaultRowDimension()->getRowHeight();
780 4
            $defaultRowHeight = SharedDrawing::pointsToPixels(($defaultRowHeight >= 0) ? $defaultRowHeight : SharedFont::getDefaultRowHeightByFont($this->defaultFont));
781 4
            if ($tlCoordinate[1] <= $brCoordinate[1] && $tlCoordinate[0] <= $brCoordinate[0]) {
782 4
                for ($row = $tlCoordinate[1]; $row <= $brCoordinate[1]; ++$row) {
783 4
                    $height = $sheet->getRowDimension($row)->getRowHeight('pt');
784 4
                    $totalHeight += ($height >= 0) ? $height : $defaultRowHeight;
785
                }
786 4
                $rightEdge = $brCoordinate[2];
787 4
                ++$rightEdge;
788 4
                for ($column = $tlCoordinate[2]; $column !== $rightEdge; ++$column) {
789 4
                    $width = $sheet->getColumnDimension($column)->getWidth();
790 4
                    $width = ($width < 0) ? self::DEFAULT_CELL_WIDTH_PIXELS : SharedDrawing::cellDimensionToPixels($sheet->getColumnDimension($column)->getWidth(), $this->defaultFont);
791 4
                    $totalWidth += $width;
792
                }
793 4
                $chart->setRenderedWidth($totalWidth);
794 4
                $chart->setRenderedHeight($totalHeight);
795
            }
796
        }
797
    }
798
799
    /**
800
     * Generate CSS styles.
801
     *
802
     * @param bool $generateSurroundingHTML Generate surrounding HTML tags? (&lt;style&gt; and &lt;/style&gt;)
803
     */
804 522
    public function generateStyles(bool $generateSurroundingHTML = true): string
805
    {
806
        // Build CSS
807 522
        $css = $this->buildCSS($generateSurroundingHTML);
808
809
        // Construct HTML
810 522
        $html = '';
811
812
        // Start styles
813 522
        if ($generateSurroundingHTML) {
814 522
            $html .= '    <style type="text/css">' . PHP_EOL;
815 522
            $html .= (array_key_exists('html', $css)) ? ('      html { ' . $this->assembleCSS($css['html']) . ' }' . PHP_EOL) : '';
816
        }
817
818
        // Write all other styles
819 522
        foreach ($css as $styleName => $styleDefinition) {
820 522
            if ($styleName != 'html') {
821 522
                $html .= '      ' . $styleName . ' { ' . $this->assembleCSS($styleDefinition) . ' }' . PHP_EOL;
822
            }
823
        }
824 522
        $html .= $this->generatePageDeclarations(false);
825
826
        // End styles
827 522
        if ($generateSurroundingHTML) {
828 522
            $html .= '    </style>' . PHP_EOL;
829
        }
830
831
        // Return
832 522
        return $html;
833
    }
834
835 530
    private function buildCssRowHeights(Worksheet $sheet, array &$css, int $sheetIndex): void
836
    {
837
        // Calculate row heights
838 530
        foreach ($sheet->getRowDimensions() as $rowDimension) {
839 24
            $row = $rowDimension->getRowIndex() - 1;
840
841
            // table.sheetN tr.rowYYYYYY { }
842 24
            $css['table.sheet' . $sheetIndex . ' tr.row' . $row] = [];
843
844 24
            if ($rowDimension->getRowHeight() != -1) {
845 14
                $pt_height = $rowDimension->getRowHeight();
846 14
                $css['table.sheet' . $sheetIndex . ' tr.row' . $row]['height'] = $pt_height . 'pt';
847
            }
848 24
            if ($rowDimension->getVisible() === false) {
849 8
                $css['table.sheet' . $sheetIndex . ' tr.row' . $row]['display'] = 'none';
850 8
                $css['table.sheet' . $sheetIndex . ' tr.row' . $row]['visibility'] = 'hidden';
851
            }
852
        }
853
    }
854
855 530
    private function buildCssPerSheet(Worksheet $sheet, array &$css): void
856
    {
857
        // Calculate hash code
858 530
        $sheetIndex = $sheet->getParentOrThrow()->getIndex($sheet);
859 530
        $setup = $sheet->getPageSetup();
860 530
        if ($setup->getFitToPage() && $setup->getFitToHeight() === 1) {
861 3
            $css["table.sheet$sheetIndex"]['page-break-inside'] = 'avoid';
862 3
            $css["table.sheet$sheetIndex"]['break-inside'] = 'avoid';
863
        }
864 530
        $picture = $sheet->getBackgroundImage();
865 530
        if ($picture !== '') {
866 1
            $base64 = base64_encode($picture);
867 1
            $css["table.sheet$sheetIndex"]['background-image'] = 'url(data:' . $sheet->getBackgroundMime() . ';base64,' . $base64 . ')';
868
        }
869
870
        // Build styles
871
        // Calculate column widths
872 530
        $sheet->calculateColumnWidths();
873
874
        // col elements, initialize
875 530
        $highestColumnIndex = Coordinate::columnIndexFromString($sheet->getHighestColumn()) - 1;
876 530
        $column = -1;
877 530
        $colStr = 'A';
878 530
        while ($column++ < $highestColumnIndex) {
879 530
            $this->columnWidths[$sheetIndex][$column] = self::DEFAULT_CELL_WIDTH_POINTS; // approximation
880 530
            if ($this->shouldGenerateColumn($sheet, $colStr)) {
881 530
                $css['table.sheet' . $sheetIndex . ' col.col' . $column]['width'] = self::DEFAULT_CELL_WIDTH_POINTS . 'pt';
882
            }
883 530
            ++$colStr;
884
        }
885
886
        // col elements, loop through columnDimensions and set width
887 530
        foreach ($sheet->getColumnDimensions() as $columnDimension) {
888 30
            $column = Coordinate::columnIndexFromString($columnDimension->getColumnIndex()) - 1;
889 30
            $width = SharedDrawing::cellDimensionToPixels($columnDimension->getWidth(), $this->defaultFont);
890 30
            $width = SharedDrawing::pixelsToPoints($width);
891 30
            if ($columnDimension->getVisible() === false) {
892 10
                $css['table.sheet' . $sheetIndex . ' .column' . $column]['display'] = 'none';
893
                // This would be better but Firefox has an 11-year-old bug.
894
                // https://bugzilla.mozilla.org/show_bug.cgi?id=819045
895
                //$css['table.sheet' . $sheetIndex . ' col.col' . $column]['visibility'] = 'collapse';
896
            }
897 30
            if ($width >= 0) {
898 19
                $this->columnWidths[$sheetIndex][$column] = $width;
899 19
                $css['table.sheet' . $sheetIndex . ' col.col' . $column]['width'] = $width . 'pt';
900
            }
901
        }
902
903
        // Default row height
904 530
        $rowDimension = $sheet->getDefaultRowDimension();
905
906
        // table.sheetN tr { }
907 530
        $css['table.sheet' . $sheetIndex . ' tr'] = [];
908
909 530
        if ($rowDimension->getRowHeight() == -1) {
910 522
            $pt_height = SharedFont::getDefaultRowHeightByFont($this->spreadsheet->getDefaultStyle()->getFont());
911
        } else {
912 8
            $pt_height = $rowDimension->getRowHeight();
913
        }
914 530
        $css['table.sheet' . $sheetIndex . ' tr']['height'] = $pt_height . 'pt';
915 530
        if ($rowDimension->getVisible() === false) {
916 1
            $css['table.sheet' . $sheetIndex . ' tr']['display'] = 'none';
917 1
            $css['table.sheet' . $sheetIndex . ' tr']['visibility'] = 'hidden';
918
        }
919
920 530
        $this->buildCssRowHeights($sheet, $css, $sheetIndex);
921
    }
922
923
    /**
924
     * Build CSS styles.
925
     *
926
     * @param bool $generateSurroundingHTML Generate surrounding HTML style? (html { })
927
     */
928 530
    public function buildCSS(bool $generateSurroundingHTML = true): array
929
    {
930
        // Cached?
931 530
        if ($this->cssStyles !== null) {
932 520
            return $this->cssStyles;
933
        }
934
935
        // Ensure that spans have been calculated
936 530
        $this->calculateSpans();
937
938
        // Construct CSS
939 530
        $css = [];
940
941
        // Start styles
942 530
        if ($generateSurroundingHTML) {
943
            // html { }
944 521
            $css['html']['font-family'] = 'Calibri, Arial, Helvetica, sans-serif';
945 521
            $css['html']['font-size'] = '11pt';
946 521
            $css['html']['background-color'] = 'white';
947
        }
948
949
        // CSS for comments as found in LibreOffice
950 530
        $css['a.comment-indicator:hover + div.comment'] = [
951 530
            'background' => '#ffd',
952 530
            'position' => 'absolute',
953 530
            'display' => 'block',
954 530
            'border' => '1px solid black',
955 530
            'padding' => '0.5em',
956 530
        ];
957
958 530
        $css['a.comment-indicator'] = [
959 530
            'background' => 'red',
960 530
            'display' => 'inline-block',
961 530
            'border' => '1px solid black',
962 530
            'width' => '0.5em',
963 530
            'height' => '0.5em',
964 530
        ];
965
966 530
        $css['div.comment']['display'] = 'none';
967
968
        // table { }
969 530
        $css['table']['border-collapse'] = 'collapse';
970
971
        // .b {}
972 530
        $css['.b']['text-align'] = 'center'; // BOOL
973
974
        // .e {}
975 530
        $css['.e']['text-align'] = 'center'; // ERROR
976
977
        // .f {}
978 530
        $css['.f']['text-align'] = 'right'; // FORMULA
979
980
        // .inlineStr {}
981 530
        $css['.inlineStr']['text-align'] = 'left'; // INLINE
982
983
        // .n {}
984 530
        $css['.n']['text-align'] = 'right'; // NUMERIC
985
986
        // .s {}
987 530
        $css['.s']['text-align'] = 'left'; // STRING
988
989
        // Calculate cell style hashes
990 530
        foreach ($this->spreadsheet->getCellXfCollection() as $index => $style) {
991 530
            $css['td.style' . $index . ', th.style' . $index] = $this->createCSSStyle($style);
992
            //$css['th.style' . $index] = $this->createCSSStyle($style);
993
        }
994
995
        // Fetch sheets
996 530
        $sheets = [];
997 530
        if ($this->sheetIndex === null) {
998 15
            $sheets = $this->spreadsheet->getAllSheets();
999
        } else {
1000 522
            $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex);
1001
        }
1002
1003
        // Build styles per sheet
1004 530
        foreach ($sheets as $sheet) {
1005 530
            $this->buildCssPerSheet($sheet, $css);
1006
        }
1007
1008
        // Cache
1009 530
        if ($this->cssStyles === null) {
1010 530
            $this->cssStyles = $css;
1011
        }
1012
1013
        // Return
1014 530
        return $css;
1015
    }
1016
1017
    /**
1018
     * Create CSS style.
1019
     */
1020 530
    private function createCSSStyle(Style $style): array
1021
    {
1022
        // Create CSS
1023 530
        return array_merge(
1024 530
            $this->createCSSStyleAlignment($style->getAlignment()),
1025 530
            $this->createCSSStyleBorders($style->getBorders()),
1026 530
            $this->createCSSStyleFont($style->getFont()),
1027 530
            $this->createCSSStyleFill($style->getFill())
1028 530
        );
1029
    }
1030
1031
    /**
1032
     * Create CSS style.
1033
     */
1034 530
    private function createCSSStyleAlignment(Alignment $alignment): array
1035
    {
1036
        // Construct CSS
1037 530
        $css = [];
1038
1039
        // Create CSS
1040 530
        $verticalAlign = $this->mapVAlign($alignment->getVertical() ?? '');
1041 530
        if ($verticalAlign) {
1042 530
            $css['vertical-align'] = $verticalAlign;
1043
        }
1044 530
        $textAlign = $this->mapHAlign($alignment->getHorizontal() ?? '');
1045 530
        if ($textAlign) {
1046 18
            $css['text-align'] = $textAlign;
1047 18
            if (in_array($textAlign, ['left', 'right'])) {
1048 11
                $css['padding-' . $textAlign] = (string) ((int) $alignment->getIndent() * 9) . 'px';
1049
            }
1050
        }
1051 530
        $rotation = $alignment->getTextRotation();
1052 530
        if ($rotation !== 0 && $rotation !== Alignment::TEXTROTATION_STACK_PHPSPREADSHEET) {
1053 5
            if ($this instanceof Pdf\Mpdf) {
1054 1
                $css['text-rotate'] = "$rotation";
1055
            } else {
1056 4
                $css['transform'] = "rotate({$rotation}deg)";
1057
            }
1058
        }
1059
1060 530
        return $css;
1061
    }
1062
1063
    /**
1064
     * Create CSS style.
1065
     */
1066 530
    private function createCSSStyleFont(Font $font): array
1067
    {
1068
        // Construct CSS
1069 530
        $css = [];
1070
1071
        // Create CSS
1072 530
        if ($font->getBold()) {
1073 20
            $css['font-weight'] = 'bold';
1074
        }
1075 530
        if ($font->getUnderline() != Font::UNDERLINE_NONE && $font->getStrikethrough()) {
1076 1
            $css['text-decoration'] = 'underline line-through';
1077 530
        } elseif ($font->getUnderline() != Font::UNDERLINE_NONE) {
1078 12
            $css['text-decoration'] = 'underline';
1079 530
        } elseif ($font->getStrikethrough()) {
1080 1
            $css['text-decoration'] = 'line-through';
1081
        }
1082 530
        if ($font->getItalic()) {
1083 10
            $css['font-style'] = 'italic';
1084
        }
1085
1086 530
        $css['color'] = '#' . $font->getColor()->getRGB();
1087 530
        $css['font-family'] = '\'' . htmlspecialchars((string) $font->getName(), ENT_QUOTES) . '\'';
1088 530
        $css['font-size'] = $font->getSize() . 'pt';
1089
1090 530
        return $css;
1091
    }
1092
1093
    /**
1094
     * Create CSS style.
1095
     *
1096
     * @param Borders $borders Borders
1097
     */
1098 530
    private function createCSSStyleBorders(Borders $borders): array
1099
    {
1100
        // Construct CSS
1101 530
        $css = [];
1102
1103
        // Create CSS
1104 530
        if (!($this instanceof Pdf\Mpdf)) {
1105 514
            $css['border-bottom'] = $this->createCSSStyleBorder($borders->getBottom());
1106 514
            $css['border-top'] = $this->createCSSStyleBorder($borders->getTop());
1107 514
            $css['border-left'] = $this->createCSSStyleBorder($borders->getLeft());
1108 514
            $css['border-right'] = $this->createCSSStyleBorder($borders->getRight());
1109
        } else {
1110
            // Mpdf doesn't process !important, so omit unimportant border none
1111 22
            if ($borders->getBottom()->getBorderStyle() !== Border::BORDER_NONE) {
1112 6
                $css['border-bottom'] = $this->createCSSStyleBorder($borders->getBottom());
1113
            }
1114 22
            if ($borders->getTop()->getBorderStyle() !== Border::BORDER_NONE) {
1115 6
                $css['border-top'] = $this->createCSSStyleBorder($borders->getTop());
1116
            }
1117 22
            if ($borders->getLeft()->getBorderStyle() !== Border::BORDER_NONE) {
1118 6
                $css['border-left'] = $this->createCSSStyleBorder($borders->getLeft());
1119
            }
1120 22
            if ($borders->getRight()->getBorderStyle() !== Border::BORDER_NONE) {
1121 6
                $css['border-right'] = $this->createCSSStyleBorder($borders->getRight());
1122
            }
1123
        }
1124
1125 530
        return $css;
1126
    }
1127
1128
    /**
1129
     * Create CSS style.
1130
     *
1131
     * @param Border $border Border
1132
     */
1133 519
    private function createCSSStyleBorder(Border $border): string
1134
    {
1135
        //    Create CSS - add !important to non-none border styles for merged cells
1136 519
        $borderStyle = $this->mapBorderStyle($border->getBorderStyle());
1137
1138 519
        return $borderStyle . ' #' . $border->getColor()->getRGB() . (($borderStyle === self::BORDER_NONE) ? '' : ' !important');
1139
    }
1140
1141
    /**
1142
     * Create CSS style (Fill).
1143
     *
1144
     * @param Fill $fill Fill
1145
     */
1146 530
    private function createCSSStyleFill(Fill $fill): array
1147
    {
1148
        // Construct HTML
1149 530
        $css = [];
1150
1151
        // Create CSS
1152 530
        if ($fill->getFillType() !== Fill::FILL_NONE) {
1153
            if (
1154 12
                (in_array($fill->getFillType(), ['', Fill::FILL_SOLID], true) || !$fill->getEndColor()->getRGB())
1155 12
                && $fill->getStartColor()->getRGB()
1156
            ) {
1157 12
                $value = '#' . $fill->getStartColor()->getRGB();
1158 12
                $css['background-color'] = $value;
1159 9
            } elseif ($fill->getEndColor()->getRGB()) {
1160 9
                $value = '#' . $fill->getEndColor()->getRGB();
1161 9
                $css['background-color'] = $value;
1162
            }
1163
        }
1164
1165 530
        return $css;
1166
    }
1167
1168
    /**
1169
     * Generate HTML footer.
1170
     */
1171 528
    public function generateHTMLFooter(): string
1172
    {
1173
        // Construct HTML
1174 528
        $html = '';
1175 528
        $html .= '  </body>' . PHP_EOL;
1176 528
        $html .= '</html>' . PHP_EOL;
1177
1178 528
        return $html;
1179
    }
1180
1181 14
    private function generateTableTagInline(Worksheet $worksheet, string $id): string
1182
    {
1183 14
        $style = isset($this->cssStyles['table'])
1184 14
            ? $this->assembleCSS($this->cssStyles['table']) : '';
1185
1186 14
        $prntgrid = $worksheet->getPrintGridlines();
1187 14
        $viewgrid = $this->isPdf ? $prntgrid : $worksheet->getShowGridlines();
1188 14
        if ($viewgrid && $prntgrid) {
1189 1
            $html = "    <table border='1' cellpadding='1' $id cellspacing='1' style='$style' class='gridlines gridlinesp'>" . PHP_EOL;
1190 14
        } elseif ($viewgrid) {
1191 7
            $html = "    <table border='0' cellpadding='0' $id cellspacing='0' style='$style' class='gridlines'>" . PHP_EOL;
1192 8
        } elseif ($prntgrid) {
1193 1
            $html = "    <table border='0' cellpadding='0' $id cellspacing='0' style='$style' class='gridlinesp'>" . PHP_EOL;
1194
        } else {
1195 8
            $html = "    <table border='0' cellpadding='1' $id cellspacing='0' style='$style'>" . PHP_EOL;
1196
        }
1197
1198 14
        return $html;
1199
    }
1200
1201 528
    private function generateTableTag(Worksheet $worksheet, string $id, string &$html, int $sheetIndex): void
1202
    {
1203 528
        if (!$this->useInlineCss) {
1204 520
            $gridlines = $worksheet->getShowGridlines() ? ' gridlines' : '';
1205 520
            $gridlinesp = $worksheet->getPrintGridlines() ? ' gridlinesp' : '';
1206 520
            $html .= "    <table border='0' cellpadding='0' cellspacing='0' $id class='sheet$sheetIndex$gridlines$gridlinesp'>" . PHP_EOL;
1207
        } else {
1208 14
            $html .= $this->generateTableTagInline($worksheet, $id);
1209
        }
1210
    }
1211
1212
    /**
1213
     * Generate table header.
1214
     *
1215
     * @param Worksheet $worksheet The worksheet for the table we are writing
1216
     * @param bool $showid whether or not to add id to table tag
1217
     */
1218 528
    private function generateTableHeader(Worksheet $worksheet, bool $showid = true): string
1219
    {
1220 528
        $sheetIndex = $worksheet->getParentOrThrow()->getIndex($worksheet);
1221
1222
        // Construct HTML
1223 528
        $html = '';
1224 528
        $id = $showid ? "id='sheet$sheetIndex'" : '';
1225 528
        if ($showid) {
1226 528
            $html .= "<div style='page: page$sheetIndex'>" . PHP_EOL;
1227
        } else {
1228 2
            $html .= "<div style='page: page$sheetIndex' class='scrpgbrk'>" . PHP_EOL;
1229
        }
1230
1231 528
        $this->generateTableTag($worksheet, $id, $html, $sheetIndex);
1232
1233
        // Write <col> elements
1234 528
        $highestColumnIndex = Coordinate::columnIndexFromString($worksheet->getHighestColumn()) - 1;
1235 528
        $i = -1;
1236 528
        while ($i++ < $highestColumnIndex) {
1237 528
            if (!$this->useInlineCss) {
1238 520
                $html .= '        <col class="col' . $i . '" />' . PHP_EOL;
1239
            } else {
1240 14
                $style = isset($this->cssStyles['table.sheet' . $sheetIndex . ' col.col' . $i])
1241 14
                    ? $this->assembleCSS($this->cssStyles['table.sheet' . $sheetIndex . ' col.col' . $i]) : '';
1242 14
                $html .= '        <col style="' . $style . '" />' . PHP_EOL;
1243
            }
1244
        }
1245
1246 528
        return $html;
1247
    }
1248
1249
    /**
1250
     * Generate table footer.
1251
     */
1252 528
    private function generateTableFooter(): string
1253
    {
1254 528
        return '    </tbody></table>' . PHP_EOL . '</div>' . PHP_EOL;
1255
    }
1256
1257
    /**
1258
     * Generate row start.
1259
     *
1260
     * @param int $sheetIndex Sheet index (0-based)
1261
     * @param int $row row number
1262
     */
1263 528
    private function generateRowStart(Worksheet $worksheet, int $sheetIndex, int $row): string
1264
    {
1265 528
        $html = '';
1266 528
        if (count($worksheet->getBreaks()) > 0) {
1267 2
            $breaks = $worksheet->getRowBreaks();
1268
1269
            // check if a break is needed before this row
1270 2
            if (isset($breaks['A' . $row])) {
1271
                // close table: </table>
1272 2
                $html .= $this->generateTableFooter();
1273 2
                if ($this->isPdf && $this->useInlineCss) {
1274 1
                    $html .= '<div style="page-break-before:always" />';
1275
                }
1276
1277
                // open table again: <table> + <col> etc.
1278 2
                $html .= $this->generateTableHeader($worksheet, false);
1279 2
                $html .= '<tbody>' . PHP_EOL;
1280
            }
1281
        }
1282
1283
        // Write row start
1284 528
        if (!$this->useInlineCss) {
1285 520
            $html .= '          <tr class="row' . $row . '">' . PHP_EOL;
1286
        } else {
1287 14
            $style = isset($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $row])
1288 14
                ? $this->assembleCSS($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $row]) : '';
1289
1290 14
            $html .= '          <tr style="' . $style . '">' . PHP_EOL;
1291
        }
1292
1293 528
        return $html;
1294
    }
1295
1296 528
    private function generateRowCellCss(Worksheet $worksheet, string $cellAddress, int $row, int $columnNumber): array
1297
    {
1298 528
        $cell = ($cellAddress > '') ? $worksheet->getCellCollection()->get($cellAddress) : '';
1299 528
        $coordinate = Coordinate::stringFromColumnIndex($columnNumber + 1) . ($row + 1);
1300 528
        if (!$this->useInlineCss) {
1301 520
            $cssClass = 'column' . $columnNumber;
1302
        } else {
1303 14
            $cssClass = [];
1304
            // The statements below do nothing.
1305
            // Commenting out the code rather than deleting it
1306
            // in case someone can figure out what their intent was.
1307
            //if ($cellType == 'th') {
1308
            //    if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' th.column' . $colNum])) {
1309
            //        $this->cssStyles['table.sheet' . $sheetIndex . ' th.column' . $colNum];
1310
            //    }
1311
            //} else {
1312
            //    if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' td.column' . $colNum])) {
1313
            //        $this->cssStyles['table.sheet' . $sheetIndex . ' td.column' . $colNum];
1314
            //    }
1315
            //}
1316
            // End of mystery statements.
1317
        }
1318
1319 528
        return [$cell, $cssClass, $coordinate];
1320
    }
1321
1322 31
    private function generateRowCellDataValueRich(RichText $richText): string
1323
    {
1324 31
        $cellData = '';
1325
        // Loop through rich text elements
1326 31
        $elements = $richText->getRichTextElements();
1327 31
        foreach ($elements as $element) {
1328
            // Rich text start?
1329 31
            if ($element instanceof Run) {
1330 15
                $cellEnd = '';
1331 15
                if ($element->getFont() !== null) {
1332 15
                    $cellData .= '<span style="' . $this->assembleCSS($this->createCSSStyleFont($element->getFont())) . '">';
1333
1334 15
                    if ($element->getFont()->getSuperscript()) {
1335 1
                        $cellData .= '<sup>';
1336 1
                        $cellEnd = '</sup>';
1337 15
                    } elseif ($element->getFont()->getSubscript()) {
1338 1
                        $cellData .= '<sub>';
1339 1
                        $cellEnd = '</sub>';
1340
                    }
1341
                } else {
1342
                    $cellData .= '<span>';
1343
                }
1344
1345
                // Convert UTF8 data to PCDATA
1346 15
                $cellText = $element->getText();
1347 15
                $cellData .= htmlspecialchars($cellText, Settings::htmlEntityFlags());
1348
1349 15
                $cellData .= $cellEnd;
1350
1351 15
                $cellData .= '</span>';
1352
            } else {
1353
                // Convert UTF8 data to PCDATA
1354 27
                $cellText = $element->getText();
1355 27
                $cellData .= htmlspecialchars($cellText, Settings::htmlEntityFlags());
1356
            }
1357
        }
1358
1359 31
        return nl2br($cellData);
1360
    }
1361
1362 526
    private function generateRowCellDataValue(Worksheet $worksheet, Cell $cell, string &$cellData): void
1363
    {
1364 526
        if ($cell->getValue() instanceof RichText) {
1365 11
            $cellData .= $this->generateRowCellDataValueRich($cell->getValue());
1366
        } else {
1367 526
            if ($this->preCalculateFormulas) {
1368 525
                $origData = $cell->getCalculatedValue();
1369 525
                if ($this->betterBoolean && is_bool($origData)) {
1370 3
                    $origData2 = $origData ? $this->getTrue : $this->getFalse;
1371
                } else {
1372 525
                    $origData2 = $cell->getCalculatedValueString();
1373
                }
1374
            } else {
1375 1
                $origData = $cell->getValue();
1376 1
                if ($this->betterBoolean && is_bool($origData)) {
1377
                    $origData2 = $origData ? $this->getTrue : $this->getFalse;
1378
                } else {
1379 1
                    $origData2 = $cell->getValueString();
1380
                }
1381
            }
1382 526
            $formatCode = $worksheet->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())->getNumberFormat()->getFormatCode();
1383
1384 526
            $cellData = NumberFormat::toFormattedString(
1385 526
                $origData2,
1386 526
                $formatCode ?? NumberFormat::FORMAT_GENERAL,
1387 526
                [$this, 'formatColor']
1388 526
            );
1389
1390 526
            if ($cellData === $origData) {
1391 104
                $cellData = htmlspecialchars($cellData, Settings::htmlEntityFlags());
1392
            }
1393 526
            if ($worksheet->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())->getFont()->getSuperscript()) {
1394 1
                $cellData = '<sup>' . $cellData . '</sup>';
1395 526
            } elseif ($worksheet->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())->getFont()->getSubscript()) {
1396 1
                $cellData = '<sub>' . $cellData . '</sub>';
1397
            }
1398
        }
1399
    }
1400
1401 528
    private function generateRowCellData(Worksheet $worksheet, null|Cell|string $cell, array|string &$cssClass): string
1402
    {
1403 528
        $cellData = '&nbsp;';
1404 528
        if ($cell instanceof Cell) {
1405 526
            $cellData = '';
1406
            // Don't know what this does, and no test cases.
1407
            //if ($cell->getParent() === null) {
1408
            //    $cell->attach($worksheet);
1409
            //}
1410
            // Value
1411 526
            $this->generateRowCellDataValue($worksheet, $cell, $cellData);
1412
1413
            // Converts the cell content so that spaces occuring at beginning of each new line are replaced by &nbsp;
1414
            // Example: "  Hello\n to the world" is converted to "&nbsp;&nbsp;Hello\n&nbsp;to the world"
1415 526
            $cellData = Preg::replace('/(?m)(?:^|\\G) /', '&nbsp;', $cellData);
1416
1417
            // convert newline "\n" to '<br>'
1418 526
            $cellData = nl2br($cellData);
1419
1420
            // Extend CSS class?
1421 526
            $dataType = $cell->getDataType();
1422 526
            if ($this->betterBoolean && $this->preCalculateFormulas && $dataType === DataType::TYPE_FORMULA) {
1423 3
                $calculatedValue = $cell->getCalculatedValue();
1424 3
                if (is_bool($calculatedValue)) {
1425 3
                    $dataType = DataType::TYPE_BOOL;
1426 3
                } elseif (is_numeric($calculatedValue)) {
1427 3
                    $dataType = DataType::TYPE_NUMERIC;
1428 3
                } elseif (is_string($calculatedValue)) {
1429 3
                    $dataType = DataType::TYPE_STRING;
1430
                }
1431
            }
1432 526
            if (!$this->useInlineCss && is_string($cssClass)) {
1433 519
                $cssClass .= ' style' . $cell->getXfIndex();
1434 519
                $cssClass .= ' ' . $dataType;
1435 13
            } elseif (is_array($cssClass)) {
1436 13
                $index = $cell->getXfIndex();
1437 13
                $styleIndex = 'td.style' . $index . ', th.style' . $index;
1438 13
                if (isset($this->cssStyles[$styleIndex])) {
1439 13
                    $cssClass = array_merge($cssClass, $this->cssStyles[$styleIndex]);
1440
                }
1441
1442
                // General horizontal alignment: Actual horizontal alignment depends on dataType
1443 13
                $sharedStyle = $worksheet->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex());
1444
                if (
1445 13
                    $sharedStyle->getAlignment()->getHorizontal() == Alignment::HORIZONTAL_GENERAL
1446 13
                    && isset($this->cssStyles['.' . $cell->getDataType()]['text-align'])
1447
                ) {
1448 13
                    $cssClass['text-align'] = $this->cssStyles['.' . $dataType]['text-align'];
1449
                }
1450
            }
1451
        } else {
1452
            // Use default borders for empty cell
1453 51
            if (is_string($cssClass)) {
1454 47
                $cssClass .= ' style0';
1455
            }
1456
        }
1457
1458 528
        return $cellData;
1459
    }
1460
1461 528
    private function generateRowIncludeCharts(Worksheet $worksheet, string $coordinate): string
1462
    {
1463 528
        return $this->includeCharts ? $this->writeChartInCell($worksheet, $coordinate) : '';
1464
    }
1465
1466 528
    private function generateRowSpans(string $html, int $rowSpan, int $colSpan): string
1467
    {
1468 528
        $html .= ($colSpan > 1) ? (' colspan="' . $colSpan . '"') : '';
1469 528
        $html .= ($rowSpan > 1) ? (' rowspan="' . $rowSpan . '"') : '';
1470
1471 528
        return $html;
1472
    }
1473
1474 528
    private function generateRowWriteCell(
1475
        string &$html,
1476
        Worksheet $worksheet,
1477
        string $coordinate,
1478
        string $cellType,
1479
        string $cellData,
1480
        int $colSpan,
1481
        int $rowSpan,
1482
        array|string $cssClass,
1483
        int $colNum,
1484
        int $sheetIndex,
1485
        int $row
1486
    ): void {
1487
        // Image?
1488 528
        $htmlx = $this->writeImageInCell($coordinate);
1489
        // Chart?
1490 528
        $htmlx .= $this->generateRowIncludeCharts($worksheet, $coordinate);
1491
        // Column start
1492 528
        $html .= '            <' . $cellType;
1493 528
        if ($this->betterBoolean) {
1494 3
            $dataType = $worksheet->getCell($coordinate)->getDataType();
1495 3
            if ($dataType === DataType::TYPE_BOOL) {
1496 3
                $html .= ' data-type="' . DataType::TYPE_BOOL . '"';
1497 3
            } elseif ($dataType === DataType::TYPE_FORMULA && is_bool($worksheet->getCell($coordinate)->getCalculatedValue())) {
1498 3
                $html .= ' data-type="' . DataType::TYPE_BOOL . '"';
1499 3
            } elseif (is_numeric($cellData) && $worksheet->getCell($coordinate)->getDataType() === DataType::TYPE_STRING) {
1500 3
                $html .= ' data-type="' . DataType::TYPE_STRING . '"';
1501
            }
1502
        }
1503 528
        if (!$this->useInlineCss && !$this->isPdf && is_string($cssClass)) {
1504 496
            $html .= ' class="' . $cssClass . '"';
1505 496
            if ($htmlx) {
1506 23
                $html .= " style='position: relative;'";
1507
            }
1508
        } else {
1509
            //** Necessary redundant code for the sake of \PhpOffice\PhpSpreadsheet\Writer\Pdf **
1510
            // We must explicitly write the width of the <td> element because TCPDF
1511
            // does not recognize e.g. <col style="width:42pt">
1512 42
            if ($this->useInlineCss) {
1513 14
                $xcssClass = is_array($cssClass) ? $cssClass : [];
1514
            } else {
1515 29
                if (is_string($cssClass)) {
1516 29
                    $html .= ' class="' . $cssClass . '"';
1517
                }
1518 29
                $xcssClass = [];
1519
            }
1520 42
            $width = 0;
1521 42
            $i = $colNum - 1;
1522 42
            $e = $colNum + $colSpan - 1;
1523 42
            while ($i++ < $e) {
1524 42
                if (isset($this->columnWidths[$sheetIndex][$i])) {
1525 42
                    $width += $this->columnWidths[$sheetIndex][$i];
1526
                }
1527
            }
1528 42
            $xcssClass['width'] = (string) $width . 'pt';
1529
            // We must also explicitly write the height of the <td> element because TCPDF
1530
            // does not recognize e.g. <tr style="height:50pt">
1531 42
            if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $row]['height'])) {
1532 8
                $height = $this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $row]['height'];
1533 8
                $xcssClass['height'] = $height;
1534
            }
1535
            //** end of redundant code **
1536 42
            if ($this->useInlineCss) {
1537 14
                foreach (['border-top', 'border-bottom', 'border-right', 'border-left'] as $borderType) {
1538 14
                    if (($xcssClass[$borderType] ?? '') === 'none #000000') {
1539 13
                        unset($xcssClass[$borderType]);
1540
                    }
1541
                }
1542
            }
1543
1544 42
            if ($htmlx) {
1545 10
                $xcssClass['position'] = 'relative';
1546
            }
1547 42
            $html .= ' style="' . $this->assembleCSS($xcssClass) . '"';
1548 42
            if ($this->useInlineCss) {
1549 14
                $html .= ' class="gridlines gridlinesp"';
1550
            }
1551
        }
1552 528
        $html = $this->generateRowSpans($html, $rowSpan, $colSpan);
1553
1554 528
        $html .= '>';
1555 528
        $html .= $htmlx;
1556
1557 528
        $html .= $this->writeComment($worksheet, $coordinate);
1558
1559
        // Cell data
1560 528
        $html .= $cellData;
1561
1562
        // Column end
1563 528
        $html .= '</' . $cellType . '>' . PHP_EOL;
1564
    }
1565
1566
    /**
1567
     * Generate row.
1568
     *
1569
     * @param array<int, mixed> $values Array containing cells in a row
1570
     * @param int $row Row number (0-based)
1571
     * @param string $cellType eg: 'td'
1572
     */
1573 528
    private function generateRow(Worksheet $worksheet, array $values, int $row, string $cellType): string
1574
    {
1575
        // Sheet index
1576 528
        $sheetIndex = $worksheet->getParentOrThrow()->getIndex($worksheet);
1577 528
        $html = $this->generateRowStart($worksheet, $sheetIndex, $row);
1578
1579
        // Write cells
1580 528
        $colNum = 0;
1581 528
        $tcpdfInited = false;
1582 528
        foreach ($values as $key => $cellAddress) {
1583 528
            if ($this instanceof Pdf\Mpdf) {
1584 22
                $colNum = $key - 1;
1585 512
            } elseif ($this instanceof Pdf\Tcpdf) {
1586
                // It appears that Tcpdf requires first cell in tr.
1587 7
                $colNum = $key - 1;
1588 7
                if (!$tcpdfInited && $key !== 1) {
1589 1
                    $tempspan = ($colNum > 1) ? " colspan='$colNum'" : '';
1590 1
                    $html .= "<td$tempspan></td>\n";
1591
                }
1592 7
                $tcpdfInited = true;
1593
            }
1594 528
            [$cell, $cssClass, $coordinate] = $this->generateRowCellCss($worksheet, $cellAddress, $row, $colNum);
1595
1596
            // Cell Data
1597 528
            $cellData = $this->generateRowCellData($worksheet, $cell, $cssClass);
1598
1599
            // Hyperlink?
1600 528
            if ($worksheet->hyperlinkExists($coordinate) && !$worksheet->getHyperlink($coordinate)->isInternal()) {
1601 12
                $url = $worksheet->getHyperlink($coordinate)->getUrl();
1602 12
                $urlDecode1 = html_entity_decode($url, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
1603 12
                $urlTrim = Preg::replace('/^\\s+/u', '', $urlDecode1);
1604 12
                $parseScheme = Preg::isMatch('/^([\\w\\s\\x00-\\x1f]+):/u', strtolower($urlTrim), $matches);
1605 12
                if ($parseScheme && !in_array($matches[1], ['http', 'https', 'file', 'ftp', 'mailto', 's3'], true)) {
1606 2
                    $cellData = htmlspecialchars($url, Settings::htmlEntityFlags());
1607
                    $cellData = self::replaceControlChars($cellData);
1608 11
                } else {
1609 11
                    $tooltip = $worksheet->getHyperlink($coordinate)->getTooltip();
1610 11
                    $tooltipOut = empty($tooltip) ? '' : (' title="' . htmlspecialchars($tooltip) . '"');
1611 11
                    $cellData = '<a href="'
1612 11
                        . htmlspecialchars($url) . '"'
1613 11
                        . $tooltipOut
1614
                        . '>' . $cellData . '</a>';
1615
                }
1616
            }
1617
1618 528
            // Should the cell be written or is it swallowed by a rowspan or colspan?
1619 528
            $writeCell = !(isset($this->isSpannedCell[$worksheet->getParentOrThrow()->getIndex($worksheet)][$row + 1][$colNum])
1620
                && $this->isSpannedCell[$worksheet->getParentOrThrow()->getIndex($worksheet)][$row + 1][$colNum]);
1621
1622 528
            // Colspan and Rowspan
1623 528
            $colSpan = 1;
1624 528
            $rowSpan = 1;
1625 20
            if (isset($this->isBaseCell[$worksheet->getParentOrThrow()->getIndex($worksheet)][$row + 1][$colNum])) {
1626 20
                $spans = $this->isBaseCell[$worksheet->getParentOrThrow()->getIndex($worksheet)][$row + 1][$colNum];
1627 20
                $rowSpan = $spans['rowspan'];
1628
                $colSpan = $spans['colspan'];
1629
1630
                //    Also apply style from last cell in merge to fix borders -
1631 20
                //        relies on !important for non-none border declarations in createCSSStyleBorder
1632 20
                $endCellCoord = Coordinate::stringFromColumnIndex($colNum + $colSpan) . ($row + $rowSpan);
1633 17
                if (!$this->useInlineCss) {
1634
                    $cssClass .= ' style' . $worksheet->getCell($endCellCoord)->getXfIndex();
1635 4
                } else {
1636 4
                    $endBorders = $this->spreadsheet->getCellXfByIndex($worksheet->getCell($endCellCoord)->getXfIndex())->getBorders();
1637 4
                    $altBorders = $this->createCSSStyleBorders($endBorders);
1638 4
                    foreach ($altBorders as $altKey => $altValue) {
1639 2
                        if (str_contains($altValue, '!important')) {
1640
                            $cssClass[$altKey] = $altValue;
1641
                        }
1642
                    }
1643
                }
1644
            }
1645
1646 528
            // Write
1647 528
            if ($writeCell) {
1648
                $this->generateRowWriteCell($html, $worksheet, $coordinate, $cellType, $cellData, $colSpan, $rowSpan, $cssClass, $colNum, $sheetIndex, $row);
1649
            }
1650
1651 528
            // Next column
1652
            ++$colNum;
1653
        }
1654
1655 528
        // Write row end
1656
        $html .= '          </tr>' . PHP_EOL;
1657
1658 528
        // Return
1659
        return $html;
1660
    }
1661
1662
    private static function replaceNonAscii(array $matches): string
1663
    {
1664 530
        return '&#' . mb_ord($matches[0], 'UTF-8') . ';';
1665
    }
1666 530
1667 530
    private static function replaceControlChars(string $convert): string
1668 530
    {
1669
        return (string) preg_replace_callback(
1670 530
            '/[\\x00-\\x1f]/',
1671
            [self::class, 'replaceNonAscii'],
1672 530
            $convert
1673
        );
1674
    }
1675
1676
    /**
1677
     * Takes array where of CSS properties / values and converts to CSS string.
1678 18
     */
1679
    private function assembleCSS(array $values = []): string
1680 18
    {
1681
        $pairs = [];
1682
        foreach ($values as $property => $value) {
1683
            $pairs[] = $property . ':' . $value;
1684
        }
1685
        $string = implode('; ', $pairs);
1686
1687
        return $string;
1688 1
    }
1689
1690 1
    /**
1691
     * Get images root.
1692 1
     */
1693
    public function getImagesRoot(): string
1694
    {
1695
        return $this->imagesRoot;
1696
    }
1697
1698 5
    /**
1699
     * Set images root.
1700 5
     *
1701
     * @return $this
1702
     */
1703
    public function setImagesRoot(string $imagesRoot): static
1704
    {
1705
        $this->imagesRoot = $imagesRoot;
1706
1707
        return $this;
1708 4
    }
1709
1710 4
    /**
1711
     * Get embed images.
1712 4
     */
1713
    public function getEmbedImages(): bool
1714
    {
1715
        return $this->embedImages;
1716
    }
1717
1718 1
    /**
1719
     * Set embed images.
1720 1
     *
1721
     * @return $this
1722
     */
1723
    public function setEmbedImages(bool $embedImages): static
1724
    {
1725
        $this->embedImages = $embedImages;
1726
1727
        return $this;
1728 15
    }
1729
1730 15
    /**
1731
     * Get use inline CSS?
1732 15
     */
1733
    public function getUseInlineCss(): bool
1734
    {
1735
        return $this->useInlineCss;
1736
    }
1737
1738
    /**
1739
     * Set use inline CSS?
1740
     *
1741 393
     * @return $this
1742
     */
1743 393
    public function setUseInlineCss(bool $useInlineCss): static
1744
    {
1745
        $this->useInlineCss = $useInlineCss;
1746
1747
        return $this;
1748
    }
1749
1750
    /**
1751
     * Add color to formatted string as inline style.
1752 393
     *
1753
     * @param string $value Plain formatted value without color
1754
     * @param string $format Format code
1755 393
     */
1756 393
    public function formatColor(string $value, string $format): string
1757
    {
1758 393
        return self::formatColorStatic($value, $format);
1759 393
    }
1760 17
1761 17
    /**
1762
     * Add color to formatted string as inline style.
1763
     *
1764
     * @param string $value Plain formatted value without color
1765 393
     * @param string $format Format code
1766
     */
1767
    public static function formatColorStatic(string $value, string $format): string
1768 393
    {
1769 17
        // Color information, e.g. [Red] is always at the beginning
1770
        $color = null; // initialize
1771
        $matches = [];
1772 393
1773
        $color_regex = '/^\\[[a-zA-Z]+\\]/';
1774
        if (Preg::isMatch($color_regex, $format, $matches)) {
1775
            $color = str_replace(['[', ']'], '', $matches[0]);
1776
            $color = strtolower($color);
1777
        }
1778 530
1779
        // convert to PCDATA
1780 530
        $result = htmlspecialchars($value, Settings::htmlEntityFlags());
1781 530
1782
        // color span tag
1783
        if ($color !== null) {
1784
            $result = '<span style="color:' . $color . '">' . $result . '</span>';
1785
        }
1786 530
1787 530
        return $result;
1788
    }
1789 530
1790 530
    /**
1791
     * Calculate information about HTML colspan and rowspan which is not always the same as Excel's.
1792 530
     */
1793
    private function calculateSpans(): void
1794
    {
1795 530
        if ($this->spansAreCalculated) {
1796 20
            return;
1797 20
        }
1798 20
        // Identify all cells that should be omitted in HTML due to cell merge.
1799
        // In HTML only the upper-left cell should be written and it should have
1800 20
        //   appropriate rowspan / colspan attribute
1801 20
        $sheetIndexes = $this->sheetIndex !== null
1802
            ? [$this->sheetIndex] : range(0, $this->spreadsheet->getSheetCount() - 1);
1803 20
1804 20
        foreach ($sheetIndexes as $sheetIndex) {
1805
            $sheet = $this->spreadsheet->getSheet($sheetIndex);
1806
1807 20
            $candidateSpannedRow = [];
1808 20
1809
            // loop through all Excel merged cells
1810 20
            foreach ($sheet->getMergeCells() as $cells) {
1811
                [$cells] = Coordinate::splitRange($cells);
1812 20
                $first = $cells[0];
1813 20
                $last = $cells[1];
1814 20
1815
                [$fc, $fr] = Coordinate::indexesFromString($first);
1816 20
                $fc = $fc - 1;
1817 20
1818 20
                [$lc, $lr] = Coordinate::indexesFromString($last);
1819
                $lc = $lc - 1;
1820
1821 20
                // loop through the individual cells in the individual merge
1822 20
                $r = $fr - 1;
1823 20
                while ($r++ < $lr) {
1824 20
                    // also, flag this row as a HTML row that is candidate to be omitted
1825 20
                    $candidateSpannedRow[$r] = $r;
1826 20
1827
                    $c = $fc - 1;
1828
                    while ($c++ < $lc) {
1829
                        if (!($c == $fc && $r == $fr)) {
1830
                            // not the upper-left cell (should not be written in HTML)
1831
                            $this->isSpannedCell[$sheetIndex][$r][$c] = [
1832 530
                                'baseCell' => [$fr, $fc],
1833
                            ];
1834
                        } else {
1835
                            // upper-left is the base cell that should hold the colspan/rowspan attribute
1836
                            $this->isBaseCell[$sheetIndex][$r][$c] = [
1837
                                'xlrowspan' => $lr - $fr + 1, // Excel rowspan
1838 530
                                'rowspan' => $lr - $fr + 1, // HTML rowspan, value may change
1839
                                'xlcolspan' => $lc - $fc + 1, // Excel colspan
1840
                                'colspan' => $lc - $fc + 1, // HTML colspan, value may change
1841 530
                            ];
1842
                        }
1843
                    }
1844
                }
1845 530
            }
1846 530
1847 20
            $this->calculateSpansOmitRows($sheet, $sheetIndex, $candidateSpannedRow);
1848 20
1849 8
            // TODO: Same for columns
1850
        }
1851
1852
        // We have calculated the spans
1853
        $this->spansAreCalculated = true;
1854
    }
1855 530
1856 8
    private function calculateSpansOmitRows(Worksheet $sheet, int $sheetIndex, array $candidateSpannedRow): void
1857 8
    {
1858 8
        // Identify which rows should be omitted in HTML. These are the rows where all the cells
1859 8
        //   participate in a merge and the where base cells are somewhere above.
1860 8
        $countColumns = Coordinate::columnIndexFromString($sheet->getHighestColumn());
1861 8
        foreach ($candidateSpannedRow as $rowIndex) {
1862
            if (isset($this->isSpannedCell[$sheetIndex][$rowIndex])) {
1863 8
                if (count($this->isSpannedCell[$sheetIndex][$rowIndex]) == $countColumns) {
1864
                    $this->isSpannedRow[$sheetIndex][$rowIndex] = $rowIndex;
1865 8
                }
1866 8
            }
1867
        }
1868
1869
        // For each of the omitted rows we found above, the affected rowspans should be subtracted by 1
1870
        if (isset($this->isSpannedRow[$sheetIndex])) {
1871
            foreach ($this->isSpannedRow[$sheetIndex] as $rowIndex) {
1872
                $adjustedBaseCells = [];
1873
                $c = -1;
1874
                $e = $countColumns - 1;
1875
                while ($c++ < $e) {
1876
                    $baseCell = $this->isSpannedCell[$sheetIndex][$rowIndex][$c]['baseCell'];
1877
1878 528
                    if (!in_array($baseCell, $adjustedBaseCells, true)) {
1879
                        // subtract rowspan by 1
1880 528
                        --$this->isBaseCell[$sheetIndex][$baseCell[0]][$baseCell[1]]['rowspan'];
1881 528
                        $adjustedBaseCells[] = $baseCell;
1882 24
                    }
1883 24
                }
1884 24
            }
1885 24
        }
1886 24
    }
1887 2
1888
    /**
1889 24
     * Write a comment in the same format as LibreOffice.
1890 24
     *
1891 24
     * @see https://github.com/LibreOffice/core/blob/9fc9bf3240f8c62ad7859947ab8a033ac1fe93fa/sc/source/filter/html/htmlexp.cxx#L1073-L1092
1892 24
     */
1893
    private function writeComment(Worksheet $worksheet, string $coordinate): string
1894
    {
1895
        $result = '';
1896 528
        if (!$this->isPdf && isset($worksheet->getComments()[$coordinate])) {
1897
            $sanitizedString = $this->generateRowCellDataValueRich($worksheet->getComment($coordinate)->getText());
1898
            $dir = ($worksheet->getComment($coordinate)->getTextboxDirection() === Comment::TEXTBOX_DIRECTION_RTL) ? ' dir="rtl"' : '';
1899 500
            $align = strtolower($worksheet->getComment($coordinate)->getAlignment());
1900
            $alignment = Alignment::HORIZONTAL_ALIGNMENT_FOR_HTML[$align] ?? '';
1901
            if ($alignment !== '') {
1902 500
                $alignment = " style=\"text-align:$alignment\"";
1903
            }
1904
            if ($sanitizedString !== '') {
1905
                $result .= '<a class="comment-indicator"></a>';
1906
                $result .= "<div class=\"comment\"$dir$alignment>" . $sanitizedString . '</div>';
1907
                $result .= PHP_EOL;
1908 530
            }
1909
        }
1910
1911 530
        return $result;
1912
    }
1913
1914 530
    public function getOrientation(): ?string
1915 530
    {
1916 15
        // Expect Pdf classes to override this method.
1917
        return $this->isPdf ? PageSetup::ORIENTATION_PORTRAIT : null;
1918 522
    }
1919
1920
    /**
1921
     * Generate @page declarations.
1922 530
     */
1923
    private function generatePageDeclarations(bool $generateSurroundingHTML): string
1924
    {
1925 530
        // Ensure that Spans have been calculated?
1926 530
        $this->calculateSpans();
1927 530
1928 530
        // Fetch sheets
1929 530
        $sheets = [];
1930 530
        if ($this->sheetIndex === null) {
1931 530
            $sheets = $this->spreadsheet->getAllSheets();
1932 530
        } else {
1933 530
            $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex);
1934 530
        }
1935 530
1936 530
        // Construct HTML
1937 530
        $htmlPage = $generateSurroundingHTML ? ('<style type="text/css">' . PHP_EOL) : '';
1938 9
1939 522
        // Loop all sheets
1940 13
        $sheetId = 0;
1941
        foreach ($sheets as $worksheet) {
1942 530
            $htmlPage .= "@page page$sheetId { ";
1943 530
            $left = StringHelper::formatNumber($worksheet->getPageMargins()->getLeft()) . 'in; ';
1944
            $htmlPage .= 'margin-left: ' . $left;
1945 530
            $right = StringHelper::FormatNumber($worksheet->getPageMargins()->getRight()) . 'in; ';
1946 530
            $htmlPage .= 'margin-right: ' . $right;
1947 530
            $top = StringHelper::FormatNumber($worksheet->getPageMargins()->getTop()) . 'in; ';
1948 530
            $htmlPage .= 'margin-top: ' . $top;
1949 530
            $bottom = StringHelper::FormatNumber($worksheet->getPageMargins()->getBottom()) . 'in; ';
1950 530
            $htmlPage .= 'margin-bottom: ' . $bottom;
1951 530
            $orientation = $this->getOrientation() ?? $worksheet->getPageSetup()->getOrientation();
1952 530
            if ($orientation === PageSetup::ORIENTATION_LANDSCAPE) {
1953 530
                $htmlPage .= 'size: landscape; ';
1954 530
            } elseif ($orientation === PageSetup::ORIENTATION_PORTRAIT) {
1955 530
                $htmlPage .= 'size: portrait; ';
1956 530
            }
1957 530
            $htmlPage .= '}' . PHP_EOL;
1958 530
            ++$sheetId;
1959 530
        }
1960 530
        $htmlPage .= implode(PHP_EOL, [
1961 530
            '.navigation {page-break-after: always;}',
1962 530
            '.scrpgbrk, div + div {page-break-before: always;}',
1963
            '@media screen {',
1964 530
            '  .gridlines td {border: 1px solid black;}',
1965
            '  .gridlines th {border: 1px solid black;}',
1966
            '  body>div {margin-top: 5px;}',
1967 528
            '  body>div:first-child {margin-top: 0;}',
1968
            '  .scrpgbrk {margin-top: 1px;}',
1969 528
            '}',
1970 506
            '@media print {',
1971
            '  .gridlinesp td {border: 1px solid black;}',
1972
            '  .gridlinesp th {border: 1px solid black;}',
1973 28
            '  .navigation {display: none;}',
1974
            '}',
1975
            '',
1976 530
        ]);
1977
        $htmlPage .= $generateSurroundingHTML ? ('</style>' . PHP_EOL) : '';
1978 530
1979 508
        return $htmlPage;
1980
    }
1981 28
1982 27
    private function shouldGenerateRow(Worksheet $sheet, int $row): bool
1983
    {
1984
        if (!($this instanceof Pdf\Mpdf || $this instanceof Pdf\Tcpdf)) {
1985 11
            return true;
1986
        }
1987
1988 1
        return $sheet->isRowVisible($row);
1989
    }
1990 1
1991
    private function shouldGenerateColumn(Worksheet $sheet, string $colStr): bool
1992
    {
1993 4
        if (!($this instanceof Pdf\Mpdf || $this instanceof Pdf\Tcpdf)) {
1994
            return true;
1995 4
        }
1996
        if (!$sheet->columnDimensionExists($colStr)) {
1997 4
            return true;
1998
        }
1999
2000
        return $sheet->getColumnDimension($colStr)->getVisible();
2001
    }
2002
2003
    public function getBetterBoolean(): bool
2004
    {
2005
        return $this->betterBoolean;
2006
    }
2007
2008
    public function setBetterBoolean(bool $betterBoolean): self
2009
    {
2010
        $this->betterBoolean = $betterBoolean;
2011
2012
        return $this;
2013
    }
2014
}
2015