Failed Conditions
Push — master ( d4ffa3...a06996 )
by
unknown
16:24 queued 06:10
created

Html::generateRowWriteCell()   C

Complexity

Conditions 16
Paths 66

Size

Total Lines 80
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 38
CRAP Score 16

Importance

Changes 0
Metric Value
eloc 39
c 0
b 0
f 0
dl 0
loc 80
rs 5.5666
ccs 38
cts 38
cp 1
cc 16
nc 66
nop 11
crap 16

How to fix   Long Method    Complexity    Many Parameters   

Long Method

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

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

Commonly applied refactorings include:

Many Parameters

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

There are several approaches to avoid long parameter lists:

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