Passed
Push — master ( 63ccb0...2d493d )
by Owen
15:36
created

Html::generateRowWriteCell()   C

Complexity

Conditions 16
Paths 66

Size

Total Lines 80
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 38
CRAP Score 16

Importance

Changes 0
Metric Value
eloc 39
c 0
b 0
f 0
dl 0
loc 80
rs 5.5666
ccs 38
cts 38
cp 1
cc 16
nc 66
nop 11
crap 16

How to fix   Long Method    Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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