Passed
Pull Request — master (#4327)
by Owen
19:24 queued 08:52
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 542
    public function __construct(Spreadsheet $spreadsheet)
156
    {
157 542
        $this->spreadsheet = $spreadsheet;
158 542
        $this->defaultFont = $this->spreadsheet->getDefaultStyle()->getFont();
159 542
        $calc = Calculation::getInstance($this->spreadsheet);
160 542
        $this->getTrue = $calc->getTRUE();
161 542
        $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 530
    public function generateHtmlAll(): string
187
    {
188 530
        $sheets = $this->generateSheetPrep();
189 530
        foreach ($sheets as $sheet) {
190 530
            $sheet->calculateArrays($this->preCalculateFormulas);
191
        }
192
        // garbage collect
193 530
        $this->spreadsheet->garbageCollect();
194
195 530
        $saveDebugLog = Calculation::getInstance($this->spreadsheet)->getDebugLog()->getWriteDebugLog();
196 530
        Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog(false);
197
198
        // Build CSS
199 530
        $this->buildCSS(!$this->useInlineCss);
200
201 530
        $html = '';
202
203
        // Write headers
204 530
        $html .= $this->generateHTMLHeader(!$this->useInlineCss);
205
206
        // Write navigation (tabs)
207 530
        if ((!$this->isPdf) && ($this->generateSheetNavigationBlock)) {
208 499
            $html .= $this->generateNavigation();
209
        }
210
211
        // Write data
212 530
        $html .= $this->generateSheetData();
213
214
        // Write footer
215 530
        $html .= $this->generateHTMLFooter();
216 530
        $callback = $this->editHtmlCallback;
217 530
        if ($callback) {
218 6
            $html = $callback($html);
219
        }
220
221 530
        Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog($saveDebugLog);
222
223 530
        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 532
    private function mapVAlign(string $vAlign): string
243
    {
244 532
        return Alignment::VERTICAL_ALIGNMENT_FOR_HTML[$vAlign] ?? '';
245
    }
246
247
    /**
248
     * Map HAlign.
249
     *
250
     * @param string $hAlign Horizontal alignment
251
     */
252 532
    private function mapHAlign(string $hAlign): string
253
    {
254 532
        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 18
    public function getSheetIndex(): ?int
287
    {
288 18
        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 532
    private static function generateMeta(?string $val, string $desc): string
340
    {
341 532
        return ($val || $val === '0')
342 532
            ? ('      <meta name="' . $desc . '" content="' . htmlspecialchars($val, Settings::htmlEntityFlags()) . '" />' . PHP_EOL)
343 532
            : '';
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 532
    public function generateHTMLHeader(bool $includeStyles = false): string
362
    {
363
        // Construct HTML
364 532
        $properties = $this->spreadsheet->getProperties();
365 532
        $html = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">' . PHP_EOL;
366 532
        $html .= '<html xmlns="http://www.w3.org/1999/xhtml">' . PHP_EOL;
367 532
        $html .= '  <head>' . PHP_EOL;
368 532
        $html .= '      <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />' . PHP_EOL;
369 532
        $html .= '      <meta name="generator" content="PhpSpreadsheet, https://github.com/PHPOffice/PhpSpreadsheet" />' . PHP_EOL;
370 532
        $title = $properties->getTitle();
371 532
        if ($title === '') {
372 11
            $title = $this->spreadsheet->getActiveSheet()->getTitle();
373
        }
374 532
        $html .= '      <title>' . htmlspecialchars($title, Settings::htmlEntityFlags()) . '</title>' . PHP_EOL;
375 532
        $html .= self::generateMeta($properties->getCreator(), 'author');
376 532
        $html .= self::generateMeta($properties->getTitle(), 'title');
377 532
        $html .= self::generateMeta($properties->getDescription(), 'description');
378 532
        $html .= self::generateMeta($properties->getSubject(), 'subject');
379 532
        $html .= self::generateMeta($properties->getKeywords(), 'keywords');
380 532
        $html .= self::generateMeta($properties->getCategory(), 'category');
381 532
        $html .= self::generateMeta($properties->getCompany(), 'company');
382 532
        $html .= self::generateMeta($properties->getManager(), 'manager');
383 532
        $html .= self::generateMeta($properties->getLastModifiedBy(), 'lastModifiedBy');
384 532
        $html .= self::generateMeta($properties->getViewport(), 'viewport');
385 532
        $date = Date::dateTimeFromTimestamp((string) $properties->getCreated());
386 532
        $date->setTimeZone(Date::getDefaultOrLocalTimeZone());
387 532
        $html .= self::generateMeta($date->format(DATE_W3C), 'created');
388 532
        $date = Date::dateTimeFromTimestamp((string) $properties->getModified());
389 532
        $date->setTimeZone(Date::getDefaultOrLocalTimeZone());
390 532
        $html .= self::generateMeta($date->format(DATE_W3C), 'modified');
391
392 532
        $customProperties = $properties->getCustomProperties();
393 532
        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 532
        if (!empty($properties->getHyperlinkBase())) {
412 2
            $html .= '      <base href="' . htmlspecialchars($properties->getHyperlinkBase()) . '" />' . PHP_EOL;
413
        }
414
415 532
        $html .= $includeStyles ? $this->generateStyles(true) : $this->generatePageDeclarations(true);
416
417 532
        $html .= '  </head>' . PHP_EOL;
418 532
        $html .= '' . PHP_EOL;
419 532
        $html .= self::BODY_LINE;
420
421 532
        return $html;
422
    }
423
424
    /** @return Worksheet[] */
425 530
    private function generateSheetPrep(): array
426
    {
427
        // Fetch sheets
428 530
        if ($this->sheetIndex === null) {
429 14
            $sheets = $this->spreadsheet->getAllSheets();
430
        } else {
431 523
            $sheets = [$this->spreadsheet->getSheet($this->sheetIndex)];
432
        }
433
434 530
        return $sheets;
435
    }
436
437 530
    private function generateSheetStarts(Worksheet $sheet, int $rowMin): array
438
    {
439
        // calculate start of <tbody>, <thead>
440 530
        $tbodyStart = $rowMin;
441 530
        $theadStart = $theadEnd = 0; // default: no <thead>    no </thead>
442 530
        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 530
        return [$theadStart, $theadEnd, $tbodyStart];
454
    }
455
456 530
    private function generateSheetTags(int $row, int $theadStart, int $theadEnd, int $tbodyStart): array
457
    {
458
        // <thead> ?
459 530
        $startTag = ($row == $theadStart) ? ('        <thead>' . PHP_EOL) : '';
460 530
        if (!$startTag) {
461 530
            $startTag = ($row == $tbodyStart) ? ('        <tbody>' . PHP_EOL) : '';
462
        }
463 530
        $endTag = ($row == $theadEnd) ? ('        </thead>' . PHP_EOL) : '';
464 530
        $cellType = ($row >= $tbodyStart) ? 'td' : 'th';
465
466 530
        return [$cellType, $startTag, $endTag];
467
    }
468
469
    /**
470
     * Generate sheet data.
471
     */
472 530
    public function generateSheetData(): string
473
    {
474
        // Ensure that Spans have been calculated?
475 530
        $this->calculateSpans();
476 530
        $sheets = $this->generateSheetPrep();
477
478
        // Construct HTML
479 530
        $html = '';
480
481
        // Loop all sheets
482 530
        $sheetId = 0;
483 530
        foreach ($sheets as $sheet) {
484
            // Write table header
485 530
            $html .= $this->generateTableHeader($sheet);
486 530
            $this->sheetCharts = [];
487 530
            $this->sheetDrawings = [];
488
489
            // Get worksheet dimension
490 530
            [$min, $max] = explode(':', $sheet->calculateWorksheetDataDimension());
491 530
            [$minCol, $minRow, $minColString] = Coordinate::indexesFromString($min);
492 530
            [$maxCol, $maxRow] = Coordinate::indexesFromString($max);
493 530
            $this->extendRowsAndColumns($sheet, $maxCol, $maxRow);
494
495 530
            [$theadStart, $theadEnd, $tbodyStart] = $this->generateSheetStarts($sheet, $minRow);
496
497
            // Loop through cells
498 530
            $row = $minRow - 1;
499 530
            while ($row++ < $maxRow) {
500 530
                [$cellType, $startTag, $endTag] = $this->generateSheetTags($row, $theadStart, $theadEnd, $tbodyStart);
501 530
                $html .= $startTag;
502
503
                // Write row if there are HTML table cells in it
504 530
                if ($this->shouldGenerateRow($sheet, $row) && !isset($this->isSpannedRow[$sheet->getParentOrThrow()->getIndex($sheet)][$row])) {
505
                    // Start a new rowData
506 530
                    $rowData = [];
507
                    // Loop through columns
508 530
                    $column = $minCol;
509 530
                    $colStr = $minColString;
510 530
                    while ($column <= $maxCol) {
511
                        // Cell exists?
512 530
                        $cellAddress = Coordinate::stringFromColumnIndex($column) . $row;
513 530
                        if ($this->shouldGenerateColumn($sheet, $colStr)) {
514 530
                            $rowData[$column] = ($sheet->getCellCollection()->has($cellAddress)) ? $cellAddress : '';
515
                        }
516 530
                        ++$column;
517 530
                        ++$colStr;
518
                    }
519 530
                    $html .= $this->generateRow($sheet, $rowData, $row - 1, $cellType);
520
                }
521
522 530
                $html .= $endTag;
523
            }
524
525
            // Write table footer
526 530
            $html .= $this->generateTableFooter();
527
            // Writing PDF?
528 530
            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 530
            ++$sheetId;
536
        }
537
538 530
        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 530
    private function extendRowsAndColumns(Worksheet $worksheet, int &$colMax, int &$rowMax): void
576
    {
577 530
        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 1
                        $rowMax = $chartTL[1];
585
                    }
586 4
                    if ($chartTL[0] > $colMax) {
587 2
                        $colMax = $chartTL[0];
588
                    }
589
                }
590
            }
591
        }
592 530
        foreach ($worksheet->getDrawingCollection() as $drawing) {
593 27
            if ($drawing instanceof Drawing && $drawing->getPath() === '') {
594 2
                continue;
595
            }
596 26
            $imageTL = Coordinate::indexesFromString($drawing->getCoordinates());
597 26
            $this->sheetDrawings[$drawing->getCoordinates()] = $drawing;
598 26
            if ($imageTL[1] > $rowMax) {
599
                $rowMax = $imageTL[1];
600
            }
601 26
            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 20
    public static function winFileToUrl(string $filename, bool $mpdf = false): string
613
    {
614
        // Windows filename
615 20
        if (substr($filename, 1, 2) === ':\\') {
616 1
            $protocol = $mpdf ? '' : 'file:///';
617 1
            $filename = $protocol . str_replace('\\', '/', $filename);
618
        }
619
620 20
        return $filename;
621
    }
622
623
    /**
624
     * Generate image tag in cell.
625
     *
626
     * @param string $coordinates Cell coordinates
627
     */
628 530
    private function writeImageInCell(string $coordinates): string
629
    {
630
        // Construct HTML
631 530
        $html = '';
632
633
        // Write images
634 530
        $drawing = $this->sheetDrawings[$coordinates] ?? null;
635 530
        if ($drawing !== null) {
636 26
            $opacity = '';
637 26
            $opacityValue = $drawing->getOpacity();
638 26
            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 26
            $filedesc = $drawing->getDescription();
645 26
            $filedesc = $filedesc ? htmlspecialchars($filedesc, ENT_QUOTES) : 'Embedded image';
646 26
            if ($drawing instanceof Drawing && $drawing->getPath() !== '') {
647 19
                $filename = $drawing->getPath();
648
649
                // Strip off eventual '.'
650 19
                $filename = Preg::replace('/^[.]/', '', $filename);
651
652
                // Prepend images root
653 19
                $filename = $this->getImagesRoot() . $filename;
654
655
                // Strip off eventual '.' if followed by non-/
656 19
                $filename = Preg::replace('@^[.]([^/])@', '$1', $filename);
657
658
                // Convert UTF8 data to PCDATA
659 19
                $filename = htmlspecialchars($filename, Settings::htmlEntityFlags());
660
661 19
                $html .= PHP_EOL;
662 19
                $imageData = self::winFileToUrl($filename, $this instanceof Pdf\Mpdf);
663
664 19
                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 19
                $html .= '<img style="' . $opacity . 'position: absolute; z-index: 1; left: '
678 19
                    . $drawing->getOffsetX() . 'px; top: ' . $drawing->getOffsetY() . 'px; width: '
679 19
                    . $drawing->getWidth() . 'px; height: ' . $drawing->getHeight() . 'px;" src="'
680 19
                    . $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 530
        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 524
    public function generateStyles(bool $generateSurroundingHTML = true): string
805
    {
806
        // Build CSS
807 524
        $css = $this->buildCSS($generateSurroundingHTML);
808
809
        // Construct HTML
810 524
        $html = '';
811
812
        // Start styles
813 524
        if ($generateSurroundingHTML) {
814 524
            $html .= '    <style type="text/css">' . PHP_EOL;
815 524
            $html .= (array_key_exists('html', $css)) ? ('      html { ' . $this->assembleCSS($css['html']) . ' }' . PHP_EOL) : '';
816
        }
817
818
        // Write all other styles
819 524
        foreach ($css as $styleName => $styleDefinition) {
820 524
            if ($styleName != 'html') {
821 524
                $html .= '      ' . $styleName . ' { ' . $this->assembleCSS($styleDefinition) . ' }' . PHP_EOL;
822
            }
823
        }
824 524
        $html .= $this->generatePageDeclarations(false);
825
826
        // End styles
827 524
        if ($generateSurroundingHTML) {
828 524
            $html .= '    </style>' . PHP_EOL;
829
        }
830
831
        // Return
832 524
        return $html;
833
    }
834
835 532
    private function buildCssRowHeights(Worksheet $sheet, array &$css, int $sheetIndex): void
836
    {
837
        // Calculate row heights
838 532
        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 532
    private function buildCssPerSheet(Worksheet $sheet, array &$css): void
856
    {
857
        // Calculate hash code
858 532
        $sheetIndex = $sheet->getParentOrThrow()->getIndex($sheet);
859 532
        $setup = $sheet->getPageSetup();
860 532
        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 532
        $picture = $sheet->getBackgroundImage();
865 532
        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 532
        $sheet->calculateColumnWidths();
873
874
        // col elements, initialize
875 532
        $highestColumnIndex = Coordinate::columnIndexFromString($sheet->getHighestColumn()) - 1;
876 532
        $column = -1;
877 532
        $colStr = 'A';
878 532
        while ($column++ < $highestColumnIndex) {
879 532
            $this->columnWidths[$sheetIndex][$column] = self::DEFAULT_CELL_WIDTH_POINTS; // approximation
880 532
            if ($this->shouldGenerateColumn($sheet, $colStr)) {
881 532
                $css['table.sheet' . $sheetIndex . ' col.col' . $column]['width'] = self::DEFAULT_CELL_WIDTH_POINTS . 'pt';
882
            }
883 532
            ++$colStr;
884
        }
885
886
        // col elements, loop through columnDimensions and set width
887 532
        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 532
        $rowDimension = $sheet->getDefaultRowDimension();
905
906
        // table.sheetN tr { }
907 532
        $css['table.sheet' . $sheetIndex . ' tr'] = [];
908
909 532
        if ($rowDimension->getRowHeight() == -1) {
910 524
            $pt_height = SharedFont::getDefaultRowHeightByFont($this->spreadsheet->getDefaultStyle()->getFont());
911
        } else {
912 8
            $pt_height = $rowDimension->getRowHeight();
913
        }
914 532
        $css['table.sheet' . $sheetIndex . ' tr']['height'] = $pt_height . 'pt';
915 532
        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 532
        $this->buildCssRowHeights($sheet, $css, $sheetIndex);
921
    }
922
923
    /**
924
     * Build CSS styles.
925
     *
926
     * @param bool $generateSurroundingHTML Generate surrounding HTML style? (html { })
927
     */
928 532
    public function buildCSS(bool $generateSurroundingHTML = true): array
929
    {
930
        // Cached?
931 532
        if ($this->cssStyles !== null) {
932 522
            return $this->cssStyles;
933
        }
934
935
        // Ensure that spans have been calculated
936 532
        $this->calculateSpans();
937
938
        // Construct CSS
939 532
        $css = [];
940
941
        // Start styles
942 532
        if ($generateSurroundingHTML) {
943
            // html { }
944 523
            $css['html']['font-family'] = 'Calibri, Arial, Helvetica, sans-serif';
945 523
            $css['html']['font-size'] = '11pt';
946 523
            $css['html']['background-color'] = 'white';
947
        }
948
949
        // CSS for comments as found in LibreOffice
950 532
        $css['a.comment-indicator:hover + div.comment'] = [
951 532
            'background' => '#ffd',
952 532
            'position' => 'absolute',
953 532
            'display' => 'block',
954 532
            'border' => '1px solid black',
955 532
            'padding' => '0.5em',
956 532
        ];
957
958 532
        $css['a.comment-indicator'] = [
959 532
            'background' => 'red',
960 532
            'display' => 'inline-block',
961 532
            'border' => '1px solid black',
962 532
            'width' => '0.5em',
963 532
            'height' => '0.5em',
964 532
        ];
965
966 532
        $css['div.comment']['display'] = 'none';
967
968
        // table { }
969 532
        $css['table']['border-collapse'] = 'collapse';
970
971
        // .b {}
972 532
        $css['.b']['text-align'] = 'center'; // BOOL
973
974
        // .e {}
975 532
        $css['.e']['text-align'] = 'center'; // ERROR
976
977
        // .f {}
978 532
        $css['.f']['text-align'] = 'right'; // FORMULA
979
980
        // .inlineStr {}
981 532
        $css['.inlineStr']['text-align'] = 'left'; // INLINE
982
983
        // .n {}
984 532
        $css['.n']['text-align'] = 'right'; // NUMERIC
985
986
        // .s {}
987 532
        $css['.s']['text-align'] = 'left'; // STRING
988
989
        // Calculate cell style hashes
990 532
        foreach ($this->spreadsheet->getCellXfCollection() as $index => $style) {
991 532
            $css['td.style' . $index . ', th.style' . $index] = $this->createCSSStyle($style);
992
            //$css['th.style' . $index] = $this->createCSSStyle($style);
993
        }
994
995
        // Fetch sheets
996 532
        $sheets = [];
997 532
        if ($this->sheetIndex === null) {
998 15
            $sheets = $this->spreadsheet->getAllSheets();
999
        } else {
1000 524
            $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex);
1001
        }
1002
1003
        // Build styles per sheet
1004 532
        foreach ($sheets as $sheet) {
1005 532
            $this->buildCssPerSheet($sheet, $css);
1006
        }
1007
1008
        // Cache
1009 532
        if ($this->cssStyles === null) {
1010 532
            $this->cssStyles = $css;
1011
        }
1012
1013
        // Return
1014 532
        return $css;
1015
    }
1016
1017
    /**
1018
     * Create CSS style.
1019
     */
1020 532
    private function createCSSStyle(Style $style): array
1021
    {
1022
        // Create CSS
1023 532
        return array_merge(
1024 532
            $this->createCSSStyleAlignment($style->getAlignment()),
1025 532
            $this->createCSSStyleBorders($style->getBorders()),
1026 532
            $this->createCSSStyleFont($style->getFont()),
1027 532
            $this->createCSSStyleFill($style->getFill())
1028 532
        );
1029
    }
1030
1031
    /**
1032
     * Create CSS style.
1033
     */
1034 532
    private function createCSSStyleAlignment(Alignment $alignment): array
1035
    {
1036
        // Construct CSS
1037 532
        $css = [];
1038
1039
        // Create CSS
1040 532
        $verticalAlign = $this->mapVAlign($alignment->getVertical() ?? '');
1041 532
        if ($verticalAlign) {
1042 532
            $css['vertical-align'] = $verticalAlign;
1043
        }
1044 532
        $textAlign = $this->mapHAlign($alignment->getHorizontal() ?? '');
1045 532
        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 532
        $rotation = $alignment->getTextRotation();
1052 532
        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 532
        return $css;
1061
    }
1062
1063
    /**
1064
     * Create CSS style.
1065
     */
1066 532
    private function createCSSStyleFont(Font $font): array
1067
    {
1068
        // Construct CSS
1069 532
        $css = [];
1070
1071
        // Create CSS
1072 532
        if ($font->getBold()) {
1073 20
            $css['font-weight'] = 'bold';
1074
        }
1075 532
        if ($font->getUnderline() != Font::UNDERLINE_NONE && $font->getStrikethrough()) {
1076 1
            $css['text-decoration'] = 'underline line-through';
1077 532
        } elseif ($font->getUnderline() != Font::UNDERLINE_NONE) {
1078 12
            $css['text-decoration'] = 'underline';
1079 532
        } elseif ($font->getStrikethrough()) {
1080 1
            $css['text-decoration'] = 'line-through';
1081
        }
1082 532
        if ($font->getItalic()) {
1083 10
            $css['font-style'] = 'italic';
1084
        }
1085
1086 532
        $css['color'] = '#' . $font->getColor()->getRGB();
1087 532
        $css['font-family'] = '\'' . htmlspecialchars((string) $font->getName(), ENT_QUOTES) . '\'';
1088 532
        $css['font-size'] = $font->getSize() . 'pt';
1089
1090 532
        return $css;
1091
    }
1092
1093
    /**
1094
     * Create CSS style.
1095
     *
1096
     * @param Borders $borders Borders
1097
     */
1098 532
    private function createCSSStyleBorders(Borders $borders): array
1099
    {
1100
        // Construct CSS
1101 532
        $css = [];
1102
1103
        // Create CSS
1104 532
        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 23
            if ($borders->getBottom()->getBorderStyle() !== Border::BORDER_NONE) {
1112 6
                $css['border-bottom'] = $this->createCSSStyleBorder($borders->getBottom());
1113
            }
1114 23
            if ($borders->getTop()->getBorderStyle() !== Border::BORDER_NONE) {
1115 6
                $css['border-top'] = $this->createCSSStyleBorder($borders->getTop());
1116
            }
1117 23
            if ($borders->getLeft()->getBorderStyle() !== Border::BORDER_NONE) {
1118 6
                $css['border-left'] = $this->createCSSStyleBorder($borders->getLeft());
1119
            }
1120 23
            if ($borders->getRight()->getBorderStyle() !== Border::BORDER_NONE) {
1121 6
                $css['border-right'] = $this->createCSSStyleBorder($borders->getRight());
1122
            }
1123
        }
1124
1125 532
        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 532
    private function createCSSStyleFill(Fill $fill): array
1147
    {
1148
        // Construct HTML
1149 532
        $css = [];
1150
1151
        // Create CSS
1152 532
        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 532
        return $css;
1166
    }
1167
1168
    /**
1169
     * Generate HTML footer.
1170
     */
1171 530
    public function generateHTMLFooter(): string
1172
    {
1173
        // Construct HTML
1174 530
        $html = '';
1175 530
        $html .= '  </body>' . PHP_EOL;
1176 530
        $html .= '</html>' . PHP_EOL;
1177
1178 530
        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 530
    private function generateTableTag(Worksheet $worksheet, string $id, string &$html, int $sheetIndex): void
1202
    {
1203 530
        if (!$this->useInlineCss) {
1204 522
            $gridlines = $worksheet->getShowGridlines() ? ' gridlines' : '';
1205 522
            $gridlinesp = $worksheet->getPrintGridlines() ? ' gridlinesp' : '';
1206 522
            $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 530
    private function generateTableHeader(Worksheet $worksheet, bool $showid = true): string
1219
    {
1220 530
        $sheetIndex = $worksheet->getParentOrThrow()->getIndex($worksheet);
1221
1222
        // Construct HTML
1223 530
        $html = '';
1224 530
        $id = $showid ? "id='sheet$sheetIndex'" : '';
1225 530
        if ($showid) {
1226 530
            $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 530
        $this->generateTableTag($worksheet, $id, $html, $sheetIndex);
1232
1233
        // Write <col> elements
1234 530
        $highestColumnIndex = Coordinate::columnIndexFromString($worksheet->getHighestColumn()) - 1;
1235 530
        $i = -1;
1236 530
        while ($i++ < $highestColumnIndex) {
1237 530
            if (!$this->useInlineCss) {
1238 522
                $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 530
        return $html;
1247
    }
1248
1249
    /**
1250
     * Generate table footer.
1251
     */
1252 530
    private function generateTableFooter(): string
1253
    {
1254 530
        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 530
    private function generateRowStart(Worksheet $worksheet, int $sheetIndex, int $row): string
1264
    {
1265 530
        $html = '';
1266 530
        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 530
        if (!$this->useInlineCss) {
1285 522
            $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 530
        return $html;
1294
    }
1295
1296 530
    private function generateRowCellCss(Worksheet $worksheet, string $cellAddress, int $row, int $columnNumber): array
1297
    {
1298 530
        $cell = ($cellAddress > '') ? $worksheet->getCellCollection()->get($cellAddress) : '';
1299 530
        $coordinate = Coordinate::stringFromColumnIndex($columnNumber + 1) . ($row + 1);
1300 530
        if (!$this->useInlineCss) {
1301 522
            $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 530
        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 528
    private function generateRowCellDataValue(Worksheet $worksheet, Cell $cell, string &$cellData): void
1363
    {
1364 528
        if ($cell->getValue() instanceof RichText) {
1365 11
            $cellData .= $this->generateRowCellDataValueRich($cell->getValue());
1366
        } else {
1367 528
            if ($this->preCalculateFormulas) {
1368 527
                $origData = $cell->getCalculatedValue();
1369 527
                if ($this->betterBoolean && is_bool($origData)) {
1370 3
                    $origData2 = $origData ? $this->getTrue : $this->getFalse;
1371
                } else {
1372 527
                    $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 528
            $formatCode = $worksheet->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())->getNumberFormat()->getFormatCode();
1383
1384 528
            $cellData = NumberFormat::toFormattedString(
1385 528
                $origData2,
1386 528
                $formatCode ?? NumberFormat::FORMAT_GENERAL,
1387 528
                [$this, 'formatColor']
1388 528
            );
1389
1390 528
            if ($cellData === $origData) {
1391 106
                $cellData = htmlspecialchars($cellData, Settings::htmlEntityFlags());
1392
            }
1393 528
            if ($worksheet->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())->getFont()->getSuperscript()) {
1394 1
                $cellData = '<sup>' . $cellData . '</sup>';
1395 528
            } elseif ($worksheet->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())->getFont()->getSubscript()) {
1396 1
                $cellData = '<sub>' . $cellData . '</sub>';
1397
            }
1398
        }
1399
    }
1400
1401 530
    private function generateRowCellData(Worksheet $worksheet, null|Cell|string $cell, array|string &$cssClass): string
1402
    {
1403 530
        $cellData = '&nbsp;';
1404 530
        if ($cell instanceof Cell) {
1405 528
            $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 528
            $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 528
            $cellData = Preg::replace('/(?m)(?:^|\\G) /', '&nbsp;', $cellData);
1416
1417
            // convert newline "\n" to '<br>'
1418 528
            $cellData = nl2br($cellData);
1419
1420
            // Extend CSS class?
1421 528
            $dataType = $cell->getDataType();
1422 528
            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 528
            if (!$this->useInlineCss && is_string($cssClass)) {
1433 521
                $cssClass .= ' style' . $cell->getXfIndex();
1434 521
                $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 52
            if (is_string($cssClass)) {
1454 48
                $cssClass .= ' style0';
1455
            }
1456
        }
1457
1458 530
        return $cellData;
1459
    }
1460
1461 530
    private function generateRowIncludeCharts(Worksheet $worksheet, string $coordinate): string
1462
    {
1463 530
        return $this->includeCharts ? $this->writeChartInCell($worksheet, $coordinate) : '';
1464
    }
1465
1466 530
    private function generateRowSpans(string $html, int $rowSpan, int $colSpan): string
1467
    {
1468 530
        $html .= ($colSpan > 1) ? (' colspan="' . $colSpan . '"') : '';
1469 530
        $html .= ($rowSpan > 1) ? (' rowspan="' . $rowSpan . '"') : '';
1470
1471 530
        return $html;
1472
    }
1473
1474 530
    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 530
        $htmlx = $this->writeImageInCell($coordinate);
1489
        // Chart?
1490 530
        $htmlx .= $this->generateRowIncludeCharts($worksheet, $coordinate);
1491
        // Column start
1492 530
        $html .= '            <' . $cellType;
1493 530
        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 530
        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 43
            if ($this->useInlineCss) {
1513 14
                $xcssClass = is_array($cssClass) ? $cssClass : [];
1514
            } else {
1515 30
                if (is_string($cssClass)) {
1516 30
                    $html .= ' class="' . $cssClass . '"';
1517
                }
1518 30
                $xcssClass = [];
1519
            }
1520 43
            $width = 0;
1521 43
            $i = $colNum - 1;
1522 43
            $e = $colNum + $colSpan - 1;
1523 43
            while ($i++ < $e) {
1524 43
                if (isset($this->columnWidths[$sheetIndex][$i])) {
1525 43
                    $width += $this->columnWidths[$sheetIndex][$i];
1526
                }
1527
            }
1528 43
            $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 43
            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 43
            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 43
            if ($htmlx) {
1545 11
                $xcssClass['position'] = 'relative';
1546
            }
1547 43
            $html .= ' style="' . $this->assembleCSS($xcssClass) . '"';
1548 43
            if ($this->useInlineCss) {
1549 14
                $html .= ' class="gridlines gridlinesp"';
1550
            }
1551
        }
1552 530
        $html = $this->generateRowSpans($html, $rowSpan, $colSpan);
1553
1554 530
        $html .= '>';
1555 530
        $html .= $htmlx;
1556
1557 530
        $html .= $this->writeComment($worksheet, $coordinate);
1558
1559
        // Cell data
1560 530
        $html .= $cellData;
1561
1562
        // Column end
1563 530
        $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 530
    private function generateRow(Worksheet $worksheet, array $values, int $row, string $cellType): string
1574
    {
1575
        // Sheet index
1576 530
        $sheetIndex = $worksheet->getParentOrThrow()->getIndex($worksheet);
1577 530
        $html = $this->generateRowStart($worksheet, $sheetIndex, $row);
1578
1579
        // Write cells
1580 530
        $colNum = 0;
1581 530
        $tcpdfInited = false;
1582 530
        foreach ($values as $key => $cellAddress) {
1583 530
            if ($this instanceof Pdf\Mpdf) {
1584 23
                $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 530
            [$cell, $cssClass, $coordinate] = $this->generateRowCellCss($worksheet, $cellAddress, $row, $colNum);
1595
1596
            // Cell Data
1597 530
            $cellData = $this->generateRowCellData($worksheet, $cell, $cssClass);
1598
1599
            // Hyperlink?
1600 530
            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 530
            $writeCell = !(isset($this->isSpannedCell[$worksheet->getParentOrThrow()->getIndex($worksheet)][$row + 1][$colNum])
1620 530
                && $this->isSpannedCell[$worksheet->getParentOrThrow()->getIndex($worksheet)][$row + 1][$colNum]);
1621
1622
            // Colspan and Rowspan
1623 530
            $colSpan = 1;
1624 530
            $rowSpan = 1;
1625 530
            if (isset($this->isBaseCell[$worksheet->getParentOrThrow()->getIndex($worksheet)][$row + 1][$colNum])) {
1626 21
                $spans = $this->isBaseCell[$worksheet->getParentOrThrow()->getIndex($worksheet)][$row + 1][$colNum];
1627 21
                $rowSpan = $spans['rowspan'];
1628 21
                $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 21
                $endCellCoord = Coordinate::stringFromColumnIndex($colNum + $colSpan) . ($row + $rowSpan);
1633 21
                if (!$this->useInlineCss) {
1634 18
                    $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 530
            if ($writeCell) {
1648 530
                $this->generateRowWriteCell($html, $worksheet, $coordinate, $cellType, $cellData, $colSpan, $rowSpan, $cssClass, $colNum, $sheetIndex, $row);
1649
            }
1650
1651
            // Next column
1652 530
            ++$colNum;
1653
        }
1654
1655
        // Write row end
1656 530
        $html .= '          </tr>' . PHP_EOL;
1657
1658
        // Return
1659 530
        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 532
    private function assembleCSS(array $values = []): string
1680
    {
1681 532
        $pairs = [];
1682 532
        foreach ($values as $property => $value) {
1683 532
            $pairs[] = $property . ':' . $value;
1684
        }
1685 532
        $string = implode('; ', $pairs);
1686
1687 532
        return $string;
1688
    }
1689
1690
    /**
1691
     * Get images root.
1692
     */
1693 19
    public function getImagesRoot(): string
1694
    {
1695 19
        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 532
    private function calculateSpans(): void
1794
    {
1795 532
        if ($this->spansAreCalculated) {
1796 532
            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 532
        $sheetIndexes = $this->sheetIndex !== null
1802 532
            ? [$this->sheetIndex] : range(0, $this->spreadsheet->getSheetCount() - 1);
1803
1804 532
        foreach ($sheetIndexes as $sheetIndex) {
1805 532
            $sheet = $this->spreadsheet->getSheet($sheetIndex);
1806
1807 532
            $candidateSpannedRow = [];
1808
1809
            // loop through all Excel merged cells
1810 532
            foreach ($sheet->getMergeCells() as $cells) {
1811 21
                [$cells] = Coordinate::splitRange($cells);
1812 21
                $first = $cells[0];
1813 21
                $last = $cells[1];
1814
1815 21
                [$fc, $fr] = Coordinate::indexesFromString($first);
1816 21
                $fc = $fc - 1;
1817
1818 21
                [$lc, $lr] = Coordinate::indexesFromString($last);
1819 21
                $lc = $lc - 1;
1820
1821
                // loop through the individual cells in the individual merge
1822 21
                $r = $fr - 1;
1823 21
                while ($r++ < $lr) {
1824
                    // also, flag this row as a HTML row that is candidate to be omitted
1825 21
                    $candidateSpannedRow[$r] = $r;
1826
1827 21
                    $c = $fc - 1;
1828 21
                    while ($c++ < $lc) {
1829 21
                        if (!($c == $fc && $r == $fr)) {
1830
                            // not the upper-left cell (should not be written in HTML)
1831 21
                            $this->isSpannedCell[$sheetIndex][$r][$c] = [
1832 21
                                'baseCell' => [$fr, $fc],
1833 21
                            ];
1834
                        } else {
1835
                            // upper-left is the base cell that should hold the colspan/rowspan attribute
1836 21
                            $this->isBaseCell[$sheetIndex][$r][$c] = [
1837 21
                                'xlrowspan' => $lr - $fr + 1, // Excel rowspan
1838 21
                                'rowspan' => $lr - $fr + 1, // HTML rowspan, value may change
1839 21
                                'xlcolspan' => $lc - $fc + 1, // Excel colspan
1840 21
                                'colspan' => $lc - $fc + 1, // HTML colspan, value may change
1841 21
                            ];
1842
                        }
1843
                    }
1844
                }
1845
            }
1846
1847 532
            $this->calculateSpansOmitRows($sheet, $sheetIndex, $candidateSpannedRow);
1848
1849
            // TODO: Same for columns
1850
        }
1851
1852
        // We have calculated the spans
1853 532
        $this->spansAreCalculated = true;
1854
    }
1855
1856 532
    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 532
        $countColumns = Coordinate::columnIndexFromString($sheet->getHighestColumn());
1861 532
        foreach ($candidateSpannedRow as $rowIndex) {
1862 21
            if (isset($this->isSpannedCell[$sheetIndex][$rowIndex])) {
1863 21
                if (count($this->isSpannedCell[$sheetIndex][$rowIndex]) == $countColumns) {
1864 9
                    $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 532
        if (isset($this->isSpannedRow[$sheetIndex])) {
1871 9
            foreach ($this->isSpannedRow[$sheetIndex] as $rowIndex) {
1872 9
                $adjustedBaseCells = [];
1873 9
                $c = -1;
1874 9
                $e = $countColumns - 1;
1875 9
                while ($c++ < $e) {
1876 9
                    $baseCell = $this->isSpannedCell[$sheetIndex][$rowIndex][$c]['baseCell'];
1877
1878 9
                    if (!in_array($baseCell, $adjustedBaseCells, true)) {
1879
                        // subtract rowspan by 1
1880 9
                        --$this->isBaseCell[$sheetIndex][$baseCell[0]][$baseCell[1]]['rowspan'];
1881 9
                        $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 530
    private function writeComment(Worksheet $worksheet, string $coordinate): string
1894
    {
1895 530
        $result = '';
1896 530
        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 530
        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 532
    private function generatePageDeclarations(bool $generateSurroundingHTML): string
1924
    {
1925
        // Ensure that Spans have been calculated?
1926 532
        $this->calculateSpans();
1927
1928
        // Fetch sheets
1929 532
        $sheets = [];
1930 532
        if ($this->sheetIndex === null) {
1931 15
            $sheets = $this->spreadsheet->getAllSheets();
1932
        } else {
1933 524
            $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex);
1934
        }
1935
1936
        // Construct HTML
1937 532
        $htmlPage = $generateSurroundingHTML ? ('<style type="text/css">' . PHP_EOL) : '';
1938
1939
        // Loop all sheets
1940 532
        $sheetId = 0;
1941 532
        foreach ($sheets as $worksheet) {
1942 532
            $htmlPage .= "@page page$sheetId { ";
1943 532
            $left = StringHelper::formatNumber($worksheet->getPageMargins()->getLeft()) . 'in; ';
1944 532
            $htmlPage .= 'margin-left: ' . $left;
1945 532
            $right = StringHelper::FormatNumber($worksheet->getPageMargins()->getRight()) . 'in; ';
1946 532
            $htmlPage .= 'margin-right: ' . $right;
1947 532
            $top = StringHelper::FormatNumber($worksheet->getPageMargins()->getTop()) . 'in; ';
1948 532
            $htmlPage .= 'margin-top: ' . $top;
1949 532
            $bottom = StringHelper::FormatNumber($worksheet->getPageMargins()->getBottom()) . 'in; ';
1950 532
            $htmlPage .= 'margin-bottom: ' . $bottom;
1951 532
            $orientation = $this->getOrientation() ?? $worksheet->getPageSetup()->getOrientation();
1952 532
            if ($orientation === PageSetup::ORIENTATION_LANDSCAPE) {
1953 9
                $htmlPage .= 'size: landscape; ';
1954 524
            } elseif ($orientation === PageSetup::ORIENTATION_PORTRAIT) {
1955 13
                $htmlPage .= 'size: portrait; ';
1956
            }
1957 532
            $htmlPage .= '}' . PHP_EOL;
1958 532
            ++$sheetId;
1959
        }
1960 532
        $htmlPage .= implode(PHP_EOL, [
1961 532
            '.navigation {page-break-after: always;}',
1962 532
            '.scrpgbrk, div + div {page-break-before: always;}',
1963 532
            '@media screen {',
1964 532
            '  .gridlines td {border: 1px solid black;}',
1965 532
            '  .gridlines th {border: 1px solid black;}',
1966 532
            '  body>div {margin-top: 5px;}',
1967 532
            '  body>div:first-child {margin-top: 0;}',
1968 532
            '  .scrpgbrk {margin-top: 1px;}',
1969 532
            '}',
1970 532
            '@media print {',
1971 532
            '  .gridlinesp td {border: 1px solid black;}',
1972 532
            '  .gridlinesp th {border: 1px solid black;}',
1973 532
            '  .navigation {display: none;}',
1974 532
            '}',
1975 532
            '',
1976 532
        ]);
1977 532
        $htmlPage .= $generateSurroundingHTML ? ('</style>' . PHP_EOL) : '';
1978
1979 532
        return $htmlPage;
1980
    }
1981
1982 530
    private function shouldGenerateRow(Worksheet $sheet, int $row): bool
1983
    {
1984 530
        if (!($this instanceof Pdf\Mpdf || $this instanceof Pdf\Tcpdf)) {
1985 507
            return true;
1986
        }
1987
1988 29
        return $sheet->isRowVisible($row);
1989
    }
1990
1991 532
    private function shouldGenerateColumn(Worksheet $sheet, string $colStr): bool
1992
    {
1993 532
        if (!($this instanceof Pdf\Mpdf || $this instanceof Pdf\Tcpdf)) {
1994 509
            return true;
1995
        }
1996 29
        if (!$sheet->columnDimensionExists($colStr)) {
1997 28
            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