Failed Conditions
Pull Request — master (#4257)
by Owen
13:57
created

Html::generateRowWriteCell()   D

Complexity

Conditions 18
Paths 198

Size

Total Lines 87
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 44
CRAP Score 18

Importance

Changes 0
Metric Value
eloc 45
c 0
b 0
f 0
dl 0
loc 87
ccs 44
cts 44
cp 1
rs 4.05
cc 18
nc 198
nop 11
crap 18

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