Failed Conditions
Pull Request — master (#4257)
by Owen
12:03
created

Html::generateRowWriteCell()   F

Complexity

Conditions 22
Paths 330

Size

Total Lines 90
Code Lines 47

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 44
CRAP Score 22

Importance

Changes 0
Metric Value
eloc 47
dl 0
loc 90
rs 1.7083
c 0
b 0
f 0
ccs 44
cts 44
cp 1
cc 22
nc 330
nop 11
crap 22

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