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