Passed
Pull Request — master (#4142)
by Owen
13:37
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
            $opacity = '';
623 20
            $opacityValue = $drawing->getOpacity();
624 20
            if ($opacityValue !== null) {
625 14
                $opacityValue = $opacityValue / 100000;
626
                if ($opacityValue >= 0.0 && $opacityValue <= 1.0) {
627
                    $opacity = "opacity:$opacityValue; ";
628 14
                }
629
            }
630
            $filedesc = $drawing->getDescription();
631 14
            $filedesc = $filedesc ? htmlspecialchars($filedesc, ENT_QUOTES) : 'Embedded image';
632
            if ($drawing instanceof Drawing && $drawing->getPath() !== '') {
633
                $filename = $drawing->getPath();
634 14
635
                // Strip off eventual '.'
636
                $filename = (string) preg_replace('/^[.]/', '', $filename);
637 14
638
                // Prepend images root
639 14
                $filename = $this->getImagesRoot() . $filename;
640 14
641
                // Strip off eventual '.' if followed by non-/
642 14
                $filename = (string) preg_replace('@^[.]([^/])@', '$1', $filename);
643 8
644 8
                // Convert UTF8 data to PCDATA
645 8
                $filename = htmlspecialchars($filename, Settings::htmlEntityFlags());
646
647 8
                $html .= PHP_EOL;
648 8
                $imageData = self::winFileToUrl($filename, $this instanceof Pdf\Mpdf);
649
650
                if ($this->embedImages || str_starts_with($imageData, 'zip://')) {
651
                    $imageData = 'data:,';
652 14
                    $picture = @file_get_contents($filename);
653 14
                    if ($picture !== false) {
654 14
                        $imageDetails = getimagesize($filename) ?: ['mime' => ''];
655 14
                        if (str_starts_with($imageDetails['mime'], 'image/')) {
656 6
                            // base64 encode the binary data
657 6
                            $base64 = base64_encode($picture);
658 6
                            $imageData = 'data:' . $imageDetails['mime'] . ';base64,' . $base64;
659 6
                        }
660 6
                    }
661 6
                }
662 6
663
                $html .= '<img style="' . $opacity . 'position: absolute; z-index: 1; left: '
664 6
                    . $drawing->getOffsetX() . 'px; top: ' . $drawing->getOffsetY() . 'px; width: '
665
                    . $drawing->getWidth() . 'px; height: ' . $drawing->getHeight() . 'px;" src="'
666
                    . $imageData . '" alt="' . $filedesc . '" />';
667
            } elseif ($drawing instanceof MemoryDrawing) {
668
                $imageResource = $drawing->getImageResource();
669
                if ($imageResource) {
670
                    ob_start(); //  Let's start output buffering.
671
                    imagepng($imageResource); //  This will normally output the image, but because of ob_start(), it won't.
672
                    $contents = (string) ob_get_contents(); //  Instead, output above is saved to $contents
673
                    ob_end_clean(); //  End the output buffer.
674
675 6
                    $dataUri = 'data:image/png;base64,' . base64_encode($contents);
676 6
677
                    //  Because of the nature of tables, width is more important than height.
678
                    //  max-width: 100% ensures that image doesnt overflow containing cell
679
                    //    However, PR #3535 broke test
680
                    //    25_In_memory_image, apparently because
681 498
                    //    of the use of max-with. In addition,
682
                    //    non-memory-drawings don't use max-width.
683
                    //    Its use here is suspect and is being eliminated.
684
                    //  width: X sets width of supplied image.
685
                    //  As a result, images bigger than cell will be contained and images smaller will not get stretched
686
                    $html .= '<img alt="' . $filedesc . '" src="' . $dataUri . '" style="' . $opacity . 'width:' . $drawing->getWidth() . 'px;left: '
687
                        . $drawing->getOffsetX() . 'px; top: ' . $drawing->getOffsetY() . 'px;position: absolute; z-index: 1;" />';
688
                }
689 4
            }
690
        }
691
692 4
        return $html;
693
    }
694
695 4
    /**
696 4
     * Generate chart tag in cell.
697 4
     * This code should be exercised by sample:
698 4
     * Chart/32_Chart_read_write_PDF.php.
699 4
     */
700 4
    private function writeChartInCell(Worksheet $worksheet, string $coordinates): string
701 4
    {
702 4
        // Construct HTML
703
        $html = '';
704 4
705 4
        // Write charts
706 4
        $chart = $this->sheetCharts[$coordinates] ?? null;
707 4
        if ($chart !== null) {
708 4
            $chartCoordinates = $chart->getTopLeftPosition();
709 4
            $chartFileName = File::sysGetTempDir() . '/' . uniqid('', true) . '.png';
710 2
            $renderedWidth = $chart->getRenderedWidth();
711 2
            $renderedHeight = $chart->getRenderedHeight();
712
            if ($renderedWidth === null || $renderedHeight === null) {
713 4
                $this->adjustRendererPositions($chart, $worksheet);
714
            }
715 4
            $title = $chart->getTitle();
716 4
            $caption = null;
717 4
            $filedesc = '';
718 4
            if ($title !== null) {
719 2
                $calculatedTitle = $title->getCalculatedTitle($worksheet->getParent());
720
                if ($calculatedTitle !== null) {
721 4
                    $caption = $title->getCaption();
722
                    $title->setCaption($calculatedTitle);
723
                }
724
                $filedesc = $title->getCaptionText($worksheet->getParent());
725 4
            }
726 4
            $renderSuccessful = $chart->render($chartFileName);
727
            $chart->setRenderedWidth($renderedWidth);
728 4
            $chart->setRenderedHeight($renderedHeight);
729 4
            if (isset($title, $caption)) {
730 4
                $title->setCaption($caption);
731 4
            }
732 4
            if (!$renderSuccessful) {
733 4
                return '';
734
            }
735 4
736
            $html .= PHP_EOL;
737
            $imageDetails = getimagesize($chartFileName) ?: ['', '', 'mime' => ''];
738
739
            $filedesc = $filedesc ? htmlspecialchars($filedesc, ENT_QUOTES) : 'Embedded chart';
740 4
            $picture = file_get_contents($chartFileName);
741
            unlink($chartFileName);
742
            if ($picture !== false) {
743 4
                $base64 = base64_encode($picture);
744
                $imageData = 'data:' . $imageDetails['mime'] . ';base64,' . $base64;
745 4
746 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;
747 4
            }
748 4
        }
749 4
750 4
        // Return
751 4
        return $html;
752 4
    }
753 4
754 4
    private function adjustRendererPositions(Chart $chart, Worksheet $sheet): void
755 4
    {
756 4
        $topLeft = $chart->getTopLeftPosition();
757 4
        $bottomRight = $chart->getBottomRightPosition();
758 4
        $tlCell = $topLeft['cell'];
759 4
        $brCell = $bottomRight['cell'];
760
        if ($tlCell !== '' && $brCell !== '') {
761 4
            $tlCoordinate = Coordinate::indexesFromString($tlCell);
762 4
            $brCoordinate = Coordinate::indexesFromString($brCell);
763 4
            $totalHeight = 0.0;
764 4
            $totalWidth = 0.0;
765 4
            $defaultRowHeight = $sheet->getDefaultRowDimension()->getRowHeight();
766 4
            $defaultRowHeight = SharedDrawing::pointsToPixels(($defaultRowHeight >= 0) ? $defaultRowHeight : SharedFont::getDefaultRowHeightByFont($this->defaultFont));
767
            if ($tlCoordinate[1] <= $brCoordinate[1] && $tlCoordinate[0] <= $brCoordinate[0]) {
768 4
                for ($row = $tlCoordinate[1]; $row <= $brCoordinate[1]; ++$row) {
769 4
                    $height = $sheet->getRowDimension($row)->getRowHeight('pt');
770
                    $totalHeight += ($height >= 0) ? $height : $defaultRowHeight;
771
                }
772
                $rightEdge = $brCoordinate[2];
773
                ++$rightEdge;
774
                for ($column = $tlCoordinate[2]; $column !== $rightEdge; ++$column) {
775
                    $width = $sheet->getColumnDimension($column)->getWidth();
776
                    $width = ($width < 0) ? self::DEFAULT_CELL_WIDTH_PIXELS : SharedDrawing::cellDimensionToPixels($sheet->getColumnDimension($column)->getWidth(), $this->defaultFont);
777
                    $totalWidth += $width;
778
                }
779 493
                $chart->setRenderedWidth($totalWidth);
780
                $chart->setRenderedHeight($totalHeight);
781
            }
782 493
        }
783
    }
784
785 493
    /**
786
     * Generate CSS styles.
787
     *
788 493
     * @param bool $generateSurroundingHTML Generate surrounding HTML tags? (&lt;style&gt; and &lt;/style&gt;)
789 493
     */
790 493
    public function generateStyles(bool $generateSurroundingHTML = true): string
791
    {
792
        // Build CSS
793
        $css = $this->buildCSS($generateSurroundingHTML);
794 493
795 493
        // Construct HTML
796 493
        $html = '';
797
798
        // Start styles
799 493
        if ($generateSurroundingHTML) {
800
            $html .= '    <style type="text/css">' . PHP_EOL;
801
            $html .= (array_key_exists('html', $css)) ? ('      html { ' . $this->assembleCSS($css['html']) . ' }' . PHP_EOL) : '';
802 493
        }
803 493
804
        // Write all other styles
805
        foreach ($css as $styleName => $styleDefinition) {
806
            if ($styleName != 'html') {
807 493
                $html .= '      ' . $styleName . ' { ' . $this->assembleCSS($styleDefinition) . ' }' . PHP_EOL;
808
            }
809
        }
810 500
        $html .= $this->generatePageDeclarations(false);
811
812
        // End styles
813 500
        if ($generateSurroundingHTML) {
814 19
            $html .= '    </style>' . PHP_EOL;
815
        }
816
817 19
        // Return
818
        return $html;
819 19
    }
820 9
821 9
    private function buildCssRowHeights(Worksheet $sheet, array &$css, int $sheetIndex): void
822
    {
823 19
        // Calculate row heights
824 8
        foreach ($sheet->getRowDimensions() as $rowDimension) {
825 8
            $row = $rowDimension->getRowIndex() - 1;
826
827
            // table.sheetN tr.rowYYYYYY { }
828
            $css['table.sheet' . $sheetIndex . ' tr.row' . $row] = [];
829
830 500
            if ($rowDimension->getRowHeight() != -1) {
831
                $pt_height = $rowDimension->getRowHeight();
832
                $css['table.sheet' . $sheetIndex . ' tr.row' . $row]['height'] = $pt_height . 'pt';
833 500
            }
834 500
            if ($rowDimension->getVisible() === false) {
835 500
                $css['table.sheet' . $sheetIndex . ' tr.row' . $row]['display'] = 'none';
836 2
                $css['table.sheet' . $sheetIndex . ' tr.row' . $row]['visibility'] = 'hidden';
837 2
            }
838
        }
839 500
    }
840 500
841 1
    private function buildCssPerSheet(Worksheet $sheet, array &$css): void
842 1
    {
843
        // Calculate hash code
844
        $sheetIndex = $sheet->getParentOrThrow()->getIndex($sheet);
845
        $setup = $sheet->getPageSetup();
846
        if ($setup->getFitToPage() && $setup->getFitToHeight() === 1) {
847 500
            $css["table.sheet$sheetIndex"]['page-break-inside'] = 'avoid';
848
            $css["table.sheet$sheetIndex"]['break-inside'] = 'avoid';
849
        }
850 500
        $picture = $sheet->getBackgroundImage();
851 500
        if ($picture !== '') {
852 500
            $base64 = base64_encode($picture);
853 500
            $css["table.sheet$sheetIndex"]['background-image'] = 'url(data:' . $sheet->getBackgroundMime() . ';base64,' . $base64 . ')';
854 500
        }
855 500
856 500
        // Build styles
857
        // Calculate column widths
858 500
        $sheet->calculateColumnWidths();
859
860
        // col elements, initialize
861
        $highestColumnIndex = Coordinate::columnIndexFromString($sheet->getHighestColumn()) - 1;
862 500
        $column = -1;
863 25
        $colStr = 'A';
864 25
        while ($column++ < $highestColumnIndex) {
865 25
            $this->columnWidths[$sheetIndex][$column] = self::DEFAULT_CELL_WIDTH_POINTS; // approximation
866 25
            if ($this->shouldGenerateColumn($sheet, $colStr)) {
867 6
                $css['table.sheet' . $sheetIndex . ' col.col' . $column]['width'] = self::DEFAULT_CELL_WIDTH_POINTS . 'pt';
868
            }
869
            ++$colStr;
870
        }
871
872 25
        // col elements, loop through columnDimensions and set width
873 18
        foreach ($sheet->getColumnDimensions() as $columnDimension) {
874 18
            $column = Coordinate::columnIndexFromString($columnDimension->getColumnIndex()) - 1;
875
            $width = SharedDrawing::cellDimensionToPixels($columnDimension->getWidth(), $this->defaultFont);
876
            $width = SharedDrawing::pixelsToPoints($width);
877
            if ($columnDimension->getVisible() === false) {
878
                $css['table.sheet' . $sheetIndex . ' .column' . $column]['display'] = 'none';
879 500
                // This would be better but Firefox has an 11-year-old bug.
880
                // https://bugzilla.mozilla.org/show_bug.cgi?id=819045
881
                //$css['table.sheet' . $sheetIndex . ' col.col' . $column]['visibility'] = 'collapse';
882 500
            }
883
            if ($width >= 0) {
884 500
                $this->columnWidths[$sheetIndex][$column] = $width;
885 493
                $css['table.sheet' . $sheetIndex . ' col.col' . $column]['width'] = $width . 'pt';
886
            }
887 7
        }
888
889 500
        // Default row height
890 500
        $rowDimension = $sheet->getDefaultRowDimension();
891 1
892 1
        // table.sheetN tr { }
893
        $css['table.sheet' . $sheetIndex . ' tr'] = [];
894
895 500
        if ($rowDimension->getRowHeight() == -1) {
896
            $pt_height = SharedFont::getDefaultRowHeightByFont($this->spreadsheet->getDefaultStyle()->getFont());
897
        } else {
898
            $pt_height = $rowDimension->getRowHeight();
899
        }
900
        $css['table.sheet' . $sheetIndex . ' tr']['height'] = $pt_height . 'pt';
901
        if ($rowDimension->getVisible() === false) {
902
            $css['table.sheet' . $sheetIndex . ' tr']['display'] = 'none';
903 500
            $css['table.sheet' . $sheetIndex . ' tr']['visibility'] = 'hidden';
904
        }
905
906 500
        $this->buildCssRowHeights($sheet, $css, $sheetIndex);
907 491
    }
908
909
    /**
910
     * Build CSS styles.
911 500
     *
912
     * @param bool $generateSurroundingHTML Generate surrounding HTML style? (html { })
913
     */
914 500
    public function buildCSS(bool $generateSurroundingHTML = true): array
915
    {
916
        // Cached?
917 500
        if ($this->cssStyles !== null) {
918
            return $this->cssStyles;
919 492
        }
920 492
921 492
        // Ensure that spans have been calculated
922
        $this->calculateSpans();
923
924
        // Construct CSS
925 500
        $css = [];
926 500
927 500
        // Start styles
928 500
        if ($generateSurroundingHTML) {
929 500
            // html { }
930 500
            $css['html']['font-family'] = 'Calibri, Arial, Helvetica, sans-serif';
931 500
            $css['html']['font-size'] = '11pt';
932
            $css['html']['background-color'] = 'white';
933 500
        }
934 500
935 500
        // CSS for comments as found in LibreOffice
936 500
        $css['a.comment-indicator:hover + div.comment'] = [
937 500
            'background' => '#ffd',
938 500
            'position' => 'absolute',
939 500
            'display' => 'block',
940
            'border' => '1px solid black',
941 500
            'padding' => '0.5em',
942
        ];
943
944 500
        $css['a.comment-indicator'] = [
945
            'background' => 'red',
946
            'display' => 'inline-block',
947 500
            'border' => '1px solid black',
948
            'width' => '0.5em',
949
            'height' => '0.5em',
950 500
        ];
951
952
        $css['div.comment']['display'] = 'none';
953 500
954
        // table { }
955
        $css['table']['border-collapse'] = 'collapse';
956 500
957
        // .b {}
958
        $css['.b']['text-align'] = 'center'; // BOOL
959 500
960
        // .e {}
961
        $css['.e']['text-align'] = 'center'; // ERROR
962 500
963
        // .f {}
964
        $css['.f']['text-align'] = 'right'; // FORMULA
965 500
966 500
        // .inlineStr {}
967
        $css['.inlineStr']['text-align'] = 'left'; // INLINE
968
969
        // .n {}
970
        $css['.n']['text-align'] = 'right'; // NUMERIC
971 500
972 500
        // .s {}
973 14
        $css['.s']['text-align'] = 'left'; // STRING
974
975 493
        // Calculate cell style hashes
976
        foreach ($this->spreadsheet->getCellXfCollection() as $index => $style) {
977
            $css['td.style' . $index . ', th.style' . $index] = $this->createCSSStyle($style);
978
            //$css['th.style' . $index] = $this->createCSSStyle($style);
979 500
        }
980 500
981
        // Fetch sheets
982
        $sheets = [];
983
        if ($this->sheetIndex === null) {
984 500
            $sheets = $this->spreadsheet->getAllSheets();
985 500
        } else {
986
            $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex);
987
        }
988
989 500
        // Build styles per sheet
990
        foreach ($sheets as $sheet) {
991
            $this->buildCssPerSheet($sheet, $css);
992
        }
993
994
        // Cache
995 500
        if ($this->cssStyles === null) {
996
            $this->cssStyles = $css;
997
        }
998 500
999 500
        // Return
1000 500
        return $css;
1001 500
    }
1002 500
1003 500
    /**
1004
     * Create CSS style.
1005
     */
1006
    private function createCSSStyle(Style $style): array
1007
    {
1008
        // Create CSS
1009 500
        return array_merge(
1010
            $this->createCSSStyleAlignment($style->getAlignment()),
1011
            $this->createCSSStyleBorders($style->getBorders()),
1012 500
            $this->createCSSStyleFont($style->getFont()),
1013
            $this->createCSSStyleFill($style->getFill())
1014
        );
1015 500
    }
1016 500
1017 500
    /**
1018
     * Create CSS style.
1019 500
     */
1020 500
    private function createCSSStyleAlignment(Alignment $alignment): array
1021 13
    {
1022 13
        // Construct CSS
1023 10
        $css = [];
1024
1025
        // Create CSS
1026 500
        $verticalAlign = $this->mapVAlign($alignment->getVertical() ?? '');
1027 500
        if ($verticalAlign) {
1028 5
            $css['vertical-align'] = $verticalAlign;
1029 1
        }
1030
        $textAlign = $this->mapHAlign($alignment->getHorizontal() ?? '');
1031 4
        if ($textAlign) {
1032
            $css['text-align'] = $textAlign;
1033
            if (in_array($textAlign, ['left', 'right'])) {
1034
                $css['padding-' . $textAlign] = (string) ((int) $alignment->getIndent() * 9) . 'px';
1035 500
            }
1036
        }
1037
        $rotation = $alignment->getTextRotation();
1038
        if ($rotation !== 0 && $rotation !== Alignment::TEXTROTATION_STACK_PHPSPREADSHEET) {
1039
            if ($this instanceof Pdf\Mpdf) {
1040
                $css['text-rotate'] = "$rotation";
1041 500
            } else {
1042
                $css['transform'] = "rotate({$rotation}deg)";
1043
            }
1044 500
        }
1045
1046
        return $css;
1047 500
    }
1048 15
1049
    /**
1050 500
     * Create CSS style.
1051 1
     */
1052 500
    private function createCSSStyleFont(Font $font): array
1053 10
    {
1054 500
        // Construct CSS
1055 1
        $css = [];
1056
1057 500
        // Create CSS
1058 9
        if ($font->getBold()) {
1059
            $css['font-weight'] = 'bold';
1060
        }
1061 500
        if ($font->getUnderline() != Font::UNDERLINE_NONE && $font->getStrikethrough()) {
1062 500
            $css['text-decoration'] = 'underline line-through';
1063 500
        } elseif ($font->getUnderline() != Font::UNDERLINE_NONE) {
1064
            $css['text-decoration'] = 'underline';
1065 500
        } elseif ($font->getStrikethrough()) {
1066
            $css['text-decoration'] = 'line-through';
1067
        }
1068
        if ($font->getItalic()) {
1069
            $css['font-style'] = 'italic';
1070
        }
1071
1072
        $css['color'] = '#' . $font->getColor()->getRGB();
1073 500
        $css['font-family'] = '\'' . htmlspecialchars((string) $font->getName(), ENT_QUOTES) . '\'';
1074
        $css['font-size'] = $font->getSize() . 'pt';
1075
1076 500
        return $css;
1077
    }
1078
1079 500
    /**
1080 485
     * Create CSS style.
1081 485
     *
1082 485
     * @param Borders $borders Borders
1083 485
     */
1084
    private function createCSSStyleBorders(Borders $borders): array
1085
    {
1086 20
        // Construct CSS
1087 5
        $css = [];
1088
1089 20
        // Create CSS
1090 5
        if (!($this instanceof Pdf\Mpdf)) {
1091
            $css['border-bottom'] = $this->createCSSStyleBorder($borders->getBottom());
1092 20
            $css['border-top'] = $this->createCSSStyleBorder($borders->getTop());
1093 5
            $css['border-left'] = $this->createCSSStyleBorder($borders->getLeft());
1094
            $css['border-right'] = $this->createCSSStyleBorder($borders->getRight());
1095 20
        } else {
1096 5
            // Mpdf doesn't process !important, so omit unimportant border none
1097
            if ($borders->getBottom()->getBorderStyle() !== Border::BORDER_NONE) {
1098
                $css['border-bottom'] = $this->createCSSStyleBorder($borders->getBottom());
1099
            }
1100 500
            if ($borders->getTop()->getBorderStyle() !== Border::BORDER_NONE) {
1101
                $css['border-top'] = $this->createCSSStyleBorder($borders->getTop());
1102
            }
1103
            if ($borders->getLeft()->getBorderStyle() !== Border::BORDER_NONE) {
1104
                $css['border-left'] = $this->createCSSStyleBorder($borders->getLeft());
1105
            }
1106
            if ($borders->getRight()->getBorderStyle() !== Border::BORDER_NONE) {
1107
                $css['border-right'] = $this->createCSSStyleBorder($borders->getRight());
1108 489
            }
1109
        }
1110
1111 489
        return $css;
1112
    }
1113 489
1114
    /**
1115
     * Create CSS style.
1116
     *
1117
     * @param Border $border Border
1118
     */
1119
    private function createCSSStyleBorder(Border $border): string
1120
    {
1121 500
        //    Create CSS - add !important to non-none border styles for merged cells
1122
        $borderStyle = $this->mapBorderStyle($border->getBorderStyle());
1123
1124 500
        return $borderStyle . ' #' . $border->getColor()->getRGB() . (($borderStyle === self::BORDER_NONE) ? '' : ' !important');
1125
    }
1126
1127 500
    /**
1128 11
     * Create CSS style (Fill).
1129 11
     *
1130 11
     * @param Fill $fill Fill
1131
     */
1132
    private function createCSSStyleFill(Fill $fill): array
1133 500
    {
1134
        // Construct HTML
1135
        $css = [];
1136
1137
        // Create CSS
1138
        if ($fill->getFillType() !== Fill::FILL_NONE) {
1139 498
            $value = $fill->getFillType() == Fill::FILL_NONE
1140
                ? 'white' : '#' . $fill->getStartColor()->getRGB();
1141
            $css['background-color'] = $value;
1142 498
        }
1143 498
1144 498
        return $css;
1145
    }
1146 498
1147
    /**
1148
     * Generate HTML footer.
1149 12
     */
1150
    public function generateHTMLFooter(): string
1151 12
    {
1152 12
        // Construct HTML
1153
        $html = '';
1154 12
        $html .= '  </body>' . PHP_EOL;
1155 12
        $html .= '</html>' . PHP_EOL;
1156 12
1157 1
        return $html;
1158 12
    }
1159 6
1160 7
    private function generateTableTagInline(Worksheet $worksheet, string $id): string
1161 1
    {
1162
        $style = isset($this->cssStyles['table'])
1163 7
            ? $this->assembleCSS($this->cssStyles['table']) : '';
1164
1165
        $prntgrid = $worksheet->getPrintGridlines();
1166 12
        $viewgrid = $this->isPdf ? $prntgrid : $worksheet->getShowGridlines();
1167
        if ($viewgrid && $prntgrid) {
1168
            $html = "    <table border='1' cellpadding='1' $id cellspacing='1' style='$style' class='gridlines gridlinesp'>" . PHP_EOL;
1169 498
        } elseif ($viewgrid) {
1170
            $html = "    <table border='0' cellpadding='0' $id cellspacing='0' style='$style' class='gridlines'>" . PHP_EOL;
1171 498
        } elseif ($prntgrid) {
1172 491
            $html = "    <table border='0' cellpadding='0' $id cellspacing='0' style='$style' class='gridlinesp'>" . PHP_EOL;
1173 491
        } else {
1174 491
            $html = "    <table border='0' cellpadding='1' $id cellspacing='0' style='$style'>" . PHP_EOL;
1175
        }
1176 12
1177
        return $html;
1178
    }
1179
1180
    private function generateTableTag(Worksheet $worksheet, string $id, string &$html, int $sheetIndex): void
1181
    {
1182
        if (!$this->useInlineCss) {
1183
            $gridlines = $worksheet->getShowGridlines() ? ' gridlines' : '';
1184
            $gridlinesp = $worksheet->getPrintGridlines() ? ' gridlinesp' : '';
1185
            $html .= "    <table border='0' cellpadding='0' cellspacing='0' $id class='sheet$sheetIndex$gridlines$gridlinesp'>" . PHP_EOL;
1186 498
        } else {
1187
            $html .= $this->generateTableTagInline($worksheet, $id);
1188 498
        }
1189
    }
1190
1191 498
    /**
1192 498
     * Generate table header.
1193 498
     *
1194 498
     * @param Worksheet $worksheet The worksheet for the table we are writing
1195
     * @param bool $showid whether or not to add id to table tag
1196 2
     */
1197
    private function generateTableHeader(Worksheet $worksheet, bool $showid = true): string
1198
    {
1199 498
        $sheetIndex = $worksheet->getParentOrThrow()->getIndex($worksheet);
1200
1201
        // Construct HTML
1202 498
        $html = '';
1203 498
        $id = $showid ? "id='sheet$sheetIndex'" : '';
1204 498
        if ($showid) {
1205 498
            $html .= "<div style='page: page$sheetIndex'>" . PHP_EOL;
1206 491
        } else {
1207
            $html .= "<div style='page: page$sheetIndex' class='scrpgbrk'>" . PHP_EOL;
1208 12
        }
1209 12
1210 12
        $this->generateTableTag($worksheet, $id, $html, $sheetIndex);
1211
1212
        // Write <col> elements
1213
        $highestColumnIndex = Coordinate::columnIndexFromString($worksheet->getHighestColumn()) - 1;
1214 498
        $i = -1;
1215
        while ($i++ < $highestColumnIndex) {
1216
            if (!$this->useInlineCss) {
1217
                $html .= '        <col class="col' . $i . '" />' . PHP_EOL;
1218
            } else {
1219
                $style = isset($this->cssStyles['table.sheet' . $sheetIndex . ' col.col' . $i])
1220 498
                    ? $this->assembleCSS($this->cssStyles['table.sheet' . $sheetIndex . ' col.col' . $i]) : '';
1221
                $html .= '        <col style="' . $style . '" />' . PHP_EOL;
1222 498
            }
1223
        }
1224
1225
        return $html;
1226
    }
1227
1228
    /**
1229
     * Generate table footer.
1230
     */
1231 498
    private function generateTableFooter(): string
1232
    {
1233 498
        return '    </tbody></table>' . PHP_EOL . '</div>' . PHP_EOL;
1234 498
    }
1235 2
1236
    /**
1237
     * Generate row start.
1238 2
     *
1239
     * @param int $sheetIndex Sheet index (0-based)
1240 2
     * @param int $row row number
1241 2
     */
1242 1
    private function generateRowStart(Worksheet $worksheet, int $sheetIndex, int $row): string
1243
    {
1244
        $html = '';
1245
        if (count($worksheet->getBreaks()) > 0) {
1246 2
            $breaks = $worksheet->getRowBreaks();
1247 2
1248
            // check if a break is needed before this row
1249
            if (isset($breaks['A' . $row])) {
1250
                // close table: </table>
1251
                $html .= $this->generateTableFooter();
1252 498
                if ($this->isPdf && $this->useInlineCss) {
1253 491
                    $html .= '<div style="page-break-before:always" />';
1254
                }
1255 12
1256 12
                // open table again: <table> + <col> etc.
1257
                $html .= $this->generateTableHeader($worksheet, false);
1258 12
                $html .= '<tbody>' . PHP_EOL;
1259
            }
1260
        }
1261 498
1262
        // Write row start
1263
        if (!$this->useInlineCss) {
1264 498
            $html .= '          <tr class="row' . $row . '">' . PHP_EOL;
1265
        } else {
1266 498
            $style = isset($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $row])
1267 498
                ? $this->assembleCSS($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $row]) : '';
1268 498
1269 491
            $html .= '          <tr style="' . $style . '">' . PHP_EOL;
1270
        }
1271 12
1272
        return $html;
1273
    }
1274
1275
    private function generateRowCellCss(Worksheet $worksheet, string $cellAddress, int $row, int $columnNumber): array
1276
    {
1277
        $cell = ($cellAddress > '') ? $worksheet->getCellCollection()->get($cellAddress) : '';
1278
        $coordinate = Coordinate::stringFromColumnIndex($columnNumber + 1) . ($row + 1);
1279
        if (!$this->useInlineCss) {
1280
            $cssClass = 'column' . $columnNumber;
1281
        } else {
1282
            $cssClass = [];
1283
            // The statements below do nothing.
1284
            // Commenting out the code rather than deleting it
1285
            // in case someone can figure out what their intent was.
1286
            //if ($cellType == 'th') {
1287 498
            //    if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' th.column' . $colNum])) {
1288
            //        $this->cssStyles['table.sheet' . $sheetIndex . ' th.column' . $colNum];
1289
            //    }
1290 30
            //} else {
1291
            //    if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' td.column' . $colNum])) {
1292 30
            //        $this->cssStyles['table.sheet' . $sheetIndex . ' td.column' . $colNum];
1293
            //    }
1294 30
            //}
1295 30
            // End of mystery statements.
1296
        }
1297 30
1298 14
        return [$cell, $cssClass, $coordinate];
1299 14
    }
1300 14
1301
    private function generateRowCellDataValueRich(RichText $richText): string
1302 14
    {
1303 1
        $cellData = '';
1304 1
        // Loop through rich text elements
1305 14
        $elements = $richText->getRichTextElements();
1306 1
        foreach ($elements as $element) {
1307 1
            // Rich text start?
1308
            if ($element instanceof Run) {
1309
                $cellEnd = '';
1310
                if ($element->getFont() !== null) {
1311
                    $cellData .= '<span style="' . $this->assembleCSS($this->createCSSStyleFont($element->getFont())) . '">';
1312
1313
                    if ($element->getFont()->getSuperscript()) {
1314 14
                        $cellData .= '<sup>';
1315 14
                        $cellEnd = '</sup>';
1316
                    } elseif ($element->getFont()->getSubscript()) {
1317 14
                        $cellData .= '<sub>';
1318
                        $cellEnd = '</sub>';
1319 14
                    }
1320
                } else {
1321
                    $cellData .= '<span>';
1322 26
                }
1323 26
1324
                // Convert UTF8 data to PCDATA
1325
                $cellText = $element->getText();
1326
                $cellData .= htmlspecialchars($cellText, Settings::htmlEntityFlags());
1327 30
1328
                $cellData .= $cellEnd;
1329
1330 496
                $cellData .= '</span>';
1331
            } else {
1332 496
                // Convert UTF8 data to PCDATA
1333 10
                $cellText = $element->getText();
1334
                $cellData .= htmlspecialchars($cellText, Settings::htmlEntityFlags());
1335 496
            }
1336 496
        }
1337 496
1338
        return nl2br($cellData);
1339 496
    }
1340 496
1341 496
    private function generateRowCellDataValue(Worksheet $worksheet, Cell $cell, string &$cellData): void
1342 496
    {
1343 496
        if ($cell->getValue() instanceof RichText) {
1344
            $cellData .= $this->generateRowCellDataValueRich($cell->getValue());
1345 496
        } else {
1346 86
            $origData = $this->preCalculateFormulas ? $cell->getCalculatedValue() : $cell->getValue();
1347
            $origData2 = $this->preCalculateFormulas ? $cell->getCalculatedValueString() : $cell->getValueString();
1348 496
            $formatCode = $worksheet->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())->getNumberFormat()->getFormatCode();
1349 1
1350 496
            $cellData = NumberFormat::toFormattedString(
1351 1
                $origData2,
1352
                $formatCode ?? NumberFormat::FORMAT_GENERAL,
1353
                [$this, 'formatColor']
1354
            );
1355
1356 498
            if ($cellData === $origData) {
1357
                $cellData = htmlspecialchars($cellData, Settings::htmlEntityFlags());
1358 498
            }
1359 498
            if ($worksheet->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())->getFont()->getSuperscript()) {
1360 496
                $cellData = '<sup>' . $cellData . '</sup>';
1361
            } elseif ($worksheet->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())->getFont()->getSubscript()) {
1362
                $cellData = '<sub>' . $cellData . '</sub>';
1363
            }
1364
        }
1365
    }
1366 496
1367
    private function generateRowCellData(Worksheet $worksheet, null|Cell|string $cell, array|string &$cssClass): string
1368
    {
1369
        $cellData = '&nbsp;';
1370 496
        if ($cell instanceof Cell) {
1371
            $cellData = '';
1372
            // Don't know what this does, and no test cases.
1373 496
            //if ($cell->getParent() === null) {
1374
            //    $cell->attach($worksheet);
1375
            //}
1376 496
            // Value
1377 490
            $this->generateRowCellDataValue($worksheet, $cell, $cellData);
1378 490
1379 11
            // Converts the cell content so that spaces occuring at beginning of each new line are replaced by &nbsp;
1380 11
            // Example: "  Hello\n to the world" is converted to "&nbsp;&nbsp;Hello\n&nbsp;to the world"
1381 11
            $cellData = (string) preg_replace('/(?m)(?:^|\\G) /', '&nbsp;', $cellData);
1382 11
1383 11
            // convert newline "\n" to '<br>'
1384
            $cellData = nl2br($cellData);
1385
1386
            // Extend CSS class?
1387 11
            if (!$this->useInlineCss && is_string($cssClass)) {
1388
                $cssClass .= ' style' . $cell->getXfIndex();
1389 11
                $cssClass .= ' ' . $cell->getDataType();
1390 11
            } elseif (is_array($cssClass)) {
1391
                $index = $cell->getXfIndex();
1392 11
                $styleIndex = 'td.style' . $index . ', th.style' . $index;
1393
                if (isset($this->cssStyles[$styleIndex])) {
1394
                    $cssClass = array_merge($cssClass, $this->cssStyles[$styleIndex]);
1395
                }
1396
1397 44
                // General horizontal alignment: Actual horizontal alignment depends on dataType
1398 40
                $sharedStyle = $worksheet->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex());
1399
                if (
1400
                    $sharedStyle->getAlignment()->getHorizontal() == Alignment::HORIZONTAL_GENERAL
1401
                    && isset($this->cssStyles['.' . $cell->getDataType()]['text-align'])
1402 498
                ) {
1403
                    $cssClass['text-align'] = $this->cssStyles['.' . $cell->getDataType()]['text-align'];
1404
                }
1405 498
            }
1406
        } else {
1407 498
            // Use default borders for empty cell
1408
            if (is_string($cssClass)) {
1409
                $cssClass .= ' style0';
1410 498
            }
1411
        }
1412 498
1413 498
        return $cellData;
1414
    }
1415 498
1416
    private function generateRowIncludeCharts(Worksheet $worksheet, string $coordinate): string
1417
    {
1418 498
        return $this->includeCharts ? $this->writeChartInCell($worksheet, $coordinate) : '';
1419
    }
1420
1421
    private function generateRowSpans(string $html, int $rowSpan, int $colSpan): string
1422
    {
1423
        $html .= ($colSpan > 1) ? (' colspan="' . $colSpan . '"') : '';
1424
        $html .= ($rowSpan > 1) ? (' rowspan="' . $rowSpan . '"') : '';
1425
1426
        return $html;
1427
    }
1428
1429
    private function generateRowWriteCell(
1430
        string &$html,
1431
        Worksheet $worksheet,
1432 498
        string $coordinate,
1433
        string $cellType,
1434 498
        string $cellData,
1435
        int $colSpan,
1436 498
        int $rowSpan,
1437 498
        array|string $cssClass,
1438 469
        int $colNum,
1439 469
        int $sheetIndex,
1440 18
        int $row
1441
    ): void {
1442
        // Image?
1443
        $htmlx = $this->writeImageInCell($coordinate);
1444
        // Chart?
1445
        $htmlx .= $this->generateRowIncludeCharts($worksheet, $coordinate);
1446 37
        // Column start
1447 12
        $html .= '            <' . $cellType;
1448
        if (!$this->useInlineCss && !$this->isPdf && is_string($cssClass)) {
1449 26
            $html .= ' class="' . $cssClass . '"';
1450 26
            if ($htmlx) {
1451
                $html .= " style='position: relative;'";
1452 26
            }
1453
        } else {
1454 37
            //** Necessary redundant code for the sake of \PhpOffice\PhpSpreadsheet\Writer\Pdf **
1455 37
            // We must explicitly write the width of the <td> element because TCPDF
1456 37
            // does not recognize e.g. <col style="width:42pt">
1457 37
            if ($this->useInlineCss) {
1458 37
                $xcssClass = is_array($cssClass) ? $cssClass : [];
1459 37
            } else {
1460
                if (is_string($cssClass)) {
1461
                    $html .= ' class="' . $cssClass . '"';
1462 37
                }
1463
                $xcssClass = [];
1464
            }
1465 37
            $width = 0;
1466 5
            $i = $colNum - 1;
1467 5
            $e = $colNum + $colSpan - 1;
1468
            while ($i++ < $e) {
1469
                if (isset($this->columnWidths[$sheetIndex][$i])) {
1470 37
                    $width += $this->columnWidths[$sheetIndex][$i];
1471 12
                }
1472 12
            }
1473 11
            $xcssClass['width'] = (string) $width . 'pt';
1474
            // We must also explicitly write the height of the <td> element because TCPDF
1475
            // does not recognize e.g. <tr style="height:50pt">
1476
            if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $row]['height'])) {
1477
                $height = $this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $row]['height'];
1478 37
                $xcssClass['height'] = $height;
1479 9
            }
1480
            //** end of redundant code **
1481 37
            if ($this->useInlineCss) {
1482 37
                foreach (['border-top', 'border-bottom', 'border-right', 'border-left'] as $borderType) {
1483 12
                    if (($xcssClass[$borderType] ?? '') === 'none #000000') {
1484
                        unset($xcssClass[$borderType]);
1485
                    }
1486 498
                }
1487
            }
1488 498
1489 498
            if ($htmlx) {
1490
                $xcssClass['position'] = 'relative';
1491 498
            }
1492
            $html .= ' style="' . $this->assembleCSS($xcssClass) . '"';
1493
            if ($this->useInlineCss) {
1494 498
                $html .= ' class="gridlines gridlinesp"';
1495
            }
1496
        }
1497 498
        $html = $this->generateRowSpans($html, $rowSpan, $colSpan);
1498
1499
        $html .= '>';
1500
        $html .= $htmlx;
1501
1502
        $html .= $this->writeComment($worksheet, $coordinate);
1503
1504
        // Cell data
1505
        $html .= $cellData;
1506
1507 498
        // Column end
1508
        $html .= '</' . $cellType . '>' . PHP_EOL;
1509
    }
1510 498
1511 498
    /**
1512
     * Generate row.
1513
     *
1514 498
     * @param array $values Array containing cells in a row
1515 498
     * @param int $row Row number (0-based)
1516 498
     * @param string $cellType eg: 'td'
1517
     */
1518
    private function generateRow(Worksheet $worksheet, array $values, int $row, string $cellType): string
1519 498
    {
1520
        // Sheet index
1521
        $sheetIndex = $worksheet->getParentOrThrow()->getIndex($worksheet);
1522 498
        $html = $this->generateRowStart($worksheet, $sheetIndex, $row);
1523 9
1524
        // Write cells
1525
        $colNum = 0;
1526
        foreach ($values as $cellAddress) {
1527 498
            [$cell, $cssClass, $coordinate] = $this->generateRowCellCss($worksheet, $cellAddress, $row, $colNum);
1528 498
1529
            // Cell Data
1530
            $cellData = $this->generateRowCellData($worksheet, $cell, $cssClass);
1531 498
1532 498
            // Hyperlink?
1533 498
            if ($worksheet->hyperlinkExists($coordinate) && !$worksheet->getHyperlink($coordinate)->isInternal()) {
1534 15
                $url = $worksheet->getHyperlink($coordinate)->getUrl();
1535 15
                $urldecode = strtolower(html_entity_decode(trim($url), encoding: 'UTF-8'));
1536 15
                $parseScheme = preg_match('/^(\\w+):/', $urldecode, $matches);
1537
                if ($parseScheme === 1 && !in_array($matches[1], ['http', 'https', 'file', 'ftp', 's3'], true)) {
1538
                    $cellData = htmlspecialchars($url, Settings::htmlEntityFlags());
1539
                } else {
1540 15
                    $cellData = '<a href="' . htmlspecialchars($url, Settings::htmlEntityFlags()) . '" title="' . htmlspecialchars($worksheet->getHyperlink($coordinate)->getTooltip(), Settings::htmlEntityFlags()) . '">' . $cellData . '</a>';
1541 15
                }
1542 13
            }
1543
1544 3
            // Should the cell be written or is it swallowed by a rowspan or colspan?
1545 3
            $writeCell = !(isset($this->isSpannedCell[$worksheet->getParentOrThrow()->getIndex($worksheet)][$row + 1][$colNum])
1546 3
                && $this->isSpannedCell[$worksheet->getParentOrThrow()->getIndex($worksheet)][$row + 1][$colNum]);
1547 3
1548 1
            // Colspan and Rowspan
1549
            $colSpan = 1;
1550
            $rowSpan = 1;
1551
            if (isset($this->isBaseCell[$worksheet->getParentOrThrow()->getIndex($worksheet)][$row + 1][$colNum])) {
1552
                $spans = $this->isBaseCell[$worksheet->getParentOrThrow()->getIndex($worksheet)][$row + 1][$colNum];
1553
                $rowSpan = $spans['rowspan'];
1554
                $colSpan = $spans['colspan'];
1555 498
1556 498
                //    Also apply style from last cell in merge to fix borders -
1557
                //        relies on !important for non-none border declarations in createCSSStyleBorder
1558
                $endCellCoord = Coordinate::stringFromColumnIndex($colNum + $colSpan) . ($row + $rowSpan);
1559
                if (!$this->useInlineCss) {
1560 498
                    $cssClass .= ' style' . $worksheet->getCell($endCellCoord)->getXfIndex();
1561
                } else {
1562
                    $endBorders = $this->spreadsheet->getCellXfByIndex($worksheet->getCell($endCellCoord)->getXfIndex())->getBorders();
1563
                    $altBorders = $this->createCSSStyleBorders($endBorders);
1564 498
                    foreach ($altBorders as $altKey => $altValue) {
1565
                        if (str_contains($altValue, '!important')) {
1566
                            $cssClass[$altKey] = $altValue;
1567 498
                        }
1568
                    }
1569
                }
1570
            }
1571
1572
            // Write
1573 500
            if ($writeCell) {
1574
                $this->generateRowWriteCell($html, $worksheet, $coordinate, $cellType, $cellData, $colSpan, $rowSpan, $cssClass, $colNum, $sheetIndex, $row);
1575 500
            }
1576 500
1577 500
            // Next column
1578
            ++$colNum;
1579 500
        }
1580
1581 500
        // Write row end
1582
        $html .= '          </tr>' . PHP_EOL;
1583
1584
        // Return
1585
        return $html;
1586
    }
1587 14
1588
    /**
1589 14
     * Takes array where of CSS properties / values and converts to CSS string.
1590
     */
1591
    private function assembleCSS(array $values = []): string
1592
    {
1593
        $pairs = [];
1594
        foreach ($values as $property => $value) {
1595
            $pairs[] = $property . ':' . $value;
1596
        }
1597 1
        $string = implode('; ', $pairs);
1598
1599 1
        return $string;
1600
    }
1601 1
1602
    /**
1603
     * Get images root.
1604
     */
1605
    public function getImagesRoot(): string
1606
    {
1607 5
        return $this->imagesRoot;
1608
    }
1609 5
1610
    /**
1611
     * Set images root.
1612
     *
1613
     * @return $this
1614
     */
1615
    public function setImagesRoot(string $imagesRoot): static
1616
    {
1617 3
        $this->imagesRoot = $imagesRoot;
1618
1619 3
        return $this;
1620
    }
1621 3
1622
    /**
1623
     * Get embed images.
1624
     */
1625
    public function getEmbedImages(): bool
1626
    {
1627 1
        return $this->embedImages;
1628
    }
1629 1
1630
    /**
1631
     * Set embed images.
1632
     *
1633
     * @return $this
1634
     */
1635
    public function setEmbedImages(bool $embedImages): static
1636
    {
1637 13
        $this->embedImages = $embedImages;
1638
1639 13
        return $this;
1640
    }
1641 13
1642
    /**
1643
     * Get use inline CSS?
1644
     */
1645
    public function getUseInlineCss(): bool
1646
    {
1647
        return $this->useInlineCss;
1648
    }
1649
1650 392
    /**
1651
     * Set use inline CSS?
1652
     *
1653 392
     * @return $this
1654 392
     */
1655
    public function setUseInlineCss(bool $useInlineCss): static
1656 392
    {
1657 392
        $this->useInlineCss = $useInlineCss;
1658 17
1659 17
        return $this;
1660
    }
1661
1662
    /**
1663 392
     * Add color to formatted string as inline style.
1664
     *
1665
     * @param string $value Plain formatted value without color
1666 392
     * @param string $format Format code
1667 17
     */
1668
    public function formatColor(string $value, string $format): string
1669
    {
1670 392
        // Color information, e.g. [Red] is always at the beginning
1671
        $color = null; // initialize
1672
        $matches = [];
1673
1674
        $color_regex = '/^\\[[a-zA-Z]+\\]/';
1675
        if (preg_match($color_regex, $format, $matches)) {
1676 500
            $color = str_replace(['[', ']'], '', $matches[0]);
1677
            $color = strtolower($color);
1678 500
        }
1679 500
1680
        // convert to PCDATA
1681
        $result = htmlspecialchars($value, Settings::htmlEntityFlags());
1682
1683
        // color span tag
1684 500
        if ($color !== null) {
1685 500
            $result = '<span style="color:' . $color . '">' . $result . '</span>';
1686
        }
1687 500
1688 500
        return $result;
1689
    }
1690 500
1691
    /**
1692
     * Calculate information about HTML colspan and rowspan which is not always the same as Excel's.
1693 500
     */
1694 15
    private function calculateSpans(): void
1695 15
    {
1696 15
        if ($this->spansAreCalculated) {
1697
            return;
1698 15
        }
1699 15
        // Identify all cells that should be omitted in HTML due to cell merge.
1700
        // In HTML only the upper-left cell should be written and it should have
1701 15
        //   appropriate rowspan / colspan attribute
1702 15
        $sheetIndexes = $this->sheetIndex !== null
1703
            ? [$this->sheetIndex] : range(0, $this->spreadsheet->getSheetCount() - 1);
1704
1705 15
        foreach ($sheetIndexes as $sheetIndex) {
1706 15
            $sheet = $this->spreadsheet->getSheet($sheetIndex);
1707
1708 15
            $candidateSpannedRow = [];
1709
1710 15
            // loop through all Excel merged cells
1711 15
            foreach ($sheet->getMergeCells() as $cells) {
1712 15
                [$cells] = Coordinate::splitRange($cells);
1713
                $first = $cells[0];
1714 15
                $last = $cells[1];
1715 15
1716 15
                [$fc, $fr] = Coordinate::indexesFromString($first);
1717
                $fc = $fc - 1;
1718
1719 15
                [$lc, $lr] = Coordinate::indexesFromString($last);
1720 15
                $lc = $lc - 1;
1721 15
1722 15
                // loop through the individual cells in the individual merge
1723 15
                $r = $fr - 1;
1724 15
                while ($r++ < $lr) {
1725
                    // also, flag this row as a HTML row that is candidate to be omitted
1726
                    $candidateSpannedRow[$r] = $r;
1727
1728
                    $c = $fc - 1;
1729
                    while ($c++ < $lc) {
1730 500
                        if (!($c == $fc && $r == $fr)) {
1731
                            // not the upper-left cell (should not be written in HTML)
1732
                            $this->isSpannedCell[$sheetIndex][$r][$c] = [
1733
                                'baseCell' => [$fr, $fc],
1734
                            ];
1735
                        } else {
1736 500
                            // upper-left is the base cell that should hold the colspan/rowspan attribute
1737
                            $this->isBaseCell[$sheetIndex][$r][$c] = [
1738
                                'xlrowspan' => $lr - $fr + 1, // Excel rowspan
1739 500
                                'rowspan' => $lr - $fr + 1, // HTML rowspan, value may change
1740
                                'xlcolspan' => $lc - $fc + 1, // Excel colspan
1741
                                'colspan' => $lc - $fc + 1, // HTML colspan, value may change
1742
                            ];
1743 500
                        }
1744 500
                    }
1745 15
                }
1746 15
            }
1747 8
1748
            $this->calculateSpansOmitRows($sheet, $sheetIndex, $candidateSpannedRow);
1749
1750
            // TODO: Same for columns
1751
        }
1752
1753 500
        // We have calculated the spans
1754 8
        $this->spansAreCalculated = true;
1755 8
    }
1756 8
1757 8
    private function calculateSpansOmitRows(Worksheet $sheet, int $sheetIndex, array $candidateSpannedRow): void
1758 8
    {
1759 8
        // Identify which rows should be omitted in HTML. These are the rows where all the cells
1760
        //   participate in a merge and the where base cells are somewhere above.
1761 8
        $countColumns = Coordinate::columnIndexFromString($sheet->getHighestColumn());
1762
        foreach ($candidateSpannedRow as $rowIndex) {
1763 8
            if (isset($this->isSpannedCell[$sheetIndex][$rowIndex])) {
1764 8
                if (count($this->isSpannedCell[$sheetIndex][$rowIndex]) == $countColumns) {
1765
                    $this->isSpannedRow[$sheetIndex][$rowIndex] = $rowIndex;
1766
                }
1767
            }
1768
        }
1769
1770
        // For each of the omitted rows we found above, the affected rowspans should be subtracted by 1
1771
        if (isset($this->isSpannedRow[$sheetIndex])) {
1772
            foreach ($this->isSpannedRow[$sheetIndex] as $rowIndex) {
1773
                $adjustedBaseCells = [];
1774
                $c = -1;
1775
                $e = $countColumns - 1;
1776 498
                while ($c++ < $e) {
1777
                    $baseCell = $this->isSpannedCell[$sheetIndex][$rowIndex][$c]['baseCell'];
1778 498
1779 498
                    if (!in_array($baseCell, $adjustedBaseCells, true)) {
1780 23
                        // subtract rowspan by 1
1781 23
                        --$this->isBaseCell[$sheetIndex][$baseCell[0]][$baseCell[1]]['rowspan'];
1782 23
                        $adjustedBaseCells[] = $baseCell;
1783 23
                    }
1784 23
                }
1785 2
            }
1786
        }
1787 23
    }
1788 23
1789 23
    /**
1790 23
     * Write a comment in the same format as LibreOffice.
1791
     *
1792
     * @see https://github.com/LibreOffice/core/blob/9fc9bf3240f8c62ad7859947ab8a033ac1fe93fa/sc/source/filter/html/htmlexp.cxx#L1073-L1092
1793
     */
1794 498
    private function writeComment(Worksheet $worksheet, string $coordinate): string
1795
    {
1796
        $result = '';
1797 473
        if (!$this->isPdf && isset($worksheet->getComments()[$coordinate])) {
1798
            $sanitizedString = $this->generateRowCellDataValueRich($worksheet->getComment($coordinate)->getText());
1799
            $dir = ($worksheet->getComment($coordinate)->getTextboxDirection() === Comment::TEXTBOX_DIRECTION_RTL) ? ' dir="rtl"' : '';
1800 473
            $align = strtolower($worksheet->getComment($coordinate)->getAlignment());
1801
            $alignment = Alignment::HORIZONTAL_ALIGNMENT_FOR_HTML[$align] ?? '';
1802
            if ($alignment !== '') {
1803
                $alignment = " style=\"text-align:$alignment\"";
1804
            }
1805
            if ($sanitizedString !== '') {
1806 500
                $result .= '<a class="comment-indicator"></a>';
1807
                $result .= "<div class=\"comment\"$dir$alignment>" . $sanitizedString . '</div>';
1808
                $result .= PHP_EOL;
1809 500
            }
1810
        }
1811
1812 500
        return $result;
1813 500
    }
1814 14
1815
    public function getOrientation(): ?string
1816 493
    {
1817
        // Expect Pdf classes to override this method.
1818
        return $this->isPdf ? PageSetup::ORIENTATION_PORTRAIT : null;
1819
    }
1820 500
1821
    /**
1822
     * Generate @page declarations.
1823 500
     */
1824 500
    private function generatePageDeclarations(bool $generateSurroundingHTML): string
1825 500
    {
1826 500
        // Ensure that Spans have been calculated?
1827 500
        $this->calculateSpans();
1828 500
1829 500
        // Fetch sheets
1830 500
        $sheets = [];
1831 500
        if ($this->sheetIndex === null) {
1832 500
            $sheets = $this->spreadsheet->getAllSheets();
1833 500
        } else {
1834 500
            $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex);
1835 500
        }
1836 9
1837 492
        // Construct HTML
1838 12
        $htmlPage = $generateSurroundingHTML ? ('<style type="text/css">' . PHP_EOL) : '';
1839
1840 500
        // Loop all sheets
1841 500
        $sheetId = 0;
1842
        foreach ($sheets as $worksheet) {
1843 500
            $htmlPage .= "@page page$sheetId { ";
1844 500
            $left = StringHelper::formatNumber($worksheet->getPageMargins()->getLeft()) . 'in; ';
1845 500
            $htmlPage .= 'margin-left: ' . $left;
1846 500
            $right = StringHelper::FormatNumber($worksheet->getPageMargins()->getRight()) . 'in; ';
1847 500
            $htmlPage .= 'margin-right: ' . $right;
1848 500
            $top = StringHelper::FormatNumber($worksheet->getPageMargins()->getTop()) . 'in; ';
1849 500
            $htmlPage .= 'margin-top: ' . $top;
1850 500
            $bottom = StringHelper::FormatNumber($worksheet->getPageMargins()->getBottom()) . 'in; ';
1851 500
            $htmlPage .= 'margin-bottom: ' . $bottom;
1852 500
            $orientation = $this->getOrientation() ?? $worksheet->getPageSetup()->getOrientation();
1853 500
            if ($orientation === PageSetup::ORIENTATION_LANDSCAPE) {
1854 500
                $htmlPage .= 'size: landscape; ';
1855 500
            } elseif ($orientation === PageSetup::ORIENTATION_PORTRAIT) {
1856 500
                $htmlPage .= 'size: portrait; ';
1857 500
            }
1858 500
            $htmlPage .= '}' . PHP_EOL;
1859 500
            ++$sheetId;
1860 500
        }
1861
        $htmlPage .= implode(PHP_EOL, [
1862 500
            '.navigation {page-break-after: always;}',
1863
            '.scrpgbrk, div + div {page-break-before: always;}',
1864
            '@media screen {',
1865 498
            '  .gridlines td {border: 1px solid black;}',
1866
            '  .gridlines th {border: 1px solid black;}',
1867 498
            '  body>div {margin-top: 5px;}',
1868 478
            '  body>div:first-child {margin-top: 0;}',
1869
            '  .scrpgbrk {margin-top: 1px;}',
1870
            '}',
1871 25
            '@media print {',
1872
            '  .gridlinesp td {border: 1px solid black;}',
1873
            '  .gridlinesp th {border: 1px solid black;}',
1874 500
            '  .navigation {display: none;}',
1875
            '}',
1876 500
            '',
1877 480
        ]);
1878
        $htmlPage .= $generateSurroundingHTML ? ('</style>' . PHP_EOL) : '';
1879 25
1880 24
        return $htmlPage;
1881
    }
1882
1883 9
    private function shouldGenerateRow(Worksheet $sheet, int $row): bool
1884
    {
1885
        if (!($this instanceof Pdf\Mpdf || $this instanceof Pdf\Tcpdf)) {
1886
            return true;
1887
        }
1888
1889
        return $sheet->isRowVisible($row);
1890
    }
1891
1892
    private function shouldGenerateColumn(Worksheet $sheet, string $colStr): bool
1893
    {
1894
        if (!($this instanceof Pdf\Mpdf || $this instanceof Pdf\Tcpdf)) {
1895
            return true;
1896
        }
1897
        if (!$sheet->columnDimensionExists($colStr)) {
1898
            return true;
1899
        }
1900
1901
        return $sheet->getColumnDimension($colStr)->getVisible();
1902
    }
1903
}
1904