Passed
Pull Request — master (#4142)
by Owen
11:33
created

Html::generateRowWriteCell()   C

Complexity

Conditions 16
Paths 66

Size

Total Lines 80
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 37
CRAP Score 16

Importance

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