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