Passed
Pull Request — master (#4333)
by Owen
15:23
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 541
    public function __construct(Spreadsheet $spreadsheet)
156
    {
157 541
        $this->spreadsheet = $spreadsheet;
158 541
        $this->defaultFont = $this->spreadsheet->getDefaultStyle()->getFont();
159 541
        $calc = Calculation::getInstance($this->spreadsheet);
160 541
        $this->getTrue = $calc->getTRUE();
161 541
        $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 529
    public function generateHtmlAll(): string
187
    {
188 529
        $sheets = $this->generateSheetPrep();
189 529
        foreach ($sheets as $sheet) {
190 529
            $sheet->calculateArrays($this->preCalculateFormulas);
191
        }
192
        // garbage collect
193 529
        $this->spreadsheet->garbageCollect();
194
195 529
        $saveDebugLog = Calculation::getInstance($this->spreadsheet)->getDebugLog()->getWriteDebugLog();
196 529
        Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog(false);
197
198
        // Build CSS
199 529
        $this->buildCSS(!$this->useInlineCss);
200
201 529
        $html = '';
202
203
        // Write headers
204 529
        $html .= $this->generateHTMLHeader(!$this->useInlineCss);
205
206
        // Write navigation (tabs)
207 529
        if ((!$this->isPdf) && ($this->generateSheetNavigationBlock)) {
208 499
            $html .= $this->generateNavigation();
209
        }
210
211
        // Write data
212 529
        $html .= $this->generateSheetData();
213
214
        // Write footer
215 529
        $html .= $this->generateHTMLFooter();
216 529
        $callback = $this->editHtmlCallback;
217 529
        if ($callback) {
218 6
            $html = $callback($html);
219
        }
220
221 529
        Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog($saveDebugLog);
222
223 529
        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 531
    private function mapVAlign(string $vAlign): string
243
    {
244 531
        return Alignment::VERTICAL_ALIGNMENT_FOR_HTML[$vAlign] ?? '';
245
    }
246
247
    /**
248
     * Map HAlign.
249
     *
250
     * @param string $hAlign Horizontal alignment
251
     */
252 531
    private function mapHAlign(string $hAlign): string
253
    {
254 531
        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 520
    private function mapBorderStyle($borderStyle): string
279
    {
280 520
        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 531
    private static function generateMeta(?string $val, string $desc): string
340
    {
341 531
        return ($val || $val === '0')
342 531
            ? ('      <meta name="' . $desc . '" content="' . htmlspecialchars($val, Settings::htmlEntityFlags()) . '" />' . PHP_EOL)
343 531
            : '';
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 531
    public function generateHTMLHeader(bool $includeStyles = false): string
362
    {
363
        // Construct HTML
364 531
        $properties = $this->spreadsheet->getProperties();
365 531
        $html = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">' . PHP_EOL;
366 531
        $html .= '<html xmlns="http://www.w3.org/1999/xhtml">' . PHP_EOL;
367 531
        $html .= '  <head>' . PHP_EOL;
368 531
        $html .= '      <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />' . PHP_EOL;
369 531
        $html .= '      <meta name="generator" content="PhpSpreadsheet, https://github.com/PHPOffice/PhpSpreadsheet" />' . PHP_EOL;
370 531
        $title = $properties->getTitle();
371 531
        if ($title === '') {
372 11
            $title = $this->spreadsheet->getActiveSheet()->getTitle();
373
        }
374 531
        $html .= '      <title>' . htmlspecialchars($title, Settings::htmlEntityFlags()) . '</title>' . PHP_EOL;
375 531
        $html .= self::generateMeta($properties->getCreator(), 'author');
376 531
        $html .= self::generateMeta($properties->getTitle(), 'title');
377 531
        $html .= self::generateMeta($properties->getDescription(), 'description');
378 531
        $html .= self::generateMeta($properties->getSubject(), 'subject');
379 531
        $html .= self::generateMeta($properties->getKeywords(), 'keywords');
380 531
        $html .= self::generateMeta($properties->getCategory(), 'category');
381 531
        $html .= self::generateMeta($properties->getCompany(), 'company');
382 531
        $html .= self::generateMeta($properties->getManager(), 'manager');
383 531
        $html .= self::generateMeta($properties->getLastModifiedBy(), 'lastModifiedBy');
384 531
        $html .= self::generateMeta($properties->getViewport(), 'viewport');
385 531
        $date = Date::dateTimeFromTimestamp((string) $properties->getCreated());
386 531
        $date->setTimeZone(Date::getDefaultOrLocalTimeZone());
387 531
        $html .= self::generateMeta($date->format(DATE_W3C), 'created');
388 531
        $date = Date::dateTimeFromTimestamp((string) $properties->getModified());
389 531
        $date->setTimeZone(Date::getDefaultOrLocalTimeZone());
390 531
        $html .= self::generateMeta($date->format(DATE_W3C), 'modified');
391
392 531
        $customProperties = $properties->getCustomProperties();
393 531
        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 531
        if (!empty($properties->getHyperlinkBase())) {
412 2
            $html .= '      <base href="' . htmlspecialchars($properties->getHyperlinkBase()) . '" />' . PHP_EOL;
413
        }
414
415 531
        $html .= $includeStyles ? $this->generateStyles(true) : $this->generatePageDeclarations(true);
416
417 531
        $html .= '  </head>' . PHP_EOL;
418 531
        $html .= '' . PHP_EOL;
419 531
        $html .= self::BODY_LINE;
420
421 531
        return $html;
422
    }
423
424
    /** @return Worksheet[] */
425 529
    private function generateSheetPrep(): array
426
    {
427
        // Fetch sheets
428 529
        if ($this->sheetIndex === null) {
429 14
            $sheets = $this->spreadsheet->getAllSheets();
430
        } else {
431 522
            $sheets = [$this->spreadsheet->getSheet($this->sheetIndex)];
432
        }
433
434 529
        return $sheets;
435
    }
436
437 529
    private function generateSheetStarts(Worksheet $sheet, int $rowMin): array
438
    {
439
        // calculate start of <tbody>, <thead>
440 529
        $tbodyStart = $rowMin;
441 529
        $theadStart = $theadEnd = 0; // default: no <thead>    no </thead>
442 529
        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 529
        return [$theadStart, $theadEnd, $tbodyStart];
454
    }
455
456 529
    private function generateSheetTags(int $row, int $theadStart, int $theadEnd, int $tbodyStart): array
457
    {
458
        // <thead> ?
459 529
        $startTag = ($row == $theadStart) ? ('        <thead>' . PHP_EOL) : '';
460 529
        if (!$startTag) {
461 529
            $startTag = ($row == $tbodyStart) ? ('        <tbody>' . PHP_EOL) : '';
462
        }
463 529
        $endTag = ($row == $theadEnd) ? ('        </thead>' . PHP_EOL) : '';
464 529
        $cellType = ($row >= $tbodyStart) ? 'td' : 'th';
465
466 529
        return [$cellType, $startTag, $endTag];
467
    }
468
469
    /**
470
     * Generate sheet data.
471
     */
472 529
    public function generateSheetData(): string
473
    {
474
        // Ensure that Spans have been calculated?
475 529
        $this->calculateSpans();
476 529
        $sheets = $this->generateSheetPrep();
477
478
        // Construct HTML
479 529
        $html = '';
480
481
        // Loop all sheets
482 529
        $sheetId = 0;
483 529
        foreach ($sheets as $sheet) {
484
            // Write table header
485 529
            $html .= $this->generateTableHeader($sheet);
486 529
            $this->sheetCharts = [];
487 529
            $this->sheetDrawings = [];
488
489
            // Get worksheet dimension
490 529
            [$min, $max] = explode(':', $sheet->calculateWorksheetDataDimension());
491 529
            [$minCol, $minRow, $minColString] = Coordinate::indexesFromString($min);
492 529
            [$maxCol, $maxRow] = Coordinate::indexesFromString($max);
493 529
            $this->extendRowsAndColumns($sheet, $maxCol, $maxRow);
494
495 529
            [$theadStart, $theadEnd, $tbodyStart] = $this->generateSheetStarts($sheet, $minRow);
496
497
            // Loop through cells
498 529
            $row = $minRow - 1;
499 529
            while ($row++ < $maxRow) {
500 529
                [$cellType, $startTag, $endTag] = $this->generateSheetTags($row, $theadStart, $theadEnd, $tbodyStart);
501 529
                $html .= $startTag;
502
503
                // Write row if there are HTML table cells in it
504 529
                if ($this->shouldGenerateRow($sheet, $row) && !isset($this->isSpannedRow[$sheet->getParentOrThrow()->getIndex($sheet)][$row])) {
505
                    // Start a new rowData
506 529
                    $rowData = [];
507
                    // Loop through columns
508 529
                    $column = $minCol;
509 529
                    $colStr = $minColString;
510 529
                    while ($column <= $maxCol) {
511
                        // Cell exists?
512 529
                        $cellAddress = Coordinate::stringFromColumnIndex($column) . $row;
513 529
                        if ($this->shouldGenerateColumn($sheet, $colStr)) {
514 529
                            $rowData[$column] = ($sheet->getCellCollection()->has($cellAddress)) ? $cellAddress : '';
515
                        }
516 529
                        ++$column;
517 529
                        ++$colStr;
518
                    }
519 529
                    $html .= $this->generateRow($sheet, $rowData, $row - 1, $cellType);
520
                }
521
522 529
                $html .= $endTag;
523
            }
524
525
            // Write table footer
526 529
            $html .= $this->generateTableFooter();
527
            // Writing PDF?
528 529
            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 529
            ++$sheetId;
536
        }
537
538 529
        return $html;
539
    }
540
541
    /**
542
     * Generate sheet tabs.
543
     */
544 499
    public function generateNavigation(): string
545
    {
546
        // Fetch sheets
547 499
        $sheets = [];
548 499
        if ($this->sheetIndex === null) {
549 8
            $sheets = $this->spreadsheet->getAllSheets();
550
        } else {
551 497
            $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex);
552
        }
553
554
        // Construct HTML
555 499
        $html = '';
556
557
        // Only if there are more than 1 sheets
558 499
        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 499
        return $html;
573
    }
574
575 529
    private function extendRowsAndColumns(Worksheet $worksheet, int &$colMax, int &$rowMax): void
576
    {
577 529
        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 529
        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 529
    private function writeImageInCell(string $coordinates): string
629
    {
630
        // Construct HTML
631 529
        $html = '';
632
633
        // Write images
634 529
        $drawing = $this->sheetDrawings[$coordinates] ?? null;
635 529
        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 529
        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 523
    public function generateStyles(bool $generateSurroundingHTML = true): string
805
    {
806
        // Build CSS
807 523
        $css = $this->buildCSS($generateSurroundingHTML);
808
809
        // Construct HTML
810 523
        $html = '';
811
812
        // Start styles
813 523
        if ($generateSurroundingHTML) {
814 523
            $html .= '    <style type="text/css">' . PHP_EOL;
815 523
            $html .= (array_key_exists('html', $css)) ? ('      html { ' . $this->assembleCSS($css['html']) . ' }' . PHP_EOL) : '';
816
        }
817
818
        // Write all other styles
819 523
        foreach ($css as $styleName => $styleDefinition) {
820 523
            if ($styleName != 'html') {
821 523
                $html .= '      ' . $styleName . ' { ' . $this->assembleCSS($styleDefinition) . ' }' . PHP_EOL;
822
            }
823
        }
824 523
        $html .= $this->generatePageDeclarations(false);
825
826
        // End styles
827 523
        if ($generateSurroundingHTML) {
828 523
            $html .= '    </style>' . PHP_EOL;
829
        }
830
831
        // Return
832 523
        return $html;
833
    }
834
835 531
    private function buildCssRowHeights(Worksheet $sheet, array &$css, int $sheetIndex): void
836
    {
837
        // Calculate row heights
838 531
        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 531
    private function buildCssPerSheet(Worksheet $sheet, array &$css): void
856
    {
857
        // Calculate hash code
858 531
        $sheetIndex = $sheet->getParentOrThrow()->getIndex($sheet);
859 531
        $setup = $sheet->getPageSetup();
860 531
        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 531
        $picture = $sheet->getBackgroundImage();
865 531
        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 531
        $sheet->calculateColumnWidths();
873
874
        // col elements, initialize
875 531
        $highestColumnIndex = Coordinate::columnIndexFromString($sheet->getHighestColumn()) - 1;
876 531
        $column = -1;
877 531
        $colStr = 'A';
878 531
        while ($column++ < $highestColumnIndex) {
879 531
            $this->columnWidths[$sheetIndex][$column] = self::DEFAULT_CELL_WIDTH_POINTS; // approximation
880 531
            if ($this->shouldGenerateColumn($sheet, $colStr)) {
881 531
                $css['table.sheet' . $sheetIndex . ' col.col' . $column]['width'] = self::DEFAULT_CELL_WIDTH_POINTS . 'pt';
882
            }
883 531
            ++$colStr;
884
        }
885
886
        // col elements, loop through columnDimensions and set width
887 531
        foreach ($sheet->getColumnDimensions() as $columnDimension) {
888 31
            $column = Coordinate::columnIndexFromString($columnDimension->getColumnIndex()) - 1;
889 31
            $width = SharedDrawing::cellDimensionToPixels($columnDimension->getWidth(), $this->defaultFont);
890 31
            $width = SharedDrawing::pixelsToPoints($width);
891 31
            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 31
            if ($width >= 0) {
898 20
                $this->columnWidths[$sheetIndex][$column] = $width;
899 20
                $css['table.sheet' . $sheetIndex . ' col.col' . $column]['width'] = $width . 'pt';
900
            }
901
        }
902
903
        // Default row height
904 531
        $rowDimension = $sheet->getDefaultRowDimension();
905
906
        // table.sheetN tr { }
907 531
        $css['table.sheet' . $sheetIndex . ' tr'] = [];
908
909 531
        if ($rowDimension->getRowHeight() == -1) {
910 523
            $pt_height = SharedFont::getDefaultRowHeightByFont($this->spreadsheet->getDefaultStyle()->getFont());
911
        } else {
912 8
            $pt_height = $rowDimension->getRowHeight();
913
        }
914 531
        $css['table.sheet' . $sheetIndex . ' tr']['height'] = $pt_height . 'pt';
915 531
        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 531
        $this->buildCssRowHeights($sheet, $css, $sheetIndex);
921
    }
922
923
    /**
924
     * Build CSS styles.
925
     *
926
     * @param bool $generateSurroundingHTML Generate surrounding HTML style? (html { })
927
     */
928 531
    public function buildCSS(bool $generateSurroundingHTML = true): array
929
    {
930
        // Cached?
931 531
        if ($this->cssStyles !== null) {
932 521
            return $this->cssStyles;
933
        }
934
935
        // Ensure that spans have been calculated
936 531
        $this->calculateSpans();
937
938
        // Construct CSS
939 531
        $css = [];
940
941
        // Start styles
942 531
        if ($generateSurroundingHTML) {
943
            // html { }
944 522
            $css['html']['font-family'] = 'Calibri, Arial, Helvetica, sans-serif';
945 522
            $css['html']['font-size'] = '11pt';
946 522
            $css['html']['background-color'] = 'white';
947
        }
948
949
        // CSS for comments as found in LibreOffice
950 531
        $css['a.comment-indicator:hover + div.comment'] = [
951 531
            'background' => '#ffd',
952 531
            'position' => 'absolute',
953 531
            'display' => 'block',
954 531
            'border' => '1px solid black',
955 531
            'padding' => '0.5em',
956 531
        ];
957
958 531
        $css['a.comment-indicator'] = [
959 531
            'background' => 'red',
960 531
            'display' => 'inline-block',
961 531
            'border' => '1px solid black',
962 531
            'width' => '0.5em',
963 531
            'height' => '0.5em',
964 531
        ];
965
966 531
        $css['div.comment']['display'] = 'none';
967
968
        // table { }
969 531
        $css['table']['border-collapse'] = 'collapse';
970
971
        // .b {}
972 531
        $css['.b']['text-align'] = 'center'; // BOOL
973
974
        // .e {}
975 531
        $css['.e']['text-align'] = 'center'; // ERROR
976
977
        // .f {}
978 531
        $css['.f']['text-align'] = 'right'; // FORMULA
979
980
        // .inlineStr {}
981 531
        $css['.inlineStr']['text-align'] = 'left'; // INLINE
982
983
        // .n {}
984 531
        $css['.n']['text-align'] = 'right'; // NUMERIC
985
986
        // .s {}
987 531
        $css['.s']['text-align'] = 'left'; // STRING
988
989
        // Calculate cell style hashes
990 531
        foreach ($this->spreadsheet->getCellXfCollection() as $index => $style) {
991 531
            $css['td.style' . $index . ', th.style' . $index] = $this->createCSSStyle($style);
992
            //$css['th.style' . $index] = $this->createCSSStyle($style);
993
        }
994
995
        // Fetch sheets
996 531
        $sheets = [];
997 531
        if ($this->sheetIndex === null) {
998 15
            $sheets = $this->spreadsheet->getAllSheets();
999
        } else {
1000 523
            $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex);
1001
        }
1002
1003
        // Build styles per sheet
1004 531
        foreach ($sheets as $sheet) {
1005 531
            $this->buildCssPerSheet($sheet, $css);
1006
        }
1007
1008
        // Cache
1009 531
        if ($this->cssStyles === null) {
1010 531
            $this->cssStyles = $css;
1011
        }
1012
1013
        // Return
1014 531
        return $css;
1015
    }
1016
1017
    /**
1018
     * Create CSS style.
1019
     */
1020 531
    private function createCSSStyle(Style $style): array
1021
    {
1022
        // Create CSS
1023 531
        return array_merge(
1024 531
            $this->createCSSStyleAlignment($style->getAlignment()),
1025 531
            $this->createCSSStyleBorders($style->getBorders()),
1026 531
            $this->createCSSStyleFont($style->getFont()),
1027 531
            $this->createCSSStyleFill($style->getFill())
1028 531
        );
1029
    }
1030
1031
    /**
1032
     * Create CSS style.
1033
     */
1034 531
    private function createCSSStyleAlignment(Alignment $alignment): array
1035
    {
1036
        // Construct CSS
1037 531
        $css = [];
1038
1039
        // Create CSS
1040 531
        $verticalAlign = $this->mapVAlign($alignment->getVertical() ?? '');
1041 531
        if ($verticalAlign) {
1042 531
            $css['vertical-align'] = $verticalAlign;
1043
        }
1044 531
        $textAlign = $this->mapHAlign($alignment->getHorizontal() ?? '');
1045 531
        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 531
        $rotation = $alignment->getTextRotation();
1052 531
        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 531
        return $css;
1061
    }
1062
1063
    /**
1064
     * Create CSS style.
1065
     */
1066 531
    private function createCSSStyleFont(Font $font): array
1067
    {
1068
        // Construct CSS
1069 531
        $css = [];
1070
1071
        // Create CSS
1072 531
        if ($font->getBold()) {
1073 20
            $css['font-weight'] = 'bold';
1074
        }
1075 531
        if ($font->getUnderline() != Font::UNDERLINE_NONE && $font->getStrikethrough()) {
1076 1
            $css['text-decoration'] = 'underline line-through';
1077 531
        } elseif ($font->getUnderline() != Font::UNDERLINE_NONE) {
1078 12
            $css['text-decoration'] = 'underline';
1079 531
        } elseif ($font->getStrikethrough()) {
1080 1
            $css['text-decoration'] = 'line-through';
1081
        }
1082 531
        if ($font->getItalic()) {
1083 10
            $css['font-style'] = 'italic';
1084
        }
1085
1086 531
        $css['color'] = '#' . $font->getColor()->getRGB();
1087 531
        $css['font-family'] = '\'' . htmlspecialchars((string) $font->getName(), ENT_QUOTES) . '\'';
1088 531
        $css['font-size'] = $font->getSize() . 'pt';
1089
1090 531
        return $css;
1091
    }
1092
1093
    /**
1094
     * Create CSS style.
1095
     *
1096
     * @param Borders $borders Borders
1097
     */
1098 531
    private function createCSSStyleBorders(Borders $borders): array
1099
    {
1100
        // Construct CSS
1101 531
        $css = [];
1102
1103
        // Create CSS
1104 531
        if (!($this instanceof Pdf\Mpdf)) {
1105 515
            $css['border-bottom'] = $this->createCSSStyleBorder($borders->getBottom());
1106 515
            $css['border-top'] = $this->createCSSStyleBorder($borders->getTop());
1107 515
            $css['border-left'] = $this->createCSSStyleBorder($borders->getLeft());
1108 515
            $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 531
        return $css;
1126
    }
1127
1128
    /**
1129
     * Create CSS style.
1130
     *
1131
     * @param Border $border Border
1132
     */
1133 520
    private function createCSSStyleBorder(Border $border): string
1134
    {
1135
        //    Create CSS - add !important to non-none border styles for merged cells
1136 520
        $borderStyle = $this->mapBorderStyle($border->getBorderStyle());
1137
1138 520
        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 531
    private function createCSSStyleFill(Fill $fill): array
1147
    {
1148
        // Construct HTML
1149 531
        $css = [];
1150
1151
        // Create CSS
1152 531
        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 531
        return $css;
1166
    }
1167
1168
    /**
1169
     * Generate HTML footer.
1170
     */
1171 529
    public function generateHTMLFooter(): string
1172
    {
1173
        // Construct HTML
1174 529
        $html = '';
1175 529
        $html .= '  </body>' . PHP_EOL;
1176 529
        $html .= '</html>' . PHP_EOL;
1177
1178 529
        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 529
    private function generateTableTag(Worksheet $worksheet, string $id, string &$html, int $sheetIndex): void
1202
    {
1203 529
        if (!$this->useInlineCss) {
1204 521
            $gridlines = $worksheet->getShowGridlines() ? ' gridlines' : '';
1205 521
            $gridlinesp = $worksheet->getPrintGridlines() ? ' gridlinesp' : '';
1206 521
            $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 529
    private function generateTableHeader(Worksheet $worksheet, bool $showid = true): string
1219
    {
1220 529
        $sheetIndex = $worksheet->getParentOrThrow()->getIndex($worksheet);
1221
1222
        // Construct HTML
1223 529
        $html = '';
1224 529
        $id = $showid ? "id='sheet$sheetIndex'" : '';
1225 529
        if ($showid) {
1226 529
            $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 529
        $this->generateTableTag($worksheet, $id, $html, $sheetIndex);
1232
1233
        // Write <col> elements
1234 529
        $highestColumnIndex = Coordinate::columnIndexFromString($worksheet->getHighestColumn()) - 1;
1235 529
        $i = -1;
1236 529
        while ($i++ < $highestColumnIndex) {
1237 529
            if (!$this->useInlineCss) {
1238 521
                $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 529
        return $html;
1247
    }
1248
1249
    /**
1250
     * Generate table footer.
1251
     */
1252 529
    private function generateTableFooter(): string
1253
    {
1254 529
        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 529
    private function generateRowStart(Worksheet $worksheet, int $sheetIndex, int $row): string
1264
    {
1265 529
        $html = '';
1266 529
        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 529
        if (!$this->useInlineCss) {
1285 521
            $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 529
        return $html;
1294
    }
1295
1296 529
    private function generateRowCellCss(Worksheet $worksheet, string $cellAddress, int $row, int $columnNumber): array
1297
    {
1298 529
        $cell = ($cellAddress > '') ? $worksheet->getCellCollection()->get($cellAddress) : '';
1299 529
        $coordinate = Coordinate::stringFromColumnIndex($columnNumber + 1) . ($row + 1);
1300 529
        if (!$this->useInlineCss) {
1301 521
            $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 529
        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 527
    private function generateRowCellDataValue(Worksheet $worksheet, Cell $cell, string &$cellData): void
1363
    {
1364 527
        if ($cell->getValue() instanceof RichText) {
1365 11
            $cellData .= $this->generateRowCellDataValueRich($cell->getValue());
1366
        } else {
1367 527
            if ($this->preCalculateFormulas) {
1368 526
                $origData = $cell->getCalculatedValue();
1369 526
                if ($this->betterBoolean && is_bool($origData)) {
1370 3
                    $origData2 = $origData ? $this->getTrue : $this->getFalse;
1371
                } else {
1372 526
                    $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 527
            $formatCode = $worksheet->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())->getNumberFormat()->getFormatCode();
1383
1384 527
            $cellData = NumberFormat::toFormattedString(
1385 527
                $origData2,
1386 527
                $formatCode ?? NumberFormat::FORMAT_GENERAL,
1387 527
                [$this, 'formatColor']
1388 527
            );
1389
1390 527
            if ($cellData === $origData) {
1391 105
                $cellData = htmlspecialchars($cellData, Settings::htmlEntityFlags());
1392
            }
1393 527
            if ($worksheet->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())->getFont()->getSuperscript()) {
1394 1
                $cellData = '<sup>' . $cellData . '</sup>';
1395 527
            } elseif ($worksheet->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())->getFont()->getSubscript()) {
1396 1
                $cellData = '<sub>' . $cellData . '</sub>';
1397
            }
1398
        }
1399
    }
1400
1401 529
    private function generateRowCellData(Worksheet $worksheet, null|Cell|string $cell, array|string &$cssClass): string
1402
    {
1403 529
        $cellData = '&nbsp;';
1404 529
        if ($cell instanceof Cell) {
1405 527
            $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 527
            $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 527
            $cellData = Preg::replace('/(?m)(?:^|\\G) /', '&nbsp;', $cellData);
1416
1417
            // convert newline "\n" to '<br>'
1418 527
            $cellData = nl2br($cellData);
1419
1420
            // Extend CSS class?
1421 527
            $dataType = $cell->getDataType();
1422 527
            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 527
            if (!$this->useInlineCss && is_string($cssClass)) {
1433 520
                $cssClass .= ' style' . $cell->getXfIndex();
1434 520
                $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 529
        return $cellData;
1459
    }
1460
1461 529
    private function generateRowIncludeCharts(Worksheet $worksheet, string $coordinate): string
1462
    {
1463 529
        return $this->includeCharts ? $this->writeChartInCell($worksheet, $coordinate) : '';
1464
    }
1465
1466 529
    private function generateRowSpans(string $html, int $rowSpan, int $colSpan): string
1467
    {
1468 529
        $html .= ($colSpan > 1) ? (' colspan="' . $colSpan . '"') : '';
1469 529
        $html .= ($rowSpan > 1) ? (' rowspan="' . $rowSpan . '"') : '';
1470
1471 529
        return $html;
1472
    }
1473
1474 529
    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 529
        $htmlx = $this->writeImageInCell($coordinate);
1489
        // Chart?
1490 529
        $htmlx .= $this->generateRowIncludeCharts($worksheet, $coordinate);
1491
        // Column start
1492 529
        $html .= '            <' . $cellType;
1493 529
        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 529
        if (!$this->useInlineCss && !$this->isPdf && is_string($cssClass)) {
1504 497
            $html .= ' class="' . $cssClass . '"';
1505 497
            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 529
        $html = $this->generateRowSpans($html, $rowSpan, $colSpan);
1553
1554 529
        $html .= '>';
1555 529
        $html .= $htmlx;
1556
1557 529
        $html .= $this->writeComment($worksheet, $coordinate);
1558
1559
        // Cell data
1560 529
        $html .= $cellData;
1561
1562
        // Column end
1563 529
        $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 529
    private function generateRow(Worksheet $worksheet, array $values, int $row, string $cellType): string
1574
    {
1575
        // Sheet index
1576 529
        $sheetIndex = $worksheet->getParentOrThrow()->getIndex($worksheet);
1577 529
        $html = $this->generateRowStart($worksheet, $sheetIndex, $row);
1578
1579
        // Write cells
1580 529
        $colNum = 0;
1581 529
        $tcpdfInited = false;
1582 529
        foreach ($values as $key => $cellAddress) {
1583 529
            if ($this instanceof Pdf\Mpdf) {
1584 22
                $colNum = $key - 1;
1585 513
            } 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 529
            [$cell, $cssClass, $coordinate] = $this->generateRowCellCss($worksheet, $cellAddress, $row, $colNum);
1595
1596
            // Cell Data
1597 529
            $cellData = $this->generateRowCellData($worksheet, $cell, $cssClass);
1598
1599
            // Hyperlink?
1600 529
            if ($worksheet->hyperlinkExists($coordinate) && !$worksheet->getHyperlink($coordinate)->isInternal()) {
1601 13
                $url = $worksheet->getHyperlink($coordinate)->getUrl();
1602 13
                $urlDecode1 = html_entity_decode($url, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
1603 13
                $urlTrim = Preg::replace('/^\\s+/u', '', $urlDecode1);
1604 13
                $parseScheme = Preg::isMatch('/^([\\w\\s\\x00-\\x1f]+):/u', strtolower($urlTrim), $matches);
1605 13
                if ($parseScheme && !in_array($matches[1], ['http', 'https', 'file', 'ftp', 'mailto', 's3'], true)) {
1606 3
                    $cellData = htmlspecialchars($url, Settings::htmlEntityFlags());
1607 3
                    $cellData = self::replaceControlChars($cellData);
1608
                } 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 11
                        . '>' . $cellData . '</a>';
1615
                }
1616
            }
1617
1618
            // Should the cell be written or is it swallowed by a rowspan or colspan?
1619 529
            $writeCell = !(isset($this->isSpannedCell[$worksheet->getParentOrThrow()->getIndex($worksheet)][$row + 1][$colNum])
1620 529
                && $this->isSpannedCell[$worksheet->getParentOrThrow()->getIndex($worksheet)][$row + 1][$colNum]);
1621
1622
            // Colspan and Rowspan
1623 529
            $colSpan = 1;
1624 529
            $rowSpan = 1;
1625 529
            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 20
                $colSpan = $spans['colspan'];
1629
1630
                //    Also apply style from last cell in merge to fix borders -
1631
                //        relies on !important for non-none border declarations in createCSSStyleBorder
1632 20
                $endCellCoord = Coordinate::stringFromColumnIndex($colNum + $colSpan) . ($row + $rowSpan);
1633 20
                if (!$this->useInlineCss) {
1634 17
                    $cssClass .= ' style' . $worksheet->getCell($endCellCoord)->getXfIndex();
1635
                } 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 4
                        if (str_contains($altValue, '!important')) {
1640 2
                            $cssClass[$altKey] = $altValue;
1641
                        }
1642
                    }
1643
                }
1644
            }
1645
1646
            // Write
1647 529
            if ($writeCell) {
1648 529
                $this->generateRowWriteCell($html, $worksheet, $coordinate, $cellType, $cellData, $colSpan, $rowSpan, $cssClass, $colNum, $sheetIndex, $row);
1649
            }
1650
1651
            // Next column
1652 529
            ++$colNum;
1653
        }
1654
1655
        // Write row end
1656 529
        $html .= '          </tr>' . PHP_EOL;
1657
1658
        // Return
1659 529
        return $html;
1660
    }
1661
1662 2
    private static function replaceNonAscii(array $matches): string
1663
    {
1664 2
        return '&#' . mb_ord($matches[0], 'UTF-8') . ';';
1665
    }
1666
1667 3
    private static function replaceControlChars(string $convert): string
1668
    {
1669 3
        return (string) preg_replace_callback(
1670 3
            '/[\\x00-\\x1f]/',
1671 3
            [self::class, 'replaceNonAscii'],
1672 3
            $convert
1673 3
        );
1674
    }
1675
1676
    /**
1677
     * Takes array where of CSS properties / values and converts to CSS string.
1678
     */
1679 531
    private function assembleCSS(array $values = []): string
1680
    {
1681 531
        $pairs = [];
1682 531
        foreach ($values as $property => $value) {
1683 531
            $pairs[] = $property . ':' . $value;
1684
        }
1685 531
        $string = implode('; ', $pairs);
1686
1687 531
        return $string;
1688
    }
1689
1690
    /**
1691
     * Get images root.
1692
     */
1693 18
    public function getImagesRoot(): string
1694
    {
1695 18
        return $this->imagesRoot;
1696
    }
1697
1698
    /**
1699
     * Set images root.
1700
     *
1701
     * @return $this
1702
     */
1703 1
    public function setImagesRoot(string $imagesRoot): static
1704
    {
1705 1
        $this->imagesRoot = $imagesRoot;
1706
1707 1
        return $this;
1708
    }
1709
1710
    /**
1711
     * Get embed images.
1712
     */
1713 5
    public function getEmbedImages(): bool
1714
    {
1715 5
        return $this->embedImages;
1716
    }
1717
1718
    /**
1719
     * Set embed images.
1720
     *
1721
     * @return $this
1722
     */
1723 4
    public function setEmbedImages(bool $embedImages): static
1724
    {
1725 4
        $this->embedImages = $embedImages;
1726
1727 4
        return $this;
1728
    }
1729
1730
    /**
1731
     * Get use inline CSS?
1732
     */
1733 1
    public function getUseInlineCss(): bool
1734
    {
1735 1
        return $this->useInlineCss;
1736
    }
1737
1738
    /**
1739
     * Set use inline CSS?
1740
     *
1741
     * @return $this
1742
     */
1743 15
    public function setUseInlineCss(bool $useInlineCss): static
1744
    {
1745 15
        $this->useInlineCss = $useInlineCss;
1746
1747 15
        return $this;
1748
    }
1749
1750
    /**
1751
     * Add color to formatted string as inline style.
1752
     *
1753
     * @param string $value Plain formatted value without color
1754
     * @param string $format Format code
1755
     */
1756 393
    public function formatColor(string $value, string $format): string
1757
    {
1758 393
        return self::formatColorStatic($value, $format);
1759
    }
1760
1761
    /**
1762
     * Add color to formatted string as inline style.
1763
     *
1764
     * @param string $value Plain formatted value without color
1765
     * @param string $format Format code
1766
     */
1767 393
    public static function formatColorStatic(string $value, string $format): string
1768
    {
1769
        // Color information, e.g. [Red] is always at the beginning
1770 393
        $color = null; // initialize
1771 393
        $matches = [];
1772
1773 393
        $color_regex = '/^\\[[a-zA-Z]+\\]/';
1774 393
        if (Preg::isMatch($color_regex, $format, $matches)) {
1775 17
            $color = str_replace(['[', ']'], '', $matches[0]);
1776 17
            $color = strtolower($color);
1777
        }
1778
1779
        // convert to PCDATA
1780 393
        $result = htmlspecialchars($value, Settings::htmlEntityFlags());
1781
1782
        // color span tag
1783 393
        if ($color !== null) {
1784 17
            $result = '<span style="color:' . $color . '">' . $result . '</span>';
1785
        }
1786
1787 393
        return $result;
1788
    }
1789
1790
    /**
1791
     * Calculate information about HTML colspan and rowspan which is not always the same as Excel's.
1792
     */
1793 531
    private function calculateSpans(): void
1794
    {
1795 531
        if ($this->spansAreCalculated) {
1796 531
            return;
1797
        }
1798
        // 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
        //   appropriate rowspan / colspan attribute
1801 531
        $sheetIndexes = $this->sheetIndex !== null
1802 531
            ? [$this->sheetIndex] : range(0, $this->spreadsheet->getSheetCount() - 1);
1803
1804 531
        foreach ($sheetIndexes as $sheetIndex) {
1805 531
            $sheet = $this->spreadsheet->getSheet($sheetIndex);
1806
1807 531
            $candidateSpannedRow = [];
1808
1809
            // loop through all Excel merged cells
1810 531
            foreach ($sheet->getMergeCells() as $cells) {
1811 20
                [$cells] = Coordinate::splitRange($cells);
1812 20
                $first = $cells[0];
1813 20
                $last = $cells[1];
1814
1815 20
                [$fc, $fr] = Coordinate::indexesFromString($first);
1816 20
                $fc = $fc - 1;
1817
1818 20
                [$lc, $lr] = Coordinate::indexesFromString($last);
1819 20
                $lc = $lc - 1;
1820
1821
                // loop through the individual cells in the individual merge
1822 20
                $r = $fr - 1;
1823 20
                while ($r++ < $lr) {
1824
                    // also, flag this row as a HTML row that is candidate to be omitted
1825 20
                    $candidateSpannedRow[$r] = $r;
1826
1827 20
                    $c = $fc - 1;
1828 20
                    while ($c++ < $lc) {
1829 20
                        if (!($c == $fc && $r == $fr)) {
1830
                            // not the upper-left cell (should not be written in HTML)
1831 20
                            $this->isSpannedCell[$sheetIndex][$r][$c] = [
1832 20
                                'baseCell' => [$fr, $fc],
1833 20
                            ];
1834
                        } else {
1835
                            // upper-left is the base cell that should hold the colspan/rowspan attribute
1836 20
                            $this->isBaseCell[$sheetIndex][$r][$c] = [
1837 20
                                'xlrowspan' => $lr - $fr + 1, // Excel rowspan
1838 20
                                'rowspan' => $lr - $fr + 1, // HTML rowspan, value may change
1839 20
                                'xlcolspan' => $lc - $fc + 1, // Excel colspan
1840 20
                                'colspan' => $lc - $fc + 1, // HTML colspan, value may change
1841 20
                            ];
1842
                        }
1843
                    }
1844
                }
1845
            }
1846
1847 531
            $this->calculateSpansOmitRows($sheet, $sheetIndex, $candidateSpannedRow);
1848
1849
            // TODO: Same for columns
1850
        }
1851
1852
        // We have calculated the spans
1853 531
        $this->spansAreCalculated = true;
1854
    }
1855
1856 531
    private function calculateSpansOmitRows(Worksheet $sheet, int $sheetIndex, array $candidateSpannedRow): void
1857
    {
1858
        // Identify which rows should be omitted in HTML. These are the rows where all the cells
1859
        //   participate in a merge and the where base cells are somewhere above.
1860 531
        $countColumns = Coordinate::columnIndexFromString($sheet->getHighestColumn());
1861 531
        foreach ($candidateSpannedRow as $rowIndex) {
1862 20
            if (isset($this->isSpannedCell[$sheetIndex][$rowIndex])) {
1863 20
                if (count($this->isSpannedCell[$sheetIndex][$rowIndex]) == $countColumns) {
1864 8
                    $this->isSpannedRow[$sheetIndex][$rowIndex] = $rowIndex;
1865
                }
1866
            }
1867
        }
1868
1869
        // For each of the omitted rows we found above, the affected rowspans should be subtracted by 1
1870 531
        if (isset($this->isSpannedRow[$sheetIndex])) {
1871 8
            foreach ($this->isSpannedRow[$sheetIndex] as $rowIndex) {
1872 8
                $adjustedBaseCells = [];
1873 8
                $c = -1;
1874 8
                $e = $countColumns - 1;
1875 8
                while ($c++ < $e) {
1876 8
                    $baseCell = $this->isSpannedCell[$sheetIndex][$rowIndex][$c]['baseCell'];
1877
1878 8
                    if (!in_array($baseCell, $adjustedBaseCells, true)) {
1879
                        // subtract rowspan by 1
1880 8
                        --$this->isBaseCell[$sheetIndex][$baseCell[0]][$baseCell[1]]['rowspan'];
1881 8
                        $adjustedBaseCells[] = $baseCell;
1882
                    }
1883
                }
1884
            }
1885
        }
1886
    }
1887
1888
    /**
1889
     * Write a comment in the same format as LibreOffice.
1890
     *
1891
     * @see https://github.com/LibreOffice/core/blob/9fc9bf3240f8c62ad7859947ab8a033ac1fe93fa/sc/source/filter/html/htmlexp.cxx#L1073-L1092
1892
     */
1893 529
    private function writeComment(Worksheet $worksheet, string $coordinate): string
1894
    {
1895 529
        $result = '';
1896 529
        if (!$this->isPdf && isset($worksheet->getComments()[$coordinate])) {
1897 24
            $sanitizedString = $this->generateRowCellDataValueRich($worksheet->getComment($coordinate)->getText());
1898 24
            $dir = ($worksheet->getComment($coordinate)->getTextboxDirection() === Comment::TEXTBOX_DIRECTION_RTL) ? ' dir="rtl"' : '';
1899 24
            $align = strtolower($worksheet->getComment($coordinate)->getAlignment());
1900 24
            $alignment = Alignment::HORIZONTAL_ALIGNMENT_FOR_HTML[$align] ?? '';
1901 24
            if ($alignment !== '') {
1902 2
                $alignment = " style=\"text-align:$alignment\"";
1903
            }
1904 24
            if ($sanitizedString !== '') {
1905 24
                $result .= '<a class="comment-indicator"></a>';
1906 24
                $result .= "<div class=\"comment\"$dir$alignment>" . $sanitizedString . '</div>';
1907 24
                $result .= PHP_EOL;
1908
            }
1909
        }
1910
1911 529
        return $result;
1912
    }
1913
1914 501
    public function getOrientation(): ?string
1915
    {
1916
        // Expect Pdf classes to override this method.
1917 501
        return $this->isPdf ? PageSetup::ORIENTATION_PORTRAIT : null;
1918
    }
1919
1920
    /**
1921
     * Generate @page declarations.
1922
     */
1923 531
    private function generatePageDeclarations(bool $generateSurroundingHTML): string
1924
    {
1925
        // Ensure that Spans have been calculated?
1926 531
        $this->calculateSpans();
1927
1928
        // Fetch sheets
1929 531
        $sheets = [];
1930 531
        if ($this->sheetIndex === null) {
1931 15
            $sheets = $this->spreadsheet->getAllSheets();
1932
        } else {
1933 523
            $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex);
1934
        }
1935
1936
        // Construct HTML
1937 531
        $htmlPage = $generateSurroundingHTML ? ('<style type="text/css">' . PHP_EOL) : '';
1938
1939
        // Loop all sheets
1940 531
        $sheetId = 0;
1941 531
        foreach ($sheets as $worksheet) {
1942 531
            $htmlPage .= "@page page$sheetId { ";
1943 531
            $left = StringHelper::formatNumber($worksheet->getPageMargins()->getLeft()) . 'in; ';
1944 531
            $htmlPage .= 'margin-left: ' . $left;
1945 531
            $right = StringHelper::FormatNumber($worksheet->getPageMargins()->getRight()) . 'in; ';
1946 531
            $htmlPage .= 'margin-right: ' . $right;
1947 531
            $top = StringHelper::FormatNumber($worksheet->getPageMargins()->getTop()) . 'in; ';
1948 531
            $htmlPage .= 'margin-top: ' . $top;
1949 531
            $bottom = StringHelper::FormatNumber($worksheet->getPageMargins()->getBottom()) . 'in; ';
1950 531
            $htmlPage .= 'margin-bottom: ' . $bottom;
1951 531
            $orientation = $this->getOrientation() ?? $worksheet->getPageSetup()->getOrientation();
1952 531
            if ($orientation === PageSetup::ORIENTATION_LANDSCAPE) {
1953 9
                $htmlPage .= 'size: landscape; ';
1954 523
            } elseif ($orientation === PageSetup::ORIENTATION_PORTRAIT) {
1955 13
                $htmlPage .= 'size: portrait; ';
1956
            }
1957 531
            $htmlPage .= '}' . PHP_EOL;
1958 531
            ++$sheetId;
1959
        }
1960 531
        $htmlPage .= implode(PHP_EOL, [
1961 531
            '.navigation {page-break-after: always;}',
1962 531
            '.scrpgbrk, div + div {page-break-before: always;}',
1963 531
            '@media screen {',
1964 531
            '  .gridlines td {border: 1px solid black;}',
1965 531
            '  .gridlines th {border: 1px solid black;}',
1966 531
            '  body>div {margin-top: 5px;}',
1967 531
            '  body>div:first-child {margin-top: 0;}',
1968 531
            '  .scrpgbrk {margin-top: 1px;}',
1969 531
            '}',
1970 531
            '@media print {',
1971 531
            '  .gridlinesp td {border: 1px solid black;}',
1972 531
            '  .gridlinesp th {border: 1px solid black;}',
1973 531
            '  .navigation {display: none;}',
1974 531
            '}',
1975 531
            '',
1976 531
        ]);
1977 531
        $htmlPage .= $generateSurroundingHTML ? ('</style>' . PHP_EOL) : '';
1978
1979 531
        return $htmlPage;
1980
    }
1981
1982 529
    private function shouldGenerateRow(Worksheet $sheet, int $row): bool
1983
    {
1984 529
        if (!($this instanceof Pdf\Mpdf || $this instanceof Pdf\Tcpdf)) {
1985 507
            return true;
1986
        }
1987
1988 28
        return $sheet->isRowVisible($row);
1989
    }
1990
1991 531
    private function shouldGenerateColumn(Worksheet $sheet, string $colStr): bool
1992
    {
1993 531
        if (!($this instanceof Pdf\Mpdf || $this instanceof Pdf\Tcpdf)) {
1994 509
            return true;
1995
        }
1996 28
        if (!$sheet->columnDimensionExists($colStr)) {
1997 27
            return true;
1998
        }
1999
2000 11
        return $sheet->getColumnDimension($colStr)->getVisible();
2001
    }
2002
2003 1
    public function getBetterBoolean(): bool
2004
    {
2005 1
        return $this->betterBoolean;
2006
    }
2007
2008 4
    public function setBetterBoolean(bool $betterBoolean): self
2009
    {
2010 4
        $this->betterBoolean = $betterBoolean;
2011
2012 4
        return $this;
2013
    }
2014
}
2015