Passed
Pull Request — master (#4142)
by Owen
20:09
created

Html::generateRowWriteCell()   C

Complexity

Conditions 16
Paths 66

Size

Total Lines 80
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 37
CRAP Score 16

Importance

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