Failed Conditions
Pull Request — master (#4412)
by
unknown
22:45 queued 07:48
created

Html::generateRowWriteCell()   F

Complexity

Conditions 41
Paths 10230

Size

Total Lines 140
Code Lines 81

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 78
CRAP Score 41.0034

Importance

Changes 0
Metric Value
eloc 81
dl 0
loc 140
ccs 78
cts 79
cp 0.9873
rs 0
c 0
b 0
f 0
cc 41
nc 10230
nop 12
crap 41.0034

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