Failed Conditions
Pull Request — master (#4328)
by Owen
15:26 queued 04:43
created

Html::generateRowWriteCell()   F

Complexity

Conditions 23
Paths 330

Size

Total Lines 90
Code Lines 47

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 46
CRAP Score 23

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 23
nc 330
nop 11
crap 23

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