Failed Conditions
Push — master ( 8e3417...f52ae2 )
by
unknown
18:26 queued 07:14
created

Html::generateRowWriteCell()   F

Complexity

Conditions 41
Paths 10230

Size

Total Lines 140
Code Lines 81

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 78
CRAP Score 41.0034

Importance

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