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

Html::generateRowWriteCell()   F

Complexity

Conditions 22
Paths 330

Size

Total Lines 90
Code Lines 47

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 46
CRAP Score 22

Importance

Changes 0
Metric Value
eloc 47
c 0
b 0
f 0
dl 0
loc 90
rs 1.7083
ccs 46
cts 46
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
            $value = $fill->getFillType() == Fill::FILL_NONE
1153 514
                ? 'white' : '#' . $fill->getStartColor()->getRGB();
1154
            $css['background-color'] = $value;
1155
        }
1156 514
1157 514
        return $css;
1158 514
    }
1159
1160 514
    /**
1161
     * Generate HTML footer.
1162
     */
1163 12
    public function generateHTMLFooter(): string
1164
    {
1165 12
        // Construct HTML
1166 12
        $html = '';
1167
        $html .= '  </body>' . PHP_EOL;
1168 12
        $html .= '</html>' . PHP_EOL;
1169 12
1170 12
        return $html;
1171 1
    }
1172 12
1173 6
    private function generateTableTagInline(Worksheet $worksheet, string $id): string
1174 7
    {
1175 1
        $style = isset($this->cssStyles['table'])
1176
            ? $this->assembleCSS($this->cssStyles['table']) : '';
1177 7
1178
        $prntgrid = $worksheet->getPrintGridlines();
1179
        $viewgrid = $this->isPdf ? $prntgrid : $worksheet->getShowGridlines();
1180 12
        if ($viewgrid && $prntgrid) {
1181
            $html = "    <table border='1' cellpadding='1' $id cellspacing='1' style='$style' class='gridlines gridlinesp'>" . PHP_EOL;
1182
        } elseif ($viewgrid) {
1183 514
            $html = "    <table border='0' cellpadding='0' $id cellspacing='0' style='$style' class='gridlines'>" . PHP_EOL;
1184
        } elseif ($prntgrid) {
1185 514
            $html = "    <table border='0' cellpadding='0' $id cellspacing='0' style='$style' class='gridlinesp'>" . PHP_EOL;
1186 507
        } else {
1187 507
            $html = "    <table border='0' cellpadding='1' $id cellspacing='0' style='$style'>" . PHP_EOL;
1188 507
        }
1189
1190 12
        return $html;
1191
    }
1192
1193
    private function generateTableTag(Worksheet $worksheet, string $id, string &$html, int $sheetIndex): void
1194
    {
1195
        if (!$this->useInlineCss) {
1196
            $gridlines = $worksheet->getShowGridlines() ? ' gridlines' : '';
1197
            $gridlinesp = $worksheet->getPrintGridlines() ? ' gridlinesp' : '';
1198
            $html .= "    <table border='0' cellpadding='0' cellspacing='0' $id class='sheet$sheetIndex$gridlines$gridlinesp'>" . PHP_EOL;
1199
        } else {
1200 514
            $html .= $this->generateTableTagInline($worksheet, $id);
1201
        }
1202 514
    }
1203
1204
    /**
1205 514
     * Generate table header.
1206 514
     *
1207 514
     * @param Worksheet $worksheet The worksheet for the table we are writing
1208 514
     * @param bool $showid whether or not to add id to table tag
1209
     */
1210 2
    private function generateTableHeader(Worksheet $worksheet, bool $showid = true): string
1211
    {
1212
        $sheetIndex = $worksheet->getParentOrThrow()->getIndex($worksheet);
1213 514
1214
        // Construct HTML
1215
        $html = '';
1216 514
        $id = $showid ? "id='sheet$sheetIndex'" : '';
1217 514
        if ($showid) {
1218 514
            $html .= "<div style='page: page$sheetIndex'>" . PHP_EOL;
1219 514
        } else {
1220 507
            $html .= "<div style='page: page$sheetIndex' class='scrpgbrk'>" . PHP_EOL;
1221
        }
1222 12
1223 12
        $this->generateTableTag($worksheet, $id, $html, $sheetIndex);
1224 12
1225
        // Write <col> elements
1226
        $highestColumnIndex = Coordinate::columnIndexFromString($worksheet->getHighestColumn()) - 1;
1227
        $i = -1;
1228 514
        while ($i++ < $highestColumnIndex) {
1229
            if (!$this->useInlineCss) {
1230
                $html .= '        <col class="col' . $i . '" />' . PHP_EOL;
1231
            } else {
1232
                $style = isset($this->cssStyles['table.sheet' . $sheetIndex . ' col.col' . $i])
1233
                    ? $this->assembleCSS($this->cssStyles['table.sheet' . $sheetIndex . ' col.col' . $i]) : '';
1234 514
                $html .= '        <col style="' . $style . '" />' . PHP_EOL;
1235
            }
1236 514
        }
1237
1238
        return $html;
1239
    }
1240
1241
    /**
1242
     * Generate table footer.
1243
     */
1244
    private function generateTableFooter(): string
1245 514
    {
1246
        return '    </tbody></table>' . PHP_EOL . '</div>' . PHP_EOL;
1247 514
    }
1248 514
1249 2
    /**
1250
     * Generate row start.
1251
     *
1252 2
     * @param int $sheetIndex Sheet index (0-based)
1253
     * @param int $row row number
1254 2
     */
1255 2
    private function generateRowStart(Worksheet $worksheet, int $sheetIndex, int $row): string
1256 1
    {
1257
        $html = '';
1258
        if (count($worksheet->getBreaks()) > 0) {
1259
            $breaks = $worksheet->getRowBreaks();
1260 2
1261 2
            // check if a break is needed before this row
1262
            if (isset($breaks['A' . $row])) {
1263
                // close table: </table>
1264
                $html .= $this->generateTableFooter();
1265
                if ($this->isPdf && $this->useInlineCss) {
1266 514
                    $html .= '<div style="page-break-before:always" />';
1267 507
                }
1268
1269 12
                // open table again: <table> + <col> etc.
1270 12
                $html .= $this->generateTableHeader($worksheet, false);
1271
                $html .= '<tbody>' . PHP_EOL;
1272 12
            }
1273
        }
1274
1275 514
        // Write row start
1276
        if (!$this->useInlineCss) {
1277
            $html .= '          <tr class="row' . $row . '">' . PHP_EOL;
1278 514
        } else {
1279
            $style = isset($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $row])
1280 514
                ? $this->assembleCSS($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $row]) : '';
1281 514
1282 514
            $html .= '          <tr style="' . $style . '">' . PHP_EOL;
1283 507
        }
1284
1285 12
        return $html;
1286
    }
1287
1288
    private function generateRowCellCss(Worksheet $worksheet, string $cellAddress, int $row, int $columnNumber): array
1289
    {
1290
        $cell = ($cellAddress > '') ? $worksheet->getCellCollection()->get($cellAddress) : '';
1291
        $coordinate = Coordinate::stringFromColumnIndex($columnNumber + 1) . ($row + 1);
1292
        if (!$this->useInlineCss) {
1293
            $cssClass = 'column' . $columnNumber;
1294
        } else {
1295
            $cssClass = [];
1296
            // The statements below do nothing.
1297
            // Commenting out the code rather than deleting it
1298
            // in case someone can figure out what their intent was.
1299
            //if ($cellType == 'th') {
1300
            //    if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' th.column' . $colNum])) {
1301 514
            //        $this->cssStyles['table.sheet' . $sheetIndex . ' th.column' . $colNum];
1302
            //    }
1303
            //} else {
1304 30
            //    if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' td.column' . $colNum])) {
1305
            //        $this->cssStyles['table.sheet' . $sheetIndex . ' td.column' . $colNum];
1306 30
            //    }
1307
            //}
1308 30
            // End of mystery statements.
1309 30
        }
1310
1311 30
        return [$cell, $cssClass, $coordinate];
1312 14
    }
1313 14
1314 14
    private function generateRowCellDataValueRich(RichText $richText): string
1315
    {
1316 14
        $cellData = '';
1317 1
        // Loop through rich text elements
1318 1
        $elements = $richText->getRichTextElements();
1319 14
        foreach ($elements as $element) {
1320 1
            // Rich text start?
1321 1
            if ($element instanceof Run) {
1322
                $cellEnd = '';
1323
                if ($element->getFont() !== null) {
1324
                    $cellData .= '<span style="' . $this->assembleCSS($this->createCSSStyleFont($element->getFont())) . '">';
1325
1326
                    if ($element->getFont()->getSuperscript()) {
1327
                        $cellData .= '<sup>';
1328 14
                        $cellEnd = '</sup>';
1329 14
                    } elseif ($element->getFont()->getSubscript()) {
1330
                        $cellData .= '<sub>';
1331 14
                        $cellEnd = '</sub>';
1332
                    }
1333 14
                } else {
1334
                    $cellData .= '<span>';
1335
                }
1336 26
1337 26
                // Convert UTF8 data to PCDATA
1338
                $cellText = $element->getText();
1339
                $cellData .= htmlspecialchars($cellText, Settings::htmlEntityFlags());
1340
1341 30
                $cellData .= $cellEnd;
1342
1343
                $cellData .= '</span>';
1344 512
            } else {
1345
                // Convert UTF8 data to PCDATA
1346 512
                $cellText = $element->getText();
1347 10
                $cellData .= htmlspecialchars($cellText, Settings::htmlEntityFlags());
1348
            }
1349 512
        }
1350 512
1351 512
        return nl2br($cellData);
1352
    }
1353 512
1354 512
    private function generateRowCellDataValue(Worksheet $worksheet, Cell $cell, string &$cellData): void
1355 512
    {
1356 512
        if ($cell->getValue() instanceof RichText) {
1357 512
            $cellData .= $this->generateRowCellDataValueRich($cell->getValue());
1358
        } else {
1359 512
            if ($this->preCalculateFormulas) {
1360 93
                $origData = $cell->getCalculatedValue();
1361
                if ($this->betterBoolean && is_bool($origData)) {
1362 512
                    $origData2 = $origData ? $this->getTrue : $this->getFalse;
1363 1
                } else {
1364 512
                    $origData2 = $cell->getCalculatedValueString();
1365 1
                }
1366
            } else {
1367
                $origData = $cell->getValue();
1368
                if ($this->betterBoolean && is_bool($origData)) {
1369
                    $origData2 = $origData ? $this->getTrue : $this->getFalse;
1370 514
                } else {
1371
                    $origData2 = $cell->getValueString();
1372 514
                }
1373 514
            }
1374 512
            $formatCode = $worksheet->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())->getNumberFormat()->getFormatCode();
1375
1376
            $cellData = NumberFormat::toFormattedString(
1377
                $origData2,
1378
                $formatCode ?? NumberFormat::FORMAT_GENERAL,
1379
                [$this, 'formatColor']
1380 512
            );
1381
1382
            if ($cellData === $origData) {
1383
                $cellData = htmlspecialchars($cellData, Settings::htmlEntityFlags());
1384 512
            }
1385
            if ($worksheet->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())->getFont()->getSuperscript()) {
1386
                $cellData = '<sup>' . $cellData . '</sup>';
1387 512
            } elseif ($worksheet->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())->getFont()->getSubscript()) {
1388
                $cellData = '<sub>' . $cellData . '</sub>';
1389
            }
1390 512
        }
1391 506
    }
1392 506
1393 11
    private function generateRowCellData(Worksheet $worksheet, null|Cell|string $cell, array|string &$cssClass): string
1394 11
    {
1395 11
        $cellData = '&nbsp;';
1396 11
        if ($cell instanceof Cell) {
1397 11
            $cellData = '';
1398
            // Don't know what this does, and no test cases.
1399
            //if ($cell->getParent() === null) {
1400
            //    $cell->attach($worksheet);
1401 11
            //}
1402
            // Value
1403 11
            $this->generateRowCellDataValue($worksheet, $cell, $cellData);
1404 11
1405
            // Converts the cell content so that spaces occuring at beginning of each new line are replaced by &nbsp;
1406 11
            // Example: "  Hello\n to the world" is converted to "&nbsp;&nbsp;Hello\n&nbsp;to the world"
1407
            $cellData = (string) preg_replace('/(?m)(?:^|\\G) /', '&nbsp;', $cellData);
1408
1409
            // convert newline "\n" to '<br>'
1410
            $cellData = nl2br($cellData);
1411 47
1412 43
            // Extend CSS class?
1413
            if (!$this->useInlineCss && is_string($cssClass)) {
1414
                $dataType = $cell->getDataType();
1415
                if ($this->betterBoolean && $this->preCalculateFormulas && $dataType === DataType::TYPE_FORMULA) {
1416 514
                    $calculatedValue = $cell->getCalculatedValue();
1417
                    if (is_bool($calculatedValue)) {
1418
                        $dataType = DataType::TYPE_BOOL;
1419 514
                    } elseif (is_numeric($calculatedValue)) {
1420
                        $dataType = DataType::TYPE_NUMERIC;
1421 514
                    } elseif (is_string($calculatedValue)) {
1422
                        $dataType = DataType::TYPE_STRING;
1423
                    }
1424 514
                }
1425
                $cssClass .= ' style' . $cell->getXfIndex();
1426 514
                $cssClass .= ' ' . $dataType;
1427 514
            } elseif (is_array($cssClass)) {
1428
                $index = $cell->getXfIndex();
1429 514
                $styleIndex = 'td.style' . $index . ', th.style' . $index;
1430
                if (isset($this->cssStyles[$styleIndex])) {
1431
                    $cssClass = array_merge($cssClass, $this->cssStyles[$styleIndex]);
1432 514
                }
1433
1434
                // General horizontal alignment: Actual horizontal alignment depends on dataType
1435
                $sharedStyle = $worksheet->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex());
1436
                if (
1437
                    $sharedStyle->getAlignment()->getHorizontal() == Alignment::HORIZONTAL_GENERAL
1438
                    && isset($this->cssStyles['.' . $cell->getDataType()]['text-align'])
1439
                ) {
1440
                    $cssClass['text-align'] = $this->cssStyles['.' . $cell->getDataType()]['text-align'];
1441
                }
1442
            }
1443
        } else {
1444
            // Use default borders for empty cell
1445
            if (is_string($cssClass)) {
1446 514
                $cssClass .= ' style0';
1447
            }
1448 514
        }
1449
1450 514
        return $cellData;
1451 514
    }
1452 485
1453 485
    private function generateRowIncludeCharts(Worksheet $worksheet, string $coordinate): string
1454 22
    {
1455
        return $this->includeCharts ? $this->writeChartInCell($worksheet, $coordinate) : '';
1456
    }
1457
1458
    private function generateRowSpans(string $html, int $rowSpan, int $colSpan): string
1459
    {
1460 38
        $html .= ($colSpan > 1) ? (' colspan="' . $colSpan . '"') : '';
1461 12
        $html .= ($rowSpan > 1) ? (' rowspan="' . $rowSpan . '"') : '';
1462
1463 27
        return $html;
1464 27
    }
1465
1466 27
    private function generateRowWriteCell(
1467
        string &$html,
1468 38
        Worksheet $worksheet,
1469 38
        string $coordinate,
1470 38
        string $cellType,
1471 38
        string $cellData,
1472 38
        int $colSpan,
1473 38
        int $rowSpan,
1474
        array|string $cssClass,
1475
        int $colNum,
1476 38
        int $sheetIndex,
1477
        int $row
1478
    ): void {
1479 38
        // Image?
1480 5
        $htmlx = $this->writeImageInCell($coordinate);
1481 5
        // Chart?
1482
        $htmlx .= $this->generateRowIncludeCharts($worksheet, $coordinate);
1483
        // Column start
1484 38
        $html .= '            <' . $cellType;
1485 12
        if ($this->betterBoolean) {
1486 12
            $dataType = $worksheet->getCell($coordinate)->getDataType();
1487 11
            if ($dataType === DataType::TYPE_BOOL) {
1488
                $html .= ' data-type="' . DataType::TYPE_BOOL . '"';
1489
            } elseif ($dataType === DataType::TYPE_FORMULA && is_bool($worksheet->getCell($coordinate)->getCalculatedValue())) {
1490
                $html .= ' data-type="' . DataType::TYPE_BOOL . '"';
1491
            } elseif (is_numeric($cellData) && $worksheet->getCell($coordinate)->getDataType() === DataType::TYPE_STRING) {
1492 38
                $html .= ' data-type="' . DataType::TYPE_STRING . '"';
1493 10
            }
1494
        }
1495 38
        if (!$this->useInlineCss && !$this->isPdf && is_string($cssClass)) {
1496 38
            $html .= ' class="' . $cssClass . '"';
1497 12
            if ($htmlx) {
1498
                $html .= " style='position: relative;'";
1499
            }
1500 514
        } else {
1501
            //** Necessary redundant code for the sake of \PhpOffice\PhpSpreadsheet\Writer\Pdf **
1502 514
            // We must explicitly write the width of the <td> element because TCPDF
1503 514
            // does not recognize e.g. <col style="width:42pt">
1504
            if ($this->useInlineCss) {
1505 514
                $xcssClass = is_array($cssClass) ? $cssClass : [];
1506
            } else {
1507
                if (is_string($cssClass)) {
1508 514
                    $html .= ' class="' . $cssClass . '"';
1509
                }
1510
                $xcssClass = [];
1511 514
            }
1512
            $width = 0;
1513
            $i = $colNum - 1;
1514
            $e = $colNum + $colSpan - 1;
1515
            while ($i++ < $e) {
1516
                if (isset($this->columnWidths[$sheetIndex][$i])) {
1517
                    $width += $this->columnWidths[$sheetIndex][$i];
1518
                }
1519
            }
1520
            $xcssClass['width'] = (string) $width . 'pt';
1521 514
            // We must also explicitly write the height of the <td> element because TCPDF
1522
            // does not recognize e.g. <tr style="height:50pt">
1523
            if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $row]['height'])) {
1524 514
                $height = $this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $row]['height'];
1525 514
                $xcssClass['height'] = $height;
1526
            }
1527
            //** end of redundant code **
1528 514
            if ($this->useInlineCss) {
1529 514
                foreach (['border-top', 'border-bottom', 'border-right', 'border-left'] as $borderType) {
1530 514
                    if (($xcssClass[$borderType] ?? '') === 'none #000000') {
1531
                        unset($xcssClass[$borderType]);
1532
                    }
1533 514
                }
1534
            }
1535
1536 514
            if ($htmlx) {
1537 10
                $xcssClass['position'] = 'relative';
1538 10
            }
1539 10
            $html .= ' style="' . $this->assembleCSS($xcssClass) . '"';
1540 10
            if ($this->useInlineCss) {
1541 1
                $html .= ' class="gridlines gridlinesp"';
1542
            }
1543 10
        }
1544
        $html = $this->generateRowSpans($html, $rowSpan, $colSpan);
1545
1546
        $html .= '>';
1547
        $html .= $htmlx;
1548 514
1549 514
        $html .= $this->writeComment($worksheet, $coordinate);
1550
1551
        // Cell data
1552 514
        $html .= $cellData;
1553 514
1554 514
        // Column end
1555 15
        $html .= '</' . $cellType . '>' . PHP_EOL;
1556 15
    }
1557 15
1558
    /**
1559
     * Generate row.
1560
     *
1561 15
     * @param array $values Array containing cells in a row
1562 15
     * @param int $row Row number (0-based)
1563 13
     * @param string $cellType eg: 'td'
1564
     */
1565 3
    private function generateRow(Worksheet $worksheet, array $values, int $row, string $cellType): string
1566 3
    {
1567 3
        // Sheet index
1568 3
        $sheetIndex = $worksheet->getParentOrThrow()->getIndex($worksheet);
1569 1
        $html = $this->generateRowStart($worksheet, $sheetIndex, $row);
1570
1571
        // Write cells
1572
        $colNum = 0;
1573
        foreach ($values as $cellAddress) {
1574
            [$cell, $cssClass, $coordinate] = $this->generateRowCellCss($worksheet, $cellAddress, $row, $colNum);
1575
1576 514
            // Cell Data
1577 514
            $cellData = $this->generateRowCellData($worksheet, $cell, $cssClass);
1578
1579
            // Hyperlink?
1580
            if ($worksheet->hyperlinkExists($coordinate) && !$worksheet->getHyperlink($coordinate)->isInternal()) {
1581 514
                $url = $worksheet->getHyperlink($coordinate)->getUrl();
1582
                $urldecode = strtolower(html_entity_decode(trim($url), encoding: 'UTF-8'));
1583
                $parseScheme = preg_match('/^(\\w+):/', $urldecode, $matches);
1584
                if ($parseScheme === 1 && !in_array($matches[1], ['http', 'https', 'file', 'ftp', 's3'], true)) {
1585 514
                    $cellData = htmlspecialchars($url, Settings::htmlEntityFlags());
1586
                } else {
1587
                    $cellData = '<a href="' . htmlspecialchars($url, Settings::htmlEntityFlags()) . '" title="' . htmlspecialchars($worksheet->getHyperlink($coordinate)->getTooltip(), Settings::htmlEntityFlags()) . '">' . $cellData . '</a>';
1588 514
                }
1589
            }
1590
1591
            // Should the cell be written or is it swallowed by a rowspan or colspan?
1592
            $writeCell = !(isset($this->isSpannedCell[$worksheet->getParentOrThrow()->getIndex($worksheet)][$row + 1][$colNum])
1593
                && $this->isSpannedCell[$worksheet->getParentOrThrow()->getIndex($worksheet)][$row + 1][$colNum]);
1594 516
1595
            // Colspan and Rowspan
1596 516
            $colSpan = 1;
1597 516
            $rowSpan = 1;
1598 516
            if (isset($this->isBaseCell[$worksheet->getParentOrThrow()->getIndex($worksheet)][$row + 1][$colNum])) {
1599
                $spans = $this->isBaseCell[$worksheet->getParentOrThrow()->getIndex($worksheet)][$row + 1][$colNum];
1600 516
                $rowSpan = $spans['rowspan'];
1601
                $colSpan = $spans['colspan'];
1602 516
1603
                //    Also apply style from last cell in merge to fix borders -
1604
                //        relies on !important for non-none border declarations in createCSSStyleBorder
1605
                $endCellCoord = Coordinate::stringFromColumnIndex($colNum + $colSpan) . ($row + $rowSpan);
1606
                if (!$this->useInlineCss) {
1607
                    $cssClass .= ' style' . $worksheet->getCell($endCellCoord)->getXfIndex();
1608 17
                } else {
1609
                    $endBorders = $this->spreadsheet->getCellXfByIndex($worksheet->getCell($endCellCoord)->getXfIndex())->getBorders();
1610 17
                    $altBorders = $this->createCSSStyleBorders($endBorders);
1611
                    foreach ($altBorders as $altKey => $altValue) {
1612
                        if (str_contains($altValue, '!important')) {
1613
                            $cssClass[$altKey] = $altValue;
1614
                        }
1615
                    }
1616
                }
1617
            }
1618 1
1619
            // Write
1620 1
            if ($writeCell) {
1621
                $this->generateRowWriteCell($html, $worksheet, $coordinate, $cellType, $cellData, $colSpan, $rowSpan, $cssClass, $colNum, $sheetIndex, $row);
1622 1
            }
1623
1624
            // Next column
1625
            ++$colNum;
1626
        }
1627
1628 5
        // Write row end
1629
        $html .= '          </tr>' . PHP_EOL;
1630 5
1631
        // Return
1632
        return $html;
1633
    }
1634
1635
    /**
1636
     * Takes array where of CSS properties / values and converts to CSS string.
1637
     */
1638 4
    private function assembleCSS(array $values = []): string
1639
    {
1640 4
        $pairs = [];
1641
        foreach ($values as $property => $value) {
1642 4
            $pairs[] = $property . ':' . $value;
1643
        }
1644
        $string = implode('; ', $pairs);
1645
1646
        return $string;
1647
    }
1648 1
1649
    /**
1650 1
     * Get images root.
1651
     */
1652
    public function getImagesRoot(): string
1653
    {
1654
        return $this->imagesRoot;
1655
    }
1656
1657
    /**
1658 13
     * Set images root.
1659
     *
1660 13
     * @return $this
1661
     */
1662 13
    public function setImagesRoot(string $imagesRoot): static
1663
    {
1664
        $this->imagesRoot = $imagesRoot;
1665
1666
        return $this;
1667
    }
1668
1669
    /**
1670
     * Get embed images.
1671 392
     */
1672
    public function getEmbedImages(): bool
1673
    {
1674 392
        return $this->embedImages;
1675 392
    }
1676
1677 392
    /**
1678 392
     * Set embed images.
1679 17
     *
1680 17
     * @return $this
1681
     */
1682
    public function setEmbedImages(bool $embedImages): static
1683
    {
1684 392
        $this->embedImages = $embedImages;
1685
1686
        return $this;
1687 392
    }
1688 17
1689
    /**
1690
     * Get use inline CSS?
1691 392
     */
1692
    public function getUseInlineCss(): bool
1693
    {
1694
        return $this->useInlineCss;
1695
    }
1696
1697 516
    /**
1698
     * Set use inline CSS?
1699 516
     *
1700 516
     * @return $this
1701
     */
1702
    public function setUseInlineCss(bool $useInlineCss): static
1703
    {
1704
        $this->useInlineCss = $useInlineCss;
1705 516
1706 516
        return $this;
1707
    }
1708 516
1709 516
    /**
1710
     * Add color to formatted string as inline style.
1711 516
     *
1712
     * @param string $value Plain formatted value without color
1713
     * @param string $format Format code
1714 516
     */
1715 15
    public function formatColor(string $value, string $format): string
1716 15
    {
1717 15
        // Color information, e.g. [Red] is always at the beginning
1718
        $color = null; // initialize
1719 15
        $matches = [];
1720 15
1721
        $color_regex = '/^\\[[a-zA-Z]+\\]/';
1722 15
        if (preg_match($color_regex, $format, $matches)) {
1723 15
            $color = str_replace(['[', ']'], '', $matches[0]);
1724
            $color = strtolower($color);
1725
        }
1726 15
1727 15
        // convert to PCDATA
1728
        $result = htmlspecialchars($value, Settings::htmlEntityFlags());
1729 15
1730
        // color span tag
1731 15
        if ($color !== null) {
1732 15
            $result = '<span style="color:' . $color . '">' . $result . '</span>';
1733 15
        }
1734
1735 15
        return $result;
1736 15
    }
1737 15
1738
    /**
1739
     * Calculate information about HTML colspan and rowspan which is not always the same as Excel's.
1740 15
     */
1741 15
    private function calculateSpans(): void
1742 15
    {
1743 15
        if ($this->spansAreCalculated) {
1744 15
            return;
1745 15
        }
1746
        // Identify all cells that should be omitted in HTML due to cell merge.
1747
        // In HTML only the upper-left cell should be written and it should have
1748
        //   appropriate rowspan / colspan attribute
1749
        $sheetIndexes = $this->sheetIndex !== null
1750
            ? [$this->sheetIndex] : range(0, $this->spreadsheet->getSheetCount() - 1);
1751 516
1752
        foreach ($sheetIndexes as $sheetIndex) {
1753
            $sheet = $this->spreadsheet->getSheet($sheetIndex);
1754
1755
            $candidateSpannedRow = [];
1756
1757 516
            // loop through all Excel merged cells
1758
            foreach ($sheet->getMergeCells() as $cells) {
1759
                [$cells] = Coordinate::splitRange($cells);
1760 516
                $first = $cells[0];
1761
                $last = $cells[1];
1762
1763
                [$fc, $fr] = Coordinate::indexesFromString($first);
1764 516
                $fc = $fc - 1;
1765 516
1766 15
                [$lc, $lr] = Coordinate::indexesFromString($last);
1767 15
                $lc = $lc - 1;
1768 8
1769
                // loop through the individual cells in the individual merge
1770
                $r = $fr - 1;
1771
                while ($r++ < $lr) {
1772
                    // also, flag this row as a HTML row that is candidate to be omitted
1773
                    $candidateSpannedRow[$r] = $r;
1774 516
1775 8
                    $c = $fc - 1;
1776 8
                    while ($c++ < $lc) {
1777 8
                        if (!($c == $fc && $r == $fr)) {
1778 8
                            // not the upper-left cell (should not be written in HTML)
1779 8
                            $this->isSpannedCell[$sheetIndex][$r][$c] = [
1780 8
                                'baseCell' => [$fr, $fc],
1781
                            ];
1782 8
                        } else {
1783
                            // upper-left is the base cell that should hold the colspan/rowspan attribute
1784 8
                            $this->isBaseCell[$sheetIndex][$r][$c] = [
1785 8
                                'xlrowspan' => $lr - $fr + 1, // Excel rowspan
1786
                                'rowspan' => $lr - $fr + 1, // HTML rowspan, value may change
1787
                                'xlcolspan' => $lc - $fc + 1, // Excel colspan
1788
                                'colspan' => $lc - $fc + 1, // HTML colspan, value may change
1789
                            ];
1790
                        }
1791
                    }
1792
                }
1793
            }
1794
1795
            $this->calculateSpansOmitRows($sheet, $sheetIndex, $candidateSpannedRow);
1796
1797 514
            // TODO: Same for columns
1798
        }
1799 514
1800 514
        // We have calculated the spans
1801 23
        $this->spansAreCalculated = true;
1802 23
    }
1803 23
1804 23
    private function calculateSpansOmitRows(Worksheet $sheet, int $sheetIndex, array $candidateSpannedRow): void
1805 23
    {
1806 2
        // Identify which rows should be omitted in HTML. These are the rows where all the cells
1807
        //   participate in a merge and the where base cells are somewhere above.
1808 23
        $countColumns = Coordinate::columnIndexFromString($sheet->getHighestColumn());
1809 23
        foreach ($candidateSpannedRow as $rowIndex) {
1810 23
            if (isset($this->isSpannedCell[$sheetIndex][$rowIndex])) {
1811 23
                if (count($this->isSpannedCell[$sheetIndex][$rowIndex]) == $countColumns) {
1812
                    $this->isSpannedRow[$sheetIndex][$rowIndex] = $rowIndex;
1813
                }
1814
            }
1815 514
        }
1816
1817
        // For each of the omitted rows we found above, the affected rowspans should be subtracted by 1
1818 489
        if (isset($this->isSpannedRow[$sheetIndex])) {
1819
            foreach ($this->isSpannedRow[$sheetIndex] as $rowIndex) {
1820
                $adjustedBaseCells = [];
1821 489
                $c = -1;
1822
                $e = $countColumns - 1;
1823
                while ($c++ < $e) {
1824
                    $baseCell = $this->isSpannedCell[$sheetIndex][$rowIndex][$c]['baseCell'];
1825
1826
                    if (!in_array($baseCell, $adjustedBaseCells, true)) {
1827 516
                        // subtract rowspan by 1
1828
                        --$this->isBaseCell[$sheetIndex][$baseCell[0]][$baseCell[1]]['rowspan'];
1829
                        $adjustedBaseCells[] = $baseCell;
1830 516
                    }
1831
                }
1832
            }
1833 516
        }
1834 516
    }
1835 14
1836
    /**
1837 509
     * Write a comment in the same format as LibreOffice.
1838
     *
1839
     * @see https://github.com/LibreOffice/core/blob/9fc9bf3240f8c62ad7859947ab8a033ac1fe93fa/sc/source/filter/html/htmlexp.cxx#L1073-L1092
1840
     */
1841 516
    private function writeComment(Worksheet $worksheet, string $coordinate): string
1842
    {
1843
        $result = '';
1844 516
        if (!$this->isPdf && isset($worksheet->getComments()[$coordinate])) {
1845 516
            $sanitizedString = $this->generateRowCellDataValueRich($worksheet->getComment($coordinate)->getText());
1846 516
            $dir = ($worksheet->getComment($coordinate)->getTextboxDirection() === Comment::TEXTBOX_DIRECTION_RTL) ? ' dir="rtl"' : '';
1847 516
            $align = strtolower($worksheet->getComment($coordinate)->getAlignment());
1848 516
            $alignment = Alignment::HORIZONTAL_ALIGNMENT_FOR_HTML[$align] ?? '';
1849 516
            if ($alignment !== '') {
1850 516
                $alignment = " style=\"text-align:$alignment\"";
1851 516
            }
1852 516
            if ($sanitizedString !== '') {
1853 516
                $result .= '<a class="comment-indicator"></a>';
1854 516
                $result .= "<div class=\"comment\"$dir$alignment>" . $sanitizedString . '</div>';
1855 516
                $result .= PHP_EOL;
1856 516
            }
1857 9
        }
1858 508
1859 12
        return $result;
1860
    }
1861 516
1862 516
    public function getOrientation(): ?string
1863
    {
1864 516
        // Expect Pdf classes to override this method.
1865 516
        return $this->isPdf ? PageSetup::ORIENTATION_PORTRAIT : null;
1866 516
    }
1867 516
1868 516
    /**
1869 516
     * Generate @page declarations.
1870 516
     */
1871 516
    private function generatePageDeclarations(bool $generateSurroundingHTML): string
1872 516
    {
1873 516
        // Ensure that Spans have been calculated?
1874 516
        $this->calculateSpans();
1875 516
1876 516
        // Fetch sheets
1877 516
        $sheets = [];
1878 516
        if ($this->sheetIndex === null) {
1879 516
            $sheets = $this->spreadsheet->getAllSheets();
1880 516
        } else {
1881 516
            $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex);
1882
        }
1883 516
1884
        // Construct HTML
1885
        $htmlPage = $generateSurroundingHTML ? ('<style type="text/css">' . PHP_EOL) : '';
1886 514
1887
        // Loop all sheets
1888 514
        $sheetId = 0;
1889 494
        foreach ($sheets as $worksheet) {
1890
            $htmlPage .= "@page page$sheetId { ";
1891
            $left = StringHelper::formatNumber($worksheet->getPageMargins()->getLeft()) . 'in; ';
1892 26
            $htmlPage .= 'margin-left: ' . $left;
1893
            $right = StringHelper::FormatNumber($worksheet->getPageMargins()->getRight()) . 'in; ';
1894
            $htmlPage .= 'margin-right: ' . $right;
1895 516
            $top = StringHelper::FormatNumber($worksheet->getPageMargins()->getTop()) . 'in; ';
1896
            $htmlPage .= 'margin-top: ' . $top;
1897 516
            $bottom = StringHelper::FormatNumber($worksheet->getPageMargins()->getBottom()) . 'in; ';
1898 496
            $htmlPage .= 'margin-bottom: ' . $bottom;
1899
            $orientation = $this->getOrientation() ?? $worksheet->getPageSetup()->getOrientation();
1900 26
            if ($orientation === PageSetup::ORIENTATION_LANDSCAPE) {
1901 25
                $htmlPage .= 'size: landscape; ';
1902
            } elseif ($orientation === PageSetup::ORIENTATION_PORTRAIT) {
1903
                $htmlPage .= 'size: portrait; ';
1904 9
            }
1905
            $htmlPage .= '}' . PHP_EOL;
1906
            ++$sheetId;
1907
        }
1908
        $htmlPage .= implode(PHP_EOL, [
1909
            '.navigation {page-break-after: always;}',
1910
            '.scrpgbrk, div + div {page-break-before: always;}',
1911
            '@media screen {',
1912
            '  .gridlines td {border: 1px solid black;}',
1913
            '  .gridlines th {border: 1px solid black;}',
1914
            '  body>div {margin-top: 5px;}',
1915
            '  body>div:first-child {margin-top: 0;}',
1916
            '  .scrpgbrk {margin-top: 1px;}',
1917
            '}',
1918
            '@media print {',
1919
            '  .gridlinesp td {border: 1px solid black;}',
1920
            '  .gridlinesp th {border: 1px solid black;}',
1921
            '  .navigation {display: none;}',
1922
            '}',
1923
            '',
1924
        ]);
1925
        $htmlPage .= $generateSurroundingHTML ? ('</style>' . PHP_EOL) : '';
1926
1927
        return $htmlPage;
1928
    }
1929
1930
    private function shouldGenerateRow(Worksheet $sheet, int $row): bool
1931
    {
1932
        if (!($this instanceof Pdf\Mpdf || $this instanceof Pdf\Tcpdf)) {
1933
            return true;
1934
        }
1935
1936
        return $sheet->isRowVisible($row);
1937
    }
1938
1939
    private function shouldGenerateColumn(Worksheet $sheet, string $colStr): bool
1940
    {
1941
        if (!($this instanceof Pdf\Mpdf || $this instanceof Pdf\Tcpdf)) {
1942
            return true;
1943
        }
1944
        if (!$sheet->columnDimensionExists($colStr)) {
1945
            return true;
1946
        }
1947
1948
        return $sheet->getColumnDimension($colStr)->getVisible();
1949
    }
1950
1951
    public function getBetterBoolean(): bool
1952
    {
1953
        return $this->betterBoolean;
1954
    }
1955
1956
    public function setBetterBoolean(bool $betterBoolean): self
1957
    {
1958
        $this->betterBoolean = $betterBoolean;
1959
1960
        return $this;
1961
    }
1962
}
1963