Passed
Pull Request — master (#4322)
by Owen
12:23
created

Html::generateRowWriteCell()   F

Complexity

Conditions 22
Paths 330

Size

Total Lines 90
Code Lines 47

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 46
CRAP Score 22

Importance

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

How to fix   Long Method    Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

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

There are several approaches to avoid long parameter lists:

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