Passed
Pull Request — master (#4142)
by Owen
16:30 queued 03:39
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 515
    public function __construct(Spreadsheet $spreadsheet)
148
    {
149 515
        $this->spreadsheet = $spreadsheet;
150 515
        $this->defaultFont = $this->spreadsheet->getDefaultStyle()->getFont();
151
    }
152
153
    /**
154
     * Save Spreadsheet to file.
155
     *
156
     * @param resource|string $filename
157
     */
158 444
    public function save($filename, int $flags = 0): void
159
    {
160 444
        $this->processFlags($flags);
161
162
        // Open file
163 444
        $this->openFileHandle($filename);
164
165
        // Write html
166 443
        fwrite($this->fileHandle, $this->generateHTMLAll());
167
168
        // Close file
169 443
        $this->maybeCloseFileHandle();
170
    }
171
172
    /**
173
     * Save Spreadsheet as html to variable.
174
     */
175 504
    public function generateHtmlAll(): string
176
    {
177 504
        $sheets = $this->generateSheetPrep();
178 504
        foreach ($sheets as $sheet) {
179 504
            $sheet->calculateArrays($this->preCalculateFormulas);
180
        }
181
        // garbage collect
182 504
        $this->spreadsheet->garbageCollect();
183
184 504
        $saveDebugLog = Calculation::getInstance($this->spreadsheet)->getDebugLog()->getWriteDebugLog();
185 504
        Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog(false);
186
187
        // Build CSS
188 504
        $this->buildCSS(!$this->useInlineCss);
189
190 504
        $html = '';
191
192
        // Write headers
193 504
        $html .= $this->generateHTMLHeader(!$this->useInlineCss);
194
195
        // Write navigation (tabs)
196 504
        if ((!$this->isPdf) && ($this->generateSheetNavigationBlock)) {
197 477
            $html .= $this->generateNavigation();
198
        }
199
200
        // Write data
201 504
        $html .= $this->generateSheetData();
202
203
        // Write footer
204 504
        $html .= $this->generateHTMLFooter();
205 504
        $callback = $this->editHtmlCallback;
206 504
        if ($callback) {
207 6
            $html = $callback($html);
208
        }
209
210 504
        Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog($saveDebugLog);
211
212 504
        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 506
    private function mapVAlign(string $vAlign): string
232
    {
233 506
        return Alignment::VERTICAL_ALIGNMENT_FOR_HTML[$vAlign] ?? '';
234
    }
235
236
    /**
237
     * Map HAlign.
238
     *
239
     * @param string $hAlign Horizontal alignment
240
     */
241 506
    private function mapHAlign(string $hAlign): string
242
    {
243 506
        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 495
    private function mapBorderStyle($borderStyle): string
268
    {
269 495
        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 506
    private static function generateMeta(?string $val, string $desc): string
329
    {
330 506
        return ($val || $val === '0')
331 506
            ? ('      <meta name="' . $desc . '" content="' . htmlspecialchars($val, Settings::htmlEntityFlags()) . '" />' . PHP_EOL)
332 506
            : '';
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 506
    public function generateHTMLHeader(bool $includeStyles = false): string
351
    {
352
        // Construct HTML
353 506
        $properties = $this->spreadsheet->getProperties();
354 506
        $html = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">' . PHP_EOL;
355 506
        $html .= '<html xmlns="http://www.w3.org/1999/xhtml">' . PHP_EOL;
356 506
        $html .= '  <head>' . PHP_EOL;
357 506
        $html .= '      <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />' . PHP_EOL;
358 506
        $html .= '      <meta name="generator" content="PhpSpreadsheet, https://github.com/PHPOffice/PhpSpreadsheet" />' . PHP_EOL;
359 506
        $title = $properties->getTitle();
360 506
        if ($title === '') {
361 7
            $title = $this->spreadsheet->getActiveSheet()->getTitle();
362
        }
363 506
        $html .= '      <title>' . htmlspecialchars($title, Settings::htmlEntityFlags()) . '</title>' . PHP_EOL;
364 506
        $html .= self::generateMeta($properties->getCreator(), 'author');
365 506
        $html .= self::generateMeta($properties->getTitle(), 'title');
366 506
        $html .= self::generateMeta($properties->getDescription(), 'description');
367 506
        $html .= self::generateMeta($properties->getSubject(), 'subject');
368 506
        $html .= self::generateMeta($properties->getKeywords(), 'keywords');
369 506
        $html .= self::generateMeta($properties->getCategory(), 'category');
370 506
        $html .= self::generateMeta($properties->getCompany(), 'company');
371 506
        $html .= self::generateMeta($properties->getManager(), 'manager');
372 506
        $html .= self::generateMeta($properties->getLastModifiedBy(), 'lastModifiedBy');
373 506
        $html .= self::generateMeta($properties->getViewport(), 'viewport');
374 506
        $date = Date::dateTimeFromTimestamp((string) $properties->getCreated());
375 506
        $date->setTimeZone(Date::getDefaultOrLocalTimeZone());
376 506
        $html .= self::generateMeta($date->format(DATE_W3C), 'created');
377 506
        $date = Date::dateTimeFromTimestamp((string) $properties->getModified());
378 506
        $date->setTimeZone(Date::getDefaultOrLocalTimeZone());
379 506
        $html .= self::generateMeta($date->format(DATE_W3C), 'modified');
380
381 506
        $customProperties = $properties->getCustomProperties();
382 506
        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 506
        if (!empty($properties->getHyperlinkBase())) {
401 1
            $html .= '      <base href="' . $properties->getHyperlinkBase() . '" />' . PHP_EOL;
402
        }
403
404 506
        $html .= $includeStyles ? $this->generateStyles(true) : $this->generatePageDeclarations(true);
405
406 506
        $html .= '  </head>' . PHP_EOL;
407 506
        $html .= '' . PHP_EOL;
408 506
        $html .= self::BODY_LINE;
409
410 506
        return $html;
411
    }
412
413
    /** @return Worksheet[] */
414 504
    private function generateSheetPrep(): array
415
    {
416
        // Fetch sheets
417 504
        if ($this->sheetIndex === null) {
418 13
            $sheets = $this->spreadsheet->getAllSheets();
419
        } else {
420 498
            $sheets = [$this->spreadsheet->getSheet($this->sheetIndex)];
421
        }
422
423 504
        return $sheets;
424
    }
425
426 504
    private function generateSheetStarts(Worksheet $sheet, int $rowMin): array
427
    {
428
        // calculate start of <tbody>, <thead>
429 504
        $tbodyStart = $rowMin;
430 504
        $theadStart = $theadEnd = 0; // default: no <thead>    no </thead>
431 504
        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 504
        return [$theadStart, $theadEnd, $tbodyStart];
443
    }
444
445 504
    private function generateSheetTags(int $row, int $theadStart, int $theadEnd, int $tbodyStart): array
446
    {
447
        // <thead> ?
448 504
        $startTag = ($row == $theadStart) ? ('        <thead>' . PHP_EOL) : '';
449 504
        if (!$startTag) {
450 504
            $startTag = ($row == $tbodyStart) ? ('        <tbody>' . PHP_EOL) : '';
451
        }
452 504
        $endTag = ($row == $theadEnd) ? ('        </thead>' . PHP_EOL) : '';
453 504
        $cellType = ($row >= $tbodyStart) ? 'td' : 'th';
454
455 504
        return [$cellType, $startTag, $endTag];
456
    }
457
458
    /**
459
     * Generate sheet data.
460
     */
461 504
    public function generateSheetData(): string
462
    {
463
        // Ensure that Spans have been calculated?
464 504
        $this->calculateSpans();
465 504
        $sheets = $this->generateSheetPrep();
466
467
        // Construct HTML
468 504
        $html = '';
469
470
        // Loop all sheets
471 504
        $sheetId = 0;
472 504
        foreach ($sheets as $sheet) {
473
            // Write table header
474 504
            $html .= $this->generateTableHeader($sheet);
475 504
            $this->sheetCharts = [];
476 504
            $this->sheetDrawings = [];
477
478
            // Get worksheet dimension
479 504
            [$min, $max] = explode(':', $sheet->calculateWorksheetDataDimension());
480 504
            [$minCol, $minRow, $minColString] = Coordinate::indexesFromString($min);
481 504
            [$maxCol, $maxRow] = Coordinate::indexesFromString($max);
482 504
            $this->extendRowsAndColumns($sheet, $maxCol, $maxRow);
483
484 504
            [$theadStart, $theadEnd, $tbodyStart] = $this->generateSheetStarts($sheet, $minRow);
485
486
            // Loop through cells
487 504
            $row = $minRow - 1;
488 504
            while ($row++ < $maxRow) {
489 504
                [$cellType, $startTag, $endTag] = $this->generateSheetTags($row, $theadStart, $theadEnd, $tbodyStart);
490 504
                $html .= $startTag;
491
492
                // Write row if there are HTML table cells in it
493 504
                if ($this->shouldGenerateRow($sheet, $row) && !isset($this->isSpannedRow[$sheet->getParentOrThrow()->getIndex($sheet)][$row])) {
494
                    // Start a new rowData
495 504
                    $rowData = [];
496
                    // Loop through columns
497 504
                    $column = $minCol;
498 504
                    $colStr = $minColString;
499 504
                    while ($column <= $maxCol) {
500
                        // Cell exists?
501 504
                        $cellAddress = Coordinate::stringFromColumnIndex($column) . $row;
502 504
                        if ($this->shouldGenerateColumn($sheet, $colStr)) {
503 504
                            $rowData[$column] = ($sheet->getCellCollection()->has($cellAddress)) ? $cellAddress : '';
504
                        }
505 504
                        ++$column;
506 504
                        ++$colStr;
507
                    }
508 504
                    $html .= $this->generateRow($sheet, $rowData, $row - 1, $cellType);
509
                }
510
511 504
                $html .= $endTag;
512
            }
513
514
            // Write table footer
515 504
            $html .= $this->generateTableFooter();
516
            // Writing PDF?
517 504
            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 504
            ++$sheetId;
525
        }
526
527 504
        return $html;
528
    }
529
530
    /**
531
     * Generate sheet tabs.
532
     */
533 477
    public function generateNavigation(): string
534
    {
535
        // Fetch sheets
536 477
        $sheets = [];
537 477
        if ($this->sheetIndex === null) {
538 7
            $sheets = $this->spreadsheet->getAllSheets();
539
        } else {
540 476
            $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex);
541
        }
542
543
        // Construct HTML
544 477
        $html = '';
545
546
        // Only if there are more than 1 sheets
547 477
        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 477
        return $html;
562
    }
563
564 504
    private function extendRowsAndColumns(Worksheet $worksheet, int &$colMax, int &$rowMax): void
565
    {
566 504
        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 504
        foreach ($worksheet->getDrawingCollection() as $drawing) {
582 24
            if ($drawing instanceof Drawing && $drawing->getPath() === '') {
583
                continue;
584
            }
585 24
            $imageTL = Coordinate::indexesFromString($drawing->getCoordinates());
586 24
            $this->sheetDrawings[$drawing->getCoordinates()] = $drawing;
587 24
            if ($imageTL[1] > $rowMax) {
588 3
                $rowMax = $imageTL[1];
589
            }
590 24
            if ($imageTL[0] > $colMax) {
591 1
                $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 504
    private function writeImageInCell(string $coordinates): string
618
    {
619
        // Construct HTML
620 504
        $html = '';
621
622
        // Write images
623 504
        $drawing = $this->sheetDrawings[$coordinates] ?? null;
624 504
        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 504
        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 499
    public function generateStyles(bool $generateSurroundingHTML = true): string
794
    {
795
        // Build CSS
796 499
        $css = $this->buildCSS($generateSurroundingHTML);
797
798
        // Construct HTML
799 499
        $html = '';
800
801
        // Start styles
802 499
        if ($generateSurroundingHTML) {
803 499
            $html .= '    <style type="text/css">' . PHP_EOL;
804 499
            $html .= (array_key_exists('html', $css)) ? ('      html { ' . $this->assembleCSS($css['html']) . ' }' . PHP_EOL) : '';
805
        }
806
807
        // Write all other styles
808 499
        foreach ($css as $styleName => $styleDefinition) {
809 499
            if ($styleName != 'html') {
810 499
                $html .= '      ' . $styleName . ' { ' . $this->assembleCSS($styleDefinition) . ' }' . PHP_EOL;
811
            }
812
        }
813 499
        $html .= $this->generatePageDeclarations(false);
814
815
        // End styles
816 499
        if ($generateSurroundingHTML) {
817 499
            $html .= '    </style>' . PHP_EOL;
818
        }
819
820
        // Return
821 499
        return $html;
822
    }
823
824 506
    private function buildCssRowHeights(Worksheet $sheet, array &$css, int $sheetIndex): void
825
    {
826
        // Calculate row heights
827 506
        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 506
    private function buildCssPerSheet(Worksheet $sheet, array &$css): void
845
    {
846
        // Calculate hash code
847 506
        $sheetIndex = $sheet->getParentOrThrow()->getIndex($sheet);
848 506
        $setup = $sheet->getPageSetup();
849 506
        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 506
        $picture = $sheet->getBackgroundImage();
854 506
        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 506
        $sheet->calculateColumnWidths();
862
863
        // col elements, initialize
864 506
        $highestColumnIndex = Coordinate::columnIndexFromString($sheet->getHighestColumn()) - 1;
865 506
        $column = -1;
866 506
        $colStr = 'A';
867 506
        while ($column++ < $highestColumnIndex) {
868 506
            $this->columnWidths[$sheetIndex][$column] = self::DEFAULT_CELL_WIDTH_POINTS; // approximation
869 506
            if ($this->shouldGenerateColumn($sheet, $colStr)) {
870 506
                $css['table.sheet' . $sheetIndex . ' col.col' . $column]['width'] = self::DEFAULT_CELL_WIDTH_POINTS . 'pt';
871
            }
872 506
            ++$colStr;
873
        }
874
875
        // col elements, loop through columnDimensions and set width
876 506
        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 506
        $rowDimension = $sheet->getDefaultRowDimension();
894
895
        // table.sheetN tr { }
896 506
        $css['table.sheet' . $sheetIndex . ' tr'] = [];
897
898 506
        if ($rowDimension->getRowHeight() == -1) {
899 499
            $pt_height = SharedFont::getDefaultRowHeightByFont($this->spreadsheet->getDefaultStyle()->getFont());
900
        } else {
901 7
            $pt_height = $rowDimension->getRowHeight();
902
        }
903 506
        $css['table.sheet' . $sheetIndex . ' tr']['height'] = $pt_height . 'pt';
904 506
        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 506
        $this->buildCssRowHeights($sheet, $css, $sheetIndex);
910
    }
911
912
    /**
913
     * Build CSS styles.
914
     *
915
     * @param bool $generateSurroundingHTML Generate surrounding HTML style? (html { })
916
     */
917 506
    public function buildCSS(bool $generateSurroundingHTML = true): array
918
    {
919
        // Cached?
920 506
        if ($this->cssStyles !== null) {
921 497
            return $this->cssStyles;
922
        }
923
924
        // Ensure that spans have been calculated
925 506
        $this->calculateSpans();
926
927
        // Construct CSS
928 506
        $css = [];
929
930
        // Start styles
931 506
        if ($generateSurroundingHTML) {
932
            // html { }
933 498
            $css['html']['font-family'] = 'Calibri, Arial, Helvetica, sans-serif';
934 498
            $css['html']['font-size'] = '11pt';
935 498
            $css['html']['background-color'] = 'white';
936
        }
937
938
        // CSS for comments as found in LibreOffice
939 506
        $css['a.comment-indicator:hover + div.comment'] = [
940 506
            'background' => '#ffd',
941 506
            'position' => 'absolute',
942 506
            'display' => 'block',
943 506
            'border' => '1px solid black',
944 506
            'padding' => '0.5em',
945 506
        ];
946
947 506
        $css['a.comment-indicator'] = [
948 506
            'background' => 'red',
949 506
            'display' => 'inline-block',
950 506
            'border' => '1px solid black',
951 506
            'width' => '0.5em',
952 506
            'height' => '0.5em',
953 506
        ];
954
955 506
        $css['div.comment']['display'] = 'none';
956
957
        // table { }
958 506
        $css['table']['border-collapse'] = 'collapse';
959
960
        // .b {}
961 506
        $css['.b']['text-align'] = 'center'; // BOOL
962
963
        // .e {}
964 506
        $css['.e']['text-align'] = 'center'; // ERROR
965
966
        // .f {}
967 506
        $css['.f']['text-align'] = 'right'; // FORMULA
968
969
        // .inlineStr {}
970 506
        $css['.inlineStr']['text-align'] = 'left'; // INLINE
971
972
        // .n {}
973 506
        $css['.n']['text-align'] = 'right'; // NUMERIC
974
975
        // .s {}
976 506
        $css['.s']['text-align'] = 'left'; // STRING
977
978
        // Calculate cell style hashes
979 506
        foreach ($this->spreadsheet->getCellXfCollection() as $index => $style) {
980 506
            $css['td.style' . $index . ', th.style' . $index] = $this->createCSSStyle($style);
981
            //$css['th.style' . $index] = $this->createCSSStyle($style);
982
        }
983
984
        // Fetch sheets
985 506
        $sheets = [];
986 506
        if ($this->sheetIndex === null) {
987 14
            $sheets = $this->spreadsheet->getAllSheets();
988
        } else {
989 499
            $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex);
990
        }
991
992
        // Build styles per sheet
993 506
        foreach ($sheets as $sheet) {
994 506
            $this->buildCssPerSheet($sheet, $css);
995
        }
996
997
        // Cache
998 506
        if ($this->cssStyles === null) {
999 506
            $this->cssStyles = $css;
1000
        }
1001
1002
        // Return
1003 506
        return $css;
1004
    }
1005
1006
    /**
1007
     * Create CSS style.
1008
     */
1009 506
    private function createCSSStyle(Style $style): array
1010
    {
1011
        // Create CSS
1012 506
        return array_merge(
1013 506
            $this->createCSSStyleAlignment($style->getAlignment()),
1014 506
            $this->createCSSStyleBorders($style->getBorders()),
1015 506
            $this->createCSSStyleFont($style->getFont()),
1016 506
            $this->createCSSStyleFill($style->getFill())
1017 506
        );
1018
    }
1019
1020
    /**
1021
     * Create CSS style.
1022
     */
1023 506
    private function createCSSStyleAlignment(Alignment $alignment): array
1024
    {
1025
        // Construct CSS
1026 506
        $css = [];
1027
1028
        // Create CSS
1029 506
        $verticalAlign = $this->mapVAlign($alignment->getVertical() ?? '');
1030 506
        if ($verticalAlign) {
1031 506
            $css['vertical-align'] = $verticalAlign;
1032
        }
1033 506
        $textAlign = $this->mapHAlign($alignment->getHorizontal() ?? '');
1034 506
        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 506
        $rotation = $alignment->getTextRotation();
1041 506
        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 506
        return $css;
1050
    }
1051
1052
    /**
1053
     * Create CSS style.
1054
     */
1055 506
    private function createCSSStyleFont(Font $font): array
1056
    {
1057
        // Construct CSS
1058 506
        $css = [];
1059
1060
        // Create CSS
1061 506
        if ($font->getBold()) {
1062 15
            $css['font-weight'] = 'bold';
1063
        }
1064 506
        if ($font->getUnderline() != Font::UNDERLINE_NONE && $font->getStrikethrough()) {
1065 1
            $css['text-decoration'] = 'underline line-through';
1066 506
        } elseif ($font->getUnderline() != Font::UNDERLINE_NONE) {
1067 10
            $css['text-decoration'] = 'underline';
1068 506
        } elseif ($font->getStrikethrough()) {
1069 1
            $css['text-decoration'] = 'line-through';
1070
        }
1071 506
        if ($font->getItalic()) {
1072 9
            $css['font-style'] = 'italic';
1073
        }
1074
1075 506
        $css['color'] = '#' . $font->getColor()->getRGB();
1076 506
        $css['font-family'] = '\'' . htmlspecialchars((string) $font->getName(), ENT_QUOTES) . '\'';
1077 506
        $css['font-size'] = $font->getSize() . 'pt';
1078
1079 506
        return $css;
1080
    }
1081
1082
    /**
1083
     * Create CSS style.
1084
     *
1085
     * @param Borders $borders Borders
1086
     */
1087 506
    private function createCSSStyleBorders(Borders $borders): array
1088
    {
1089
        // Construct CSS
1090 506
        $css = [];
1091
1092
        // Create CSS
1093 506
        if (!($this instanceof Pdf\Mpdf)) {
1094 491
            $css['border-bottom'] = $this->createCSSStyleBorder($borders->getBottom());
1095 491
            $css['border-top'] = $this->createCSSStyleBorder($borders->getTop());
1096 491
            $css['border-left'] = $this->createCSSStyleBorder($borders->getLeft());
1097 491
            $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 506
        return $css;
1115
    }
1116
1117
    /**
1118
     * Create CSS style.
1119
     *
1120
     * @param Border $border Border
1121
     */
1122 495
    private function createCSSStyleBorder(Border $border): string
1123
    {
1124
        //    Create CSS - add !important to non-none border styles for merged cells
1125 495
        $borderStyle = $this->mapBorderStyle($border->getBorderStyle());
1126
1127 495
        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 506
    private function createCSSStyleFill(Fill $fill): array
1136
    {
1137
        // Construct HTML
1138 506
        $css = [];
1139
1140
        // Create CSS
1141 506
        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 506
        return $css;
1148
    }
1149
1150
    /**
1151
     * Generate HTML footer.
1152
     */
1153 504
    public function generateHTMLFooter(): string
1154
    {
1155
        // Construct HTML
1156 504
        $html = '';
1157 504
        $html .= '  </body>' . PHP_EOL;
1158 504
        $html .= '</html>' . PHP_EOL;
1159
1160 504
        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 504
    private function generateTableTag(Worksheet $worksheet, string $id, string &$html, int $sheetIndex): void
1184
    {
1185 504
        if (!$this->useInlineCss) {
1186 497
            $gridlines = $worksheet->getShowGridlines() ? ' gridlines' : '';
1187 497
            $gridlinesp = $worksheet->getPrintGridlines() ? ' gridlinesp' : '';
1188 497
            $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 504
    private function generateTableHeader(Worksheet $worksheet, bool $showid = true): string
1201
    {
1202 504
        $sheetIndex = $worksheet->getParentOrThrow()->getIndex($worksheet);
1203
1204
        // Construct HTML
1205 504
        $html = '';
1206 504
        $id = $showid ? "id='sheet$sheetIndex'" : '';
1207 504
        if ($showid) {
1208 504
            $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 504
        $this->generateTableTag($worksheet, $id, $html, $sheetIndex);
1214
1215
        // Write <col> elements
1216 504
        $highestColumnIndex = Coordinate::columnIndexFromString($worksheet->getHighestColumn()) - 1;
1217 504
        $i = -1;
1218 504
        while ($i++ < $highestColumnIndex) {
1219 504
            if (!$this->useInlineCss) {
1220 497
                $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 504
        return $html;
1229
    }
1230
1231
    /**
1232
     * Generate table footer.
1233
     */
1234 504
    private function generateTableFooter(): string
1235
    {
1236 504
        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 504
    private function generateRowStart(Worksheet $worksheet, int $sheetIndex, int $row): string
1246
    {
1247 504
        $html = '';
1248 504
        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 504
        if (!$this->useInlineCss) {
1267 497
            $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 504
        return $html;
1276
    }
1277
1278 504
    private function generateRowCellCss(Worksheet $worksheet, string $cellAddress, int $row, int $columnNumber): array
1279
    {
1280 504
        $cell = ($cellAddress > '') ? $worksheet->getCellCollection()->get($cellAddress) : '';
1281 504
        $coordinate = Coordinate::stringFromColumnIndex($columnNumber + 1) . ($row + 1);
1282 504
        if (!$this->useInlineCss) {
1283 497
            $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 504
        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 502
    private function generateRowCellDataValue(Worksheet $worksheet, Cell $cell, string &$cellData): void
1345
    {
1346 502
        if ($cell->getValue() instanceof RichText) {
1347 10
            $cellData .= $this->generateRowCellDataValueRich($cell->getValue());
1348
        } else {
1349 502
            $origData = $this->preCalculateFormulas ? $cell->getCalculatedValue() : $cell->getValue();
1350 502
            $origData2 = $this->preCalculateFormulas ? $cell->getCalculatedValueString() : $cell->getValueString();
1351 502
            $formatCode = $worksheet->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())->getNumberFormat()->getFormatCode();
1352
1353 502
            $cellData = NumberFormat::toFormattedString(
1354 502
                $origData2,
1355 502
                $formatCode ?? NumberFormat::FORMAT_GENERAL,
1356 502
                [$this, 'formatColor']
1357 502
            );
1358
1359 502
            if ($cellData === $origData) {
1360 88
                $cellData = htmlspecialchars($cellData, Settings::htmlEntityFlags());
1361
            }
1362 502
            if ($worksheet->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())->getFont()->getSuperscript()) {
1363 1
                $cellData = '<sup>' . $cellData . '</sup>';
1364 502
            } elseif ($worksheet->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())->getFont()->getSubscript()) {
1365 1
                $cellData = '<sub>' . $cellData . '</sub>';
1366
            }
1367
        }
1368
    }
1369
1370 504
    private function generateRowCellData(Worksheet $worksheet, null|Cell|string $cell, array|string &$cssClass): string
1371
    {
1372 504
        $cellData = '&nbsp;';
1373 504
        if ($cell instanceof Cell) {
1374 502
            $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 502
            $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 502
            $cellData = (string) preg_replace('/(?m)(?:^|\\G) /', '&nbsp;', $cellData);
1385
1386
            // convert newline "\n" to '<br>'
1387 502
            $cellData = nl2br($cellData);
1388
1389
            // Extend CSS class?
1390 502
            if (!$this->useInlineCss && is_string($cssClass)) {
1391 496
                $cssClass .= ' style' . $cell->getXfIndex();
1392 496
                $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 504
        return $cellData;
1417
    }
1418
1419 504
    private function generateRowIncludeCharts(Worksheet $worksheet, string $coordinate): string
1420
    {
1421 504
        return $this->includeCharts ? $this->writeChartInCell($worksheet, $coordinate) : '';
1422
    }
1423
1424 504
    private function generateRowSpans(string $html, int $rowSpan, int $colSpan): string
1425
    {
1426 504
        $html .= ($colSpan > 1) ? (' colspan="' . $colSpan . '"') : '';
1427 504
        $html .= ($rowSpan > 1) ? (' rowspan="' . $rowSpan . '"') : '';
1428
1429 504
        return $html;
1430
    }
1431
1432 504
    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 504
        $htmlx = $this->writeImageInCell($coordinate);
1447
        // Chart?
1448 504
        $htmlx .= $this->generateRowIncludeCharts($worksheet, $coordinate);
1449
        // Column start
1450 504
        $html .= '            <' . $cellType;
1451 504
        if (!$this->useInlineCss && !$this->isPdf && is_string($cssClass)) {
1452 475
            $html .= ' class="' . $cssClass . '"';
1453 475
            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 504
        $html = $this->generateRowSpans($html, $rowSpan, $colSpan);
1501
1502 504
        $html .= '>';
1503 504
        $html .= $htmlx;
1504
1505 504
        $html .= $this->writeComment($worksheet, $coordinate);
1506
1507
        // Cell data
1508 504
        $html .= $cellData;
1509
1510
        // Column end
1511 504
        $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 504
    private function generateRow(Worksheet $worksheet, array $values, int $row, string $cellType): string
1522
    {
1523
        // Sheet index
1524 504
        $sheetIndex = $worksheet->getParentOrThrow()->getIndex($worksheet);
1525 504
        $html = $this->generateRowStart($worksheet, $sheetIndex, $row);
1526
1527
        // Write cells
1528 504
        $colNum = 0;
1529 504
        foreach ($values as $cellAddress) {
1530 504
            [$cell, $cssClass, $coordinate] = $this->generateRowCellCss($worksheet, $cellAddress, $row, $colNum);
1531
1532
            // Cell Data
1533 504
            $cellData = $this->generateRowCellData($worksheet, $cell, $cssClass);
1534
1535
            // Hyperlink?
1536 504
            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 504
            $writeCell = !(isset($this->isSpannedCell[$worksheet->getParentOrThrow()->getIndex($worksheet)][$row + 1][$colNum])
1549 504
                && $this->isSpannedCell[$worksheet->getParentOrThrow()->getIndex($worksheet)][$row + 1][$colNum]);
1550
1551
            // Colspan and Rowspan
1552 504
            $colSpan = 1;
1553 504
            $rowSpan = 1;
1554 504
            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 504
            if ($writeCell) {
1577 504
                $this->generateRowWriteCell($html, $worksheet, $coordinate, $cellType, $cellData, $colSpan, $rowSpan, $cssClass, $colNum, $sheetIndex, $row);
1578
            }
1579
1580
            // Next column
1581 504
            ++$colNum;
1582
        }
1583
1584
        // Write row end
1585 504
        $html .= '          </tr>' . PHP_EOL;
1586
1587
        // Return
1588 504
        return $html;
1589
    }
1590
1591
    /**
1592
     * Takes array where of CSS properties / values and converts to CSS string.
1593
     */
1594 506
    private function assembleCSS(array $values = []): string
1595
    {
1596 506
        $pairs = [];
1597 506
        foreach ($values as $property => $value) {
1598 506
            $pairs[] = $property . ':' . $value;
1599
        }
1600 506
        $string = implode('; ', $pairs);
1601
1602 506
        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 506
    private function calculateSpans(): void
1698
    {
1699 506
        if ($this->spansAreCalculated) {
1700 506
            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 506
        $sheetIndexes = $this->sheetIndex !== null
1706 506
            ? [$this->sheetIndex] : range(0, $this->spreadsheet->getSheetCount() - 1);
1707
1708 506
        foreach ($sheetIndexes as $sheetIndex) {
1709 506
            $sheet = $this->spreadsheet->getSheet($sheetIndex);
1710
1711 506
            $candidateSpannedRow = [];
1712
1713
            // loop through all Excel merged cells
1714 506
            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 506
            $this->calculateSpansOmitRows($sheet, $sheetIndex, $candidateSpannedRow);
1752
1753
            // TODO: Same for columns
1754
        }
1755
1756
        // We have calculated the spans
1757 506
        $this->spansAreCalculated = true;
1758
    }
1759
1760 506
    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 506
        $countColumns = Coordinate::columnIndexFromString($sheet->getHighestColumn());
1765 506
        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 506
        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 504
    private function writeComment(Worksheet $worksheet, string $coordinate): string
1798
    {
1799 504
        $result = '';
1800 504
        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 504
        return $result;
1816
    }
1817
1818 479
    public function getOrientation(): ?string
1819
    {
1820
        // Expect Pdf classes to override this method.
1821 479
        return $this->isPdf ? PageSetup::ORIENTATION_PORTRAIT : null;
1822
    }
1823
1824
    /**
1825
     * Generate @page declarations.
1826
     */
1827 506
    private function generatePageDeclarations(bool $generateSurroundingHTML): string
1828
    {
1829
        // Ensure that Spans have been calculated?
1830 506
        $this->calculateSpans();
1831
1832
        // Fetch sheets
1833 506
        $sheets = [];
1834 506
        if ($this->sheetIndex === null) {
1835 14
            $sheets = $this->spreadsheet->getAllSheets();
1836
        } else {
1837 499
            $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex);
1838
        }
1839
1840
        // Construct HTML
1841 506
        $htmlPage = $generateSurroundingHTML ? ('<style type="text/css">' . PHP_EOL) : '';
1842
1843
        // Loop all sheets
1844 506
        $sheetId = 0;
1845 506
        foreach ($sheets as $worksheet) {
1846 506
            $htmlPage .= "@page page$sheetId { ";
1847 506
            $left = StringHelper::formatNumber($worksheet->getPageMargins()->getLeft()) . 'in; ';
1848 506
            $htmlPage .= 'margin-left: ' . $left;
1849 506
            $right = StringHelper::FormatNumber($worksheet->getPageMargins()->getRight()) . 'in; ';
1850 506
            $htmlPage .= 'margin-right: ' . $right;
1851 506
            $top = StringHelper::FormatNumber($worksheet->getPageMargins()->getTop()) . 'in; ';
1852 506
            $htmlPage .= 'margin-top: ' . $top;
1853 506
            $bottom = StringHelper::FormatNumber($worksheet->getPageMargins()->getBottom()) . 'in; ';
1854 506
            $htmlPage .= 'margin-bottom: ' . $bottom;
1855 506
            $orientation = $this->getOrientation() ?? $worksheet->getPageSetup()->getOrientation();
1856 506
            if ($orientation === PageSetup::ORIENTATION_LANDSCAPE) {
1857 9
                $htmlPage .= 'size: landscape; ';
1858 498
            } elseif ($orientation === PageSetup::ORIENTATION_PORTRAIT) {
1859 12
                $htmlPage .= 'size: portrait; ';
1860
            }
1861 506
            $htmlPage .= '}' . PHP_EOL;
1862 506
            ++$sheetId;
1863
        }
1864 506
        $htmlPage .= implode(PHP_EOL, [
1865 506
            '.navigation {page-break-after: always;}',
1866 506
            '.scrpgbrk, div + div {page-break-before: always;}',
1867 506
            '@media screen {',
1868 506
            '  .gridlines td {border: 1px solid black;}',
1869 506
            '  .gridlines th {border: 1px solid black;}',
1870 506
            '  body>div {margin-top: 5px;}',
1871 506
            '  body>div:first-child {margin-top: 0;}',
1872 506
            '  .scrpgbrk {margin-top: 1px;}',
1873 506
            '}',
1874 506
            '@media print {',
1875 506
            '  .gridlinesp td {border: 1px solid black;}',
1876 506
            '  .gridlinesp th {border: 1px solid black;}',
1877 506
            '  .navigation {display: none;}',
1878 506
            '}',
1879 506
            '',
1880 506
        ]);
1881 506
        $htmlPage .= $generateSurroundingHTML ? ('</style>' . PHP_EOL) : '';
1882
1883 506
        return $htmlPage;
1884
    }
1885
1886 504
    private function shouldGenerateRow(Worksheet $sheet, int $row): bool
1887
    {
1888 504
        if (!($this instanceof Pdf\Mpdf || $this instanceof Pdf\Tcpdf)) {
1889 484
            return true;
1890
        }
1891
1892 26
        return $sheet->isRowVisible($row);
1893
    }
1894
1895 506
    private function shouldGenerateColumn(Worksheet $sheet, string $colStr): bool
1896
    {
1897 506
        if (!($this instanceof Pdf\Mpdf || $this instanceof Pdf\Tcpdf)) {
1898 486
            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