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