Html::generateRowWriteCell()   F
last analyzed

Complexity

Conditions 41
Paths 10230

Size

Total Lines 144
Code Lines 81

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 78
CRAP Score 41.0034

Importance

Changes 0
Metric Value
eloc 81
c 0
b 0
f 0
dl 0
loc 144
ccs 78
cts 79
cp 0.9873
rs 0
cc 41
nc 10230
nop 12
crap 41.0034

How to fix   Long Method    Complexity    Many Parameters   

Long Method

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

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

Commonly applied refactorings include:

Many Parameters

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

There are several approaches to avoid long parameter lists:

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