Passed
Push — master ( 41da84...3cb7b3 )
by
unknown
13:24 queued 13s
created

Html::flushCell()   B

Complexity

Conditions 9
Paths 21

Size

Total Lines 45
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 9.0086

Importance

Changes 0
Metric Value
eloc 23
dl 0
loc 45
ccs 20
cts 21
cp 0.9524
rs 8.0555
c 0
b 0
f 0
cc 9
nc 21
nop 5
crap 9.0086
1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Reader;
4
5
use DOMAttr;
6
use DOMDocument;
7
use DOMElement;
8
use DOMNode;
9
use DOMText;
10
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
11
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
12
use PhpOffice\PhpSpreadsheet\Cell\DataType;
13
use PhpOffice\PhpSpreadsheet\Comment;
14
use PhpOffice\PhpSpreadsheet\Document\Properties;
15
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
16
use PhpOffice\PhpSpreadsheet\Helper\Dimension as CssDimension;
17
use PhpOffice\PhpSpreadsheet\Helper\Html as HelperHtml;
18
use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner;
19
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
20
use PhpOffice\PhpSpreadsheet\Spreadsheet;
21
use PhpOffice\PhpSpreadsheet\Style\Border;
22
use PhpOffice\PhpSpreadsheet\Style\Color;
23
use PhpOffice\PhpSpreadsheet\Style\Fill;
24
use PhpOffice\PhpSpreadsheet\Style\Font;
25
use PhpOffice\PhpSpreadsheet\Style\Style;
26
use PhpOffice\PhpSpreadsheet\Worksheet\Drawing;
27
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
28
use Throwable;
29
30
class Html extends BaseReader
31
{
32
    /**
33
     * Sample size to read to determine if it's HTML or not.
34
     */
35
    const TEST_SAMPLE_SIZE = 2048;
36
37
    private const STARTS_WITH_BOM = '/^(?:\xfe\xff|\xff\xfe|\xEF\xBB\xBF)/';
38
39
    private const DECLARES_CHARSET = '/\bcharset=/i';
40
41
    /**
42
     * Input encoding.
43
     */
44
    protected string $inputEncoding = 'ANSI';
45
46
    /**
47
     * Sheet index to read.
48
     */
49
    protected int $sheetIndex = 0;
50
51
    /**
52
     * Formats.
53
     */
54
    protected array $formats = [
55
        'h1' => [
56
            'font' => [
57
                'bold' => true,
58
                'size' => 24,
59
            ],
60
        ], //    Bold, 24pt
61
        'h2' => [
62
            'font' => [
63
                'bold' => true,
64
                'size' => 18,
65
            ],
66
        ], //    Bold, 18pt
67
        'h3' => [
68
            'font' => [
69
                'bold' => true,
70
                'size' => 13.5,
71
            ],
72
        ], //    Bold, 13.5pt
73
        'h4' => [
74
            'font' => [
75
                'bold' => true,
76
                'size' => 12,
77
            ],
78
        ], //    Bold, 12pt
79
        'h5' => [
80
            'font' => [
81
                'bold' => true,
82
                'size' => 10,
83
            ],
84
        ], //    Bold, 10pt
85
        'h6' => [
86
            'font' => [
87
                'bold' => true,
88
                'size' => 7.5,
89
            ],
90
        ], //    Bold, 7.5pt
91
        'a' => [
92
            'font' => [
93
                'underline' => true,
94
                'color' => [
95
                    'argb' => Color::COLOR_BLUE,
96
                ],
97
            ],
98
        ], //    Blue underlined
99
        'hr' => [
100
            'borders' => [
101
                'bottom' => [
102
                    'borderStyle' => Border::BORDER_THIN,
103
                    'color' => [
104
                        Color::COLOR_BLACK,
105
                    ],
106
                ],
107
            ],
108
        ], //    Bottom border
109
        'strong' => [
110
            'font' => [
111
                'bold' => true,
112
            ],
113
        ], //    Bold
114
        'b' => [
115
            'font' => [
116
                'bold' => true,
117
            ],
118
        ], //    Bold
119
        'i' => [
120
            'font' => [
121
                'italic' => true,
122
            ],
123
        ], //    Italic
124
        'em' => [
125
            'font' => [
126
                'italic' => true,
127
            ],
128
        ], //    Italic
129
    ];
130
131
    protected array $rowspan = [];
132
133
    /**
134
     * Create a new HTML Reader instance.
135
     */
136 525
    public function __construct()
137
    {
138 525
        parent::__construct();
139 525
        $this->securityScanner = XmlScanner::getInstance($this);
140
    }
141
142
    /**
143
     * Validate that the current file is an HTML file.
144
     */
145 503
    public function canRead(string $filename): bool
146
    {
147
        // Check if file exists
148
        try {
149 503
            $this->openFile($filename);
150 1
        } catch (Exception) {
151 1
            return false;
152
        }
153
154 502
        $beginning = preg_replace(self::STARTS_WITH_BOM, '', $this->readBeginning()) ?? '';
155
156 502
        $startWithTag = self::startsWithTag($beginning);
157 502
        $containsTags = self::containsTags($beginning);
158 502
        $endsWithTag = self::endsWithTag($this->readEnding());
159
160 502
        fclose($this->fileHandle);
161
162 502
        return $startWithTag && $containsTags && $endsWithTag;
163
    }
164
165 502
    private function readBeginning(): string
166
    {
167 502
        fseek($this->fileHandle, 0);
168
169 502
        return (string) fread($this->fileHandle, self::TEST_SAMPLE_SIZE);
170
    }
171
172 502
    private function readEnding(): string
173
    {
174 502
        $meta = stream_get_meta_data($this->fileHandle);
175
        // Phpstan incorrectly flags following line for Php8.2-, corrected in 8.3
176 502
        $filename = $meta['uri']; //@phpstan-ignore-line
177
178 502
        clearstatcache(true, $filename);
179 502
        $size = (int) filesize($filename);
180 502
        if ($size === 0) {
181 1
            return '';
182
        }
183
184 501
        $blockSize = self::TEST_SAMPLE_SIZE;
185 501
        if ($size < $blockSize) {
186 50
            $blockSize = $size;
187
        }
188
189 501
        fseek($this->fileHandle, $size - $blockSize);
190
191 501
        return (string) fread($this->fileHandle, $blockSize);
192
    }
193
194 502
    private static function startsWithTag(string $data): bool
195
    {
196 502
        return str_starts_with(trim($data), '<');
197
    }
198
199 502
    private static function endsWithTag(string $data): bool
200
    {
201 502
        return str_ends_with(trim($data), '>');
202
    }
203
204 502
    private static function containsTags(string $data): bool
205
    {
206 502
        return strlen($data) !== strlen(strip_tags($data));
207
    }
208
209
    /**
210
     * Loads Spreadsheet from file.
211
     */
212 484
    public function loadSpreadsheetFromFile(string $filename): Spreadsheet
213
    {
214 484
        $spreadsheet = $this->newSpreadsheet();
215 484
        $spreadsheet->setValueBinder($this->valueBinder);
216
217
        // Load into this instance
218 484
        return $this->loadIntoExisting($filename, $spreadsheet);
219
    }
220
221
    //    Data Array used for testing only, should write to Spreadsheet object on completion of tests
222
223
    protected array $dataArray = [];
224
225
    protected int $tableLevel = 0;
226
227
    protected array $nestedColumn = ['A'];
228
229 499
    protected function setTableStartColumn(string $column): string
230
    {
231 499
        if ($this->tableLevel == 0) {
232 499
            $column = 'A';
233
        }
234 499
        ++$this->tableLevel;
235 499
        $this->nestedColumn[$this->tableLevel] = $column;
236
237 499
        return $this->nestedColumn[$this->tableLevel];
238
    }
239
240 495
    protected function getTableStartColumn(): string
241
    {
242 495
        return $this->nestedColumn[$this->tableLevel];
243
    }
244
245 492
    protected function releaseTableStartColumn(): string
246
    {
247 492
        --$this->tableLevel;
248
249 492
        return array_pop($this->nestedColumn);
250
    }
251
252
    /**
253
     * Flush cell.
254
     *
255
     * @param-out string $cellContent In one case, it can be bool
256
     */
257 500
    protected function flushCell(Worksheet $sheet, string $column, int|string $row, mixed &$cellContent, array $attributeArray): void
258
    {
259 500
        if (is_string($cellContent)) {
260
            //    Simple String content
261 500
            if (trim($cellContent) > '') {
262
                //    Only actually write it if there's content in the string
263
                //    Write to worksheet to be done here...
264
                //    ... we return the cell, so we can mess about with styles more easily
265
266
                // Set cell value explicitly if there is data-type attribute
267 480
                if (isset($attributeArray['data-type'])) {
268 5
                    $datatype = $attributeArray['data-type'];
269 5
                    if (in_array($datatype, [DataType::TYPE_STRING, DataType::TYPE_STRING2, DataType::TYPE_INLINE])) {
270
                        //Prevent to Excel treat string with beginning equal sign or convert big numbers to scientific number
271 5
                        if (str_starts_with($cellContent, '=')) {
272 1
                            $sheet->getCell($column . $row)
273 1
                                ->getStyle()
274 1
                                ->setQuotePrefix(true);
275
                        }
276
                    }
277 5
                    if ($datatype === DataType::TYPE_BOOL) {
278
                        // This is the case where we can set cellContent to bool rather than string
279 5
                        $cellContent = self::convertBoolean($cellContent); //* @phpstan-ignore-line
280 5
                        if (!is_bool($cellContent)) {
281 1
                            $attributeArray['data-type'] = DataType::TYPE_STRING;
282
                        }
283
                    }
284
285
                    //catching the Exception and ignoring the invalid data types
286
                    try {
287 5
                        $sheet->setCellValueExplicit($column . $row, $cellContent, $attributeArray['data-type']);
288 1
                    } catch (SpreadsheetException) {
289 1
                        $sheet->setCellValue($column . $row, $cellContent);
290
                    }
291
                } else {
292 479
                    $sheet->setCellValue($column . $row, $cellContent);
293
                }
294 480
                $this->dataArray[$row][$column] = $cellContent;
295
            }
296
        } else {
297
            //    We have a Rich Text run
298
            //    TODO
299
            $this->dataArray[$row][$column] = 'RICH TEXT: ' . StringHelper::convertToString($cellContent);
300
        }
301 500
        $cellContent = (string) '';
302
    }
303
304
    /** @var array<int, array<int, string>> */
305
    private static array $falseTrueArray = [];
306
307 5
    private static function convertBoolean(?string $cellContent): bool|string
308
    {
309 5
        if ($cellContent === '1') {
310 1
            return true;
311
        }
312 4
        if ($cellContent === '0' || $cellContent === '' || $cellContent === null) {
313
            return false;
314
        }
315 4
        if (empty(self::$falseTrueArray)) {
316 1
            $calc = Calculation::getInstance();
317 1
            self::$falseTrueArray = $calc->getFalseTrueArray();
318
        }
319 4
        if (in_array(mb_strtoupper($cellContent), self::$falseTrueArray[1], true)) {
320 4
            return true;
321
        }
322 4
        if (in_array(mb_strtoupper($cellContent), self::$falseTrueArray[0], true)) {
323 4
            return false;
324
        }
325
326 1
        return $cellContent;
327
    }
328
329 500
    private function processDomElementBody(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child): void
330
    {
331 500
        $attributeArray = [];
332
        /** @var DOMAttr $attribute */
333 500
        foreach ($child->attributes as $attribute) {
334 486
            $attributeArray[$attribute->name] = $attribute->value;
335
        }
336
337 500
        if ($child->nodeName === 'body') {
338 500
            $row = 1;
339 500
            $column = 'A';
340 500
            $cellContent = '';
341 500
            $this->tableLevel = 0;
342 500
            $this->processDomElement($child, $sheet, $row, $column, $cellContent);
343
        } else {
344 500
            $this->processDomElementTitle($sheet, $row, $column, $cellContent, $child, $attributeArray);
345
        }
346
    }
347
348 500
    private function processDomElementTitle(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
349
    {
350 500
        if ($child->nodeName === 'title') {
351 459
            $this->processDomElement($child, $sheet, $row, $column, $cellContent);
352
353
            try {
354 459
                $sheet->setTitle($cellContent, true, true);
355 456
                $sheet->getParent()?->getProperties()?->setTitle($cellContent);
356 3
            } catch (SpreadsheetException) {
357
                // leave default title if too long or illegal chars
358
            }
359 459
            $cellContent = '';
360
        } else {
361 500
            $this->processDomElementSpanEtc($sheet, $row, $column, $cellContent, $child, $attributeArray);
362
        }
363
    }
364
365
    private const SPAN_ETC = ['span', 'div', 'font', 'i', 'em', 'strong', 'b'];
366
367 500
    private function processDomElementSpanEtc(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
368
    {
369 500
        if (in_array((string) $child->nodeName, self::SPAN_ETC, true)) {
370 444
            if (isset($attributeArray['class']) && $attributeArray['class'] === 'comment') {
371 9
                $sheet->getComment($column . $row)
372 9
                    ->getText()
373 9
                    ->createTextRun($child->textContent);
374 9
                if (isset($attributeArray['dir']) && $attributeArray['dir'] === 'rtl') {
375 1
                    $sheet->getComment($column . $row)->setTextboxDirection(Comment::TEXTBOX_DIRECTION_RTL);
376
                }
377 9
                if (isset($attributeArray['style'])) {
378 2
                    $alignStyle = $attributeArray['style'];
379 2
                    if (preg_match('/\btext-align:\s*(left|right|center|justify)\b/', $alignStyle, $matches) === 1) {
380 2
                        $sheet->getComment($column . $row)->setAlignment($matches[1]);
381
                    }
382
                }
383
            } else {
384 444
                $this->processDomElement($child, $sheet, $row, $column, $cellContent);
385
            }
386
387 444
            if (isset($this->formats[$child->nodeName])) {
388 2
                $sheet->getStyle($column . $row)->applyFromArray($this->formats[$child->nodeName]);
389
            }
390
        } else {
391 500
            $this->processDomElementHr($sheet, $row, $column, $cellContent, $child, $attributeArray);
392
        }
393
    }
394
395 500
    private function processDomElementHr(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
396
    {
397 500
        if ($child->nodeName === 'hr') {
398 1
            $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
399 1
            ++$row;
400 1
            if (isset($this->formats[$child->nodeName])) {
401 1
                $sheet->getStyle($column . $row)->applyFromArray($this->formats[$child->nodeName]);
402
            }
403 1
            ++$row;
404
        }
405
        // fall through to br
406 500
        $this->processDomElementBr($sheet, $row, $column, $cellContent, $child, $attributeArray);
407
    }
408
409 500
    private function processDomElementBr(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
410
    {
411 500
        if ($child->nodeName === 'br' || $child->nodeName === 'hr') {
412 4
            if ($this->tableLevel > 0) {
413
                //    If we're inside a table, replace with a newline and set the cell to wrap
414 4
                $cellContent .= "\n";
415 4
                $sheet->getStyle($column . $row)->getAlignment()->setWrapText(true);
416
            } else {
417
                //    Otherwise flush our existing content and move the row cursor on
418 1
                $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
419 1
                ++$row;
420
            }
421
        } else {
422 500
            $this->processDomElementA($sheet, $row, $column, $cellContent, $child, $attributeArray);
423
        }
424
    }
425
426 500
    private function processDomElementA(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
427
    {
428 500
        if ($child->nodeName === 'a') {
429 12
            foreach ($attributeArray as $attributeName => $attributeValue) {
430
                switch ($attributeName) {
431 12
                    case 'href':
432 3
                        $sheet->getCell($column . $row)->getHyperlink()->setUrl($attributeValue);
433 3
                        if (isset($this->formats[$child->nodeName])) {
434 3
                            $sheet->getStyle($column . $row)->applyFromArray($this->formats[$child->nodeName]);
435
                        }
436
437 3
                        break;
438 10
                    case 'class':
439 9
                        if ($attributeValue === 'comment-indicator') {
440 9
                            break; // Ignore - it's just a red square.
441
                        }
442
                }
443
            }
444
            // no idea why this should be needed
445
            //$cellContent .= ' ';
446 12
            $this->processDomElement($child, $sheet, $row, $column, $cellContent);
447
        } else {
448 500
            $this->processDomElementH1Etc($sheet, $row, $column, $cellContent, $child, $attributeArray);
449
        }
450
    }
451
452
    private const H1_ETC = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ol', 'ul', 'p'];
453
454 500
    private function processDomElementH1Etc(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
455
    {
456 500
        if (in_array((string) $child->nodeName, self::H1_ETC, true)) {
457 2
            if ($this->tableLevel > 0) {
458
                //    If we're inside a table, replace with a newline
459 1
                $cellContent .= $cellContent ? "\n" : '';
460 1
                $sheet->getStyle($column . $row)->getAlignment()->setWrapText(true);
461 1
                $this->processDomElement($child, $sheet, $row, $column, $cellContent);
462
            } else {
463 2
                if ($cellContent > '') {
464 1
                    $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
465 1
                    ++$row;
466
                }
467 2
                $this->processDomElement($child, $sheet, $row, $column, $cellContent);
468 2
                $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
469
470 2
                if (isset($this->formats[$child->nodeName])) {
471 1
                    $sheet->getStyle($column . $row)->applyFromArray($this->formats[$child->nodeName]);
472
                }
473
474 2
                ++$row;
475 2
                $column = 'A';
476
            }
477
        } else {
478 500
            $this->processDomElementLi($sheet, $row, $column, $cellContent, $child, $attributeArray);
479
        }
480
    }
481
482 500
    private function processDomElementLi(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
483
    {
484 500
        if ($child->nodeName === 'li') {
485 2
            if ($this->tableLevel > 0) {
486
                //    If we're inside a table, replace with a newline
487 1
                $cellContent .= $cellContent ? "\n" : '';
488 1
                $this->processDomElement($child, $sheet, $row, $column, $cellContent);
489
            } else {
490 2
                if ($cellContent > '') {
491 1
                    $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
492
                }
493 2
                ++$row;
494 2
                $this->processDomElement($child, $sheet, $row, $column, $cellContent);
495 2
                $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
496 2
                $column = 'A';
497
            }
498
        } else {
499 500
            $this->processDomElementImg($sheet, $row, $column, $cellContent, $child, $attributeArray);
500
        }
501
    }
502
503 500
    private function processDomElementImg(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
504
    {
505 500
        if ($child->nodeName === 'img') {
506 17
            $this->insertImage($sheet, $column, $row, $attributeArray);
507
        } else {
508 500
            $this->processDomElementTable($sheet, $row, $column, $cellContent, $child, $attributeArray);
509
        }
510
    }
511
512
    private string $currentColumn = 'A';
513
514 500
    private function processDomElementTable(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
515
    {
516 500
        if ($child->nodeName === 'table') {
517 499
            if (isset($attributeArray['class'])) {
518 444
                $classes = explode(' ', $attributeArray['class']);
519 444
                $sheet->setShowGridlines(in_array('gridlines', $classes, true));
520 444
                $sheet->setPrintGridlines(in_array('gridlinesp', $classes, true));
521
            }
522 499
            $this->currentColumn = 'A';
523 499
            $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
524 499
            $column = $this->setTableStartColumn($column);
525 499
            if ($this->tableLevel > 1 && $row > 1) {
526 2
                --$row;
527
            }
528 499
            $this->processDomElement($child, $sheet, $row, $column, $cellContent);
529 492
            $column = $this->releaseTableStartColumn();
530 492
            if ($this->tableLevel > 1) {
531 2
                ++$column; //* @phpstan-ignore-line
532
            } else {
533 492
                ++$row;
534
            }
535
        } else {
536 500
            $this->processDomElementTr($sheet, $row, $column, $cellContent, $child, $attributeArray);
537
        }
538
    }
539
540 500
    private function processDomElementTr(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
541
    {
542 500
        if ($child->nodeName === 'col') {
543 442
            $this->applyInlineStyle($sheet, -1, $this->currentColumn, $attributeArray);
544 442
            ++$this->currentColumn;
545 500
        } elseif ($child->nodeName === 'tr') {
546 495
            $column = $this->getTableStartColumn();
547 495
            $cellContent = '';
548 495
            $this->processDomElement($child, $sheet, $row, $column, $cellContent);
549
550 488
            if (isset($attributeArray['height'])) {
551 1
                $sheet->getRowDimension($row)->setRowHeight($attributeArray['height']);
552
            }
553
554 488
            ++$row;
555
        } else {
556 500
            $this->processDomElementThTdOther($sheet, $row, $column, $cellContent, $child, $attributeArray);
557
        }
558
    }
559
560 500
    private function processDomElementThTdOther(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
561
    {
562 500
        if ($child->nodeName !== 'td' && $child->nodeName !== 'th') {
563 500
            $this->processDomElement($child, $sheet, $row, $column, $cellContent);
564
        } else {
565 495
            $this->processDomElementThTd($sheet, $row, $column, $cellContent, $child, $attributeArray);
566
        }
567
    }
568
569 488
    private function processDomElementBgcolor(Worksheet $sheet, int $row, string $column, array $attributeArray): void
570
    {
571 488
        if (isset($attributeArray['bgcolor'])) {
572 1
            $sheet->getStyle("$column$row")->applyFromArray(
573 1
                [
574 1
                    'fill' => [
575 1
                        'fillType' => Fill::FILL_SOLID,
576 1
                        'color' => ['rgb' => $this->getStyleColor($attributeArray['bgcolor'])],
577 1
                    ],
578 1
                ]
579 1
            );
580
        }
581
    }
582
583 488
    private function processDomElementWidth(Worksheet $sheet, string $column, array $attributeArray): void
584
    {
585 488
        if (isset($attributeArray['width'])) {
586 1
            $sheet->getColumnDimension($column)->setWidth((new CssDimension($attributeArray['width']))->width());
587
        }
588
    }
589
590 488
    private function processDomElementHeight(Worksheet $sheet, int $row, array $attributeArray): void
591
    {
592 488
        if (isset($attributeArray['height'])) {
593 1
            $sheet->getRowDimension($row)->setRowHeight((new CssDimension($attributeArray['height']))->height());
594
        }
595
    }
596
597 488
    private function processDomElementAlign(Worksheet $sheet, int $row, string $column, array $attributeArray): void
598
    {
599 488
        if (isset($attributeArray['align'])) {
600 1
            $sheet->getStyle($column . $row)->getAlignment()->setHorizontal($attributeArray['align']);
601
        }
602
    }
603
604 488
    private function processDomElementVAlign(Worksheet $sheet, int $row, string $column, array $attributeArray): void
605
    {
606 488
        if (isset($attributeArray['valign'])) {
607 1
            $sheet->getStyle($column . $row)->getAlignment()->setVertical($attributeArray['valign']);
608
        }
609
    }
610
611 488
    private function processDomElementDataFormat(Worksheet $sheet, int $row, string $column, array $attributeArray): void
612
    {
613 488
        if (isset($attributeArray['data-format'])) {
614 1
            $sheet->getStyle($column . $row)->getNumberFormat()->setFormatCode($attributeArray['data-format']);
615
        }
616
    }
617
618 495
    private function processDomElementThTd(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
619
    {
620 495
        while (isset($this->rowspan[$column . $row])) {
621 3
            ++$column; //* @phpstan-ignore-line
622
        }
623
        //* @phpstan-ignore-next-line
624 495
        $this->processDomElement($child, $sheet, $row, $column, $cellContent); // ++$column above confuses Phpstan
625
626
        // apply inline style
627 488
        $this->applyInlineStyle($sheet, $row, $column, $attributeArray);
628
629
        /** @var string $cellContent */
630 488
        $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
631
632 488
        $this->processDomElementBgcolor($sheet, $row, $column, $attributeArray);
633 488
        $this->processDomElementWidth($sheet, $column, $attributeArray);
634 488
        $this->processDomElementHeight($sheet, $row, $attributeArray);
635 488
        $this->processDomElementAlign($sheet, $row, $column, $attributeArray);
636 488
        $this->processDomElementVAlign($sheet, $row, $column, $attributeArray);
637 488
        $this->processDomElementDataFormat($sheet, $row, $column, $attributeArray);
638
639 488
        if (isset($attributeArray['rowspan'], $attributeArray['colspan'])) {
640
            //create merging rowspan and colspan
641 2
            $columnTo = $column;
642 2
            for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) {
643 2
                ++$columnTo;
644
            }
645 2
            $range = $column . $row . ':' . $columnTo . ($row + (int) $attributeArray['rowspan'] - 1);
646 2
            foreach (Coordinate::extractAllCellReferencesInRange($range) as $value) {
647 2
                $this->rowspan[$value] = true;
648
            }
649 2
            $sheet->mergeCells($range);
650
            //* @phpstan-ignore-next-line
651 2
            $column = $columnTo; // ++$columnTo above confuses phpstan
652 488
        } elseif (isset($attributeArray['rowspan'])) {
653
            //create merging rowspan
654 3
            $range = $column . $row . ':' . $column . ($row + (int) $attributeArray['rowspan'] - 1);
655 3
            foreach (Coordinate::extractAllCellReferencesInRange($range) as $value) {
656 3
                $this->rowspan[$value] = true;
657
            }
658 3
            $sheet->mergeCells($range);
659 488
        } elseif (isset($attributeArray['colspan'])) {
660
            //create merging colspan
661 3
            $columnTo = $column;
662 3
            for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) {
663 3
                ++$columnTo;
664
            }
665 3
            $sheet->mergeCells($column . $row . ':' . $columnTo . $row);
666
            //* @phpstan-ignore-next-line
667 3
            $column = $columnTo; // ++$columnTo above confuses phpstan
668
        }
669
670 488
        ++$column;
671
    }
672
673 500
    protected function processDomElement(DOMNode $element, Worksheet $sheet, int &$row, string &$column, string &$cellContent): void
674
    {
675 500
        foreach ($element->childNodes as $child) {
676 500
            if ($child instanceof DOMText) {
677 497
                $domText = (string) preg_replace('/\s+/', ' ', trim($child->nodeValue ?? ''));
678 497
                if ($domText === "\u{a0}") {
679 8
                    $domText = '';
680
                }
681
                //    simply append the text if the cell content is a plain text string
682 497
                $cellContent .= $domText;
683
                //    but if we have a rich text run instead, we need to append it correctly
684
                //    TODO
685 500
            } elseif ($child instanceof DOMElement) {
686 500
                $this->processDomElementBody($sheet, $row, $column, $cellContent, $child);
687
            }
688
        }
689
    }
690
691
    /**
692
     * Loads PhpSpreadsheet from file into PhpSpreadsheet instance.
693
     */
694 484
    public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Spreadsheet
695
    {
696
        // Validate
697 484
        if (!$this->canRead($filename)) {
698 1
            throw new Exception($filename . ' is an Invalid HTML file.');
699
        }
700
701
        // Create a new DOM object
702 483
        $dom = new DOMDocument();
703
704
        // Reload the HTML file into the DOM object
705
        try {
706 483
            $convert = $this->getSecurityScannerOrThrow()->scanFile($filename);
707 482
            $convert = self::replaceNonAsciiIfNeeded($convert);
708 482
            $loaded = ($convert === null) ? false : $dom->loadHTML($convert);
709 2
        } catch (Throwable $e) {
710 2
            $loaded = false;
711
        }
712 483
        if ($loaded === false) {
713 2
            throw new Exception('Failed to load ' . $filename . ' as a DOM Document', 0, $e ?? null);
714
        }
715 481
        self::loadProperties($dom, $spreadsheet);
716
717 481
        return $this->loadDocument($dom, $spreadsheet);
718
    }
719
720 500
    private static function loadProperties(DOMDocument $dom, Spreadsheet $spreadsheet): void
721
    {
722 500
        $properties = $spreadsheet->getProperties();
723 500
        foreach ($dom->getElementsByTagName('meta') as $meta) {
724 452
            $metaContent = (string) $meta->getAttribute('content');
725 452
            if ($metaContent !== '') {
726 444
                $metaName = (string) $meta->getAttribute('name');
727
                switch ($metaName) {
728 444
                    case 'author':
729 442
                        $properties->setCreator($metaContent);
730
731 442
                        break;
732 444
                    case 'category':
733 1
                        $properties->setCategory($metaContent);
734
735 1
                        break;
736 444
                    case 'company':
737 1
                        $properties->setCompany($metaContent);
738
739 1
                        break;
740 444
                    case 'created':
741 442
                        $properties->setCreated($metaContent);
742
743 442
                        break;
744 444
                    case 'description':
745 1
                        $properties->setDescription($metaContent);
746
747 1
                        break;
748 444
                    case 'keywords':
749 1
                        $properties->setKeywords($metaContent);
750
751 1
                        break;
752 444
                    case 'lastModifiedBy':
753 442
                        $properties->setLastModifiedBy($metaContent);
754
755 442
                        break;
756 444
                    case 'manager':
757 1
                        $properties->setManager($metaContent);
758
759 1
                        break;
760 444
                    case 'modified':
761 442
                        $properties->setModified($metaContent);
762
763 442
                        break;
764 444
                    case 'subject':
765 1
                        $properties->setSubject($metaContent);
766
767 1
                        break;
768 444
                    case 'title':
769 440
                        $properties->setTitle($metaContent);
770
771 440
                        break;
772 444
                    case 'viewport':
773 1
                        $properties->setViewport($metaContent);
774
775 1
                        break;
776
                    default:
777 444
                        if (preg_match('/^custom[.](bool|date|float|int|string)[.](.+)$/', $metaName, $matches) === 1) {
778 1
                            match ($matches[1]) {
779 1
                                'bool' => $properties->setCustomProperty($matches[2], (bool) $metaContent, Properties::PROPERTY_TYPE_BOOLEAN),
780 1
                                'float' => $properties->setCustomProperty($matches[2], (float) $metaContent, Properties::PROPERTY_TYPE_FLOAT),
781 1
                                'int' => $properties->setCustomProperty($matches[2], (int) $metaContent, Properties::PROPERTY_TYPE_INTEGER),
782 1
                                'date' => $properties->setCustomProperty($matches[2], $metaContent, Properties::PROPERTY_TYPE_DATE),
783
                                // string
784 1
                                default => $properties->setCustomProperty($matches[2], $metaContent, Properties::PROPERTY_TYPE_STRING),
785 1
                            };
786
                        }
787
                }
788
            }
789
        }
790 500
        if (!empty($dom->baseURI)) {
791 1
            $properties->setHyperlinkBase($dom->baseURI);
792
        }
793
    }
794
795 19
    private static function replaceNonAscii(array $matches): string
796
    {
797 19
        return '&#' . mb_ord($matches[0], 'UTF-8') . ';';
798
    }
799
800 501
    private static function replaceNonAsciiIfNeeded(string $convert): ?string
801
    {
802 501
        if (preg_match(self::STARTS_WITH_BOM, $convert) !== 1 && preg_match(self::DECLARES_CHARSET, $convert) !== 1) {
803 48
            $lowend = "\u{80}";
804 48
            $highend = "\u{10ffff}";
805 48
            $regexp = "/[$lowend-$highend]/u";
806
            /** @var callable $callback */
807 48
            $callback = [self::class, 'replaceNonAscii'];
808 48
            $convert = preg_replace_callback($regexp, $callback, $convert);
809
        }
810
811 501
        return $convert;
812
    }
813
814
    /**
815
     * Spreadsheet from content.
816
     */
817 19
    public function loadFromString(string $content, ?Spreadsheet $spreadsheet = null): Spreadsheet
818
    {
819
        //    Create a new DOM object
820 19
        $dom = new DOMDocument();
821
822
        //    Reload the HTML file into the DOM object
823
        try {
824 19
            $convert = $this->getSecurityScannerOrThrow()->scan($content);
825 19
            $convert = self::replaceNonAsciiIfNeeded($convert);
826 19
            $loaded = ($convert === null) ? false : $dom->loadHTML($convert);
827
        } catch (Throwable $e) {
828
            $loaded = false;
829
        }
830 19
        if ($loaded === false) {
831
            throw new Exception('Failed to load content as a DOM Document', 0, $e ?? null);
832
        }
833 19
        $spreadsheet = $spreadsheet ?? $this->newSpreadsheet();
834 19
        $spreadsheet->setValueBinder($this->valueBinder);
835 19
        self::loadProperties($dom, $spreadsheet);
836
837 19
        return $this->loadDocument($dom, $spreadsheet);
838
    }
839
840
    /**
841
     * Loads PhpSpreadsheet from DOMDocument into PhpSpreadsheet instance.
842
     */
843 500
    private function loadDocument(DOMDocument $document, Spreadsheet $spreadsheet): Spreadsheet
844
    {
845 500
        while ($spreadsheet->getSheetCount() <= $this->sheetIndex) {
846 2
            $spreadsheet->createSheet();
847
        }
848 500
        $spreadsheet->setActiveSheetIndex($this->sheetIndex);
849
850
        // Discard white space
851 500
        $document->preserveWhiteSpace = false;
852
853 500
        $row = 0;
854 500
        $column = 'A';
855 500
        $content = '';
856 500
        $this->rowspan = [];
857 500
        $this->processDomElement($document, $spreadsheet->getActiveSheet(), $row, $column, $content);
858
859
        // Return
860 493
        return $spreadsheet;
861
    }
862
863
    /**
864
     * Get sheet index.
865
     */
866 1
    public function getSheetIndex(): int
867
    {
868 1
        return $this->sheetIndex;
869
    }
870
871
    /**
872
     * Set sheet index.
873
     *
874
     * @param int $sheetIndex Sheet index
875
     *
876
     * @return $this
877
     */
878 2
    public function setSheetIndex(int $sheetIndex): static
879
    {
880 2
        $this->sheetIndex = $sheetIndex;
881
882 2
        return $this;
883
    }
884
885
    /**
886
     * Apply inline css inline style.
887
     *
888
     * NOTES :
889
     * Currently only intended for td & th element,
890
     * and only takes 'background-color' and 'color'; property with HEX color
891
     *
892
     * TODO :
893
     * - Implement to other propertie, such as border
894
     */
895 488
    private function applyInlineStyle(Worksheet &$sheet, int $row, string $column, array $attributeArray): void
896
    {
897 488
        if (!isset($attributeArray['style'])) {
898 482
            return;
899
        }
900
901 16
        if ($row <= 0 || $column === '') {
902 1
            $cellStyle = new Style();
903 16
        } elseif (isset($attributeArray['rowspan'], $attributeArray['colspan'])) {
904 1
            $columnTo = $column;
905 1
            for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) {
906 1
                ++$columnTo;
907
            }
908 1
            $range = $column . $row . ':' . $columnTo . ($row + (int) $attributeArray['rowspan'] - 1);
909 1
            $cellStyle = $sheet->getStyle($range);
910 16
        } elseif (isset($attributeArray['rowspan'])) {
911 1
            $range = $column . $row . ':' . $column . ($row + (int) $attributeArray['rowspan'] - 1);
912 1
            $cellStyle = $sheet->getStyle($range);
913 16
        } elseif (isset($attributeArray['colspan'])) {
914 1
            $columnTo = $column;
915 1
            for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) {
916 1
                ++$columnTo;
917
            }
918 1
            $range = $column . $row . ':' . $columnTo . $row;
919 1
            $cellStyle = $sheet->getStyle($range);
920
        } else {
921 16
            $cellStyle = $sheet->getStyle($column . $row);
922
        }
923
924
        // add color styles (background & text) from dom element,currently support : td & th, using ONLY inline css style with RGB color
925 16
        $styles = explode(';', $attributeArray['style']);
926 16
        foreach ($styles as $st) {
927 16
            $value = explode(':', $st);
928 16
            $styleName = trim($value[0]);
929 16
            $styleValue = isset($value[1]) ? trim($value[1]) : null;
930 16
            $styleValueString = (string) $styleValue;
931
932 16
            if (!$styleName) {
933 12
                continue;
934
            }
935
936
            switch ($styleName) {
937 16
                case 'background':
938 16
                case 'background-color':
939 3
                    $styleColor = $this->getStyleColor($styleValueString);
940
941 3
                    if (!$styleColor) {
942 1
                        continue 2;
943
                    }
944
945 3
                    $cellStyle->applyFromArray(['fill' => ['fillType' => Fill::FILL_SOLID, 'color' => ['rgb' => $styleColor]]]);
946
947 3
                    break;
948 16
                case 'color':
949 3
                    $styleColor = $this->getStyleColor($styleValueString);
950
951 3
                    if (!$styleColor) {
952 1
                        continue 2;
953
                    }
954
955 3
                    $cellStyle->applyFromArray(['font' => ['color' => ['rgb' => $styleColor]]]);
956
957 3
                    break;
958
959 13
                case 'border':
960 3
                    $this->setBorderStyle($cellStyle, $styleValueString, 'allBorders');
961
962 3
                    break;
963
964 11
                case 'border-top':
965 1
                    $this->setBorderStyle($cellStyle, $styleValueString, 'top');
966
967 1
                    break;
968
969 11
                case 'border-bottom':
970 1
                    $this->setBorderStyle($cellStyle, $styleValueString, 'bottom');
971
972 1
                    break;
973
974 11
                case 'border-left':
975 1
                    $this->setBorderStyle($cellStyle, $styleValueString, 'left');
976
977 1
                    break;
978
979 11
                case 'border-right':
980 1
                    $this->setBorderStyle($cellStyle, $styleValueString, 'right');
981
982 1
                    break;
983
984 10
                case 'font-size':
985 1
                    $cellStyle->getFont()->setSize(
986 1
                        (float) $styleValue
987 1
                    );
988
989 1
                    break;
990
991 10
                case 'font-weight':
992 1
                    if ($styleValue === 'bold' || $styleValue >= 500) {
993 1
                        $cellStyle->getFont()->setBold(true);
994
                    }
995
996 1
                    break;
997
998 10
                case 'font-style':
999 1
                    if ($styleValue === 'italic') {
1000 1
                        $cellStyle->getFont()->setItalic(true);
1001
                    }
1002
1003 1
                    break;
1004
1005 10
                case 'font-family':
1006 1
                    $cellStyle->getFont()->setName(str_replace('\'', '', $styleValueString));
1007
1008 1
                    break;
1009
1010 10
                case 'text-decoration':
1011
                    switch ($styleValue) {
1012 1
                        case 'underline':
1013 1
                            $cellStyle->getFont()->setUnderline(Font::UNDERLINE_SINGLE);
1014
1015 1
                            break;
1016 1
                        case 'line-through':
1017 1
                            $cellStyle->getFont()->setStrikethrough(true);
1018
1019 1
                            break;
1020
                    }
1021
1022 1
                    break;
1023
1024 9
                case 'text-align':
1025 1
                    $cellStyle->getAlignment()->setHorizontal($styleValueString);
1026
1027 1
                    break;
1028
1029 9
                case 'vertical-align':
1030 2
                    $cellStyle->getAlignment()->setVertical($styleValueString);
1031
1032 2
                    break;
1033
1034 9
                case 'width':
1035 2
                    if ($column !== '') {
1036 2
                        $sheet->getColumnDimension($column)->setWidth(
1037 2
                            (new CssDimension($styleValue ?? ''))->width()
1038 2
                        );
1039
                    }
1040
1041 2
                    break;
1042
1043 7
                case 'height':
1044 1
                    if ($row > 0) {
1045 1
                        $sheet->getRowDimension($row)->setRowHeight(
1046 1
                            (new CssDimension($styleValue ?? ''))->height()
1047 1
                        );
1048
                    }
1049
1050 1
                    break;
1051
1052 6
                case 'word-wrap':
1053 1
                    $cellStyle->getAlignment()->setWrapText(
1054 1
                        $styleValue === 'break-word'
1055 1
                    );
1056
1057 1
                    break;
1058
1059 6
                case 'text-indent':
1060 2
                    $cellStyle->getAlignment()->setIndent(
1061 2
                        (int) str_replace(['px'], '', $styleValueString)
1062 2
                    );
1063
1064 2
                    break;
1065
            }
1066
        }
1067
    }
1068
1069
    /**
1070
     * Check if has #, so we can get clean hex.
1071
     */
1072 7
    public function getStyleColor(?string $value): string
1073
    {
1074 7
        $value = (string) $value;
1075 7
        if (str_starts_with($value, '#')) {
1076 5
            return substr($value, 1);
1077
        }
1078
1079 4
        return HelperHtml::colourNameLookup($value);
1080
    }
1081
1082 17
    private function insertImage(Worksheet $sheet, string $column, int $row, array $attributes): void
1083
    {
1084 17
        if (!isset($attributes['src'])) {
1085 1
            return;
1086
        }
1087 16
        $styleArray = self::getStyleArray($attributes);
1088
1089 16
        $src = $attributes['src'];
1090 16
        if (substr($src, 0, 5) !== 'data:') {
1091 12
            $src = urldecode($src);
1092
        }
1093 16
        $width = isset($attributes['width']) ? (float) $attributes['width'] : ($styleArray['width'] ?? null);
1094 16
        $height = isset($attributes['height']) ? (float) $attributes['height'] : ($styleArray['height'] ?? null);
1095 16
        $name = $attributes['alt'] ?? null;
1096
1097 16
        $drawing = new Drawing();
1098 16
        $drawing->setPath($src, false);
1099 9
        if ($drawing->getPath() === '') {
1100 1
            return;
1101
        }
1102 8
        $drawing->setWorksheet($sheet);
1103 8
        $drawing->setCoordinates($column . $row);
1104 8
        $drawing->setOffsetX(0);
1105 8
        $drawing->setOffsetY(10);
1106 8
        $drawing->setResizeProportional(true);
1107
1108 8
        if ($name) {
1109 7
            $drawing->setName($name);
1110
        }
1111
1112 8
        if ($width) {
1113 5
            if ($height) {
1114 2
                $drawing->setWidthAndHeight((int) $width, (int) $height);
1115
            } else {
1116 3
                $drawing->setWidth((int) $width);
1117
            }
1118 3
        } elseif ($height) {
1119 1
            $drawing->setHeight((int) $height);
1120
        }
1121
1122 8
        $sheet->getColumnDimension($column)->setWidth(
1123 8
            $drawing->getWidth() / 6
1124 8
        );
1125
1126 8
        $sheet->getRowDimension($row)->setRowHeight(
1127 8
            $drawing->getHeight() * 0.9
1128 8
        );
1129
1130 8
        if (isset($styleArray['opacity'])) {
1131
            $opacity = $styleArray['opacity'];
1132
            if (is_numeric($opacity)) {
1133
                $drawing->setOpacity((int) ($opacity * 100000));
1134
            }
1135
        }
1136
    }
1137
1138 16
    private static function getStyleArray(array $attributes): array
1139
    {
1140 16
        $styleArray = [];
1141 16
        if (isset($attributes['style'])) {
1142 4
            $styles = explode(';', $attributes['style']);
1143 4
            foreach ($styles as $style) {
1144 4
                $value = explode(':', $style);
1145 4
                if (count($value) === 2) {
1146 4
                    $arrayKey = trim($value[0]);
1147 4
                    $arrayValue = trim($value[1]);
1148 4
                    if ($arrayKey === 'width') {
1149 4
                        if (substr($arrayValue, -2) === 'px') {
1150 4
                            $arrayValue = (string) (((float) substr($arrayValue, 0, -2)));
1151
                        } else {
1152
                            $arrayValue = (new CssDimension($arrayValue))->width();
1153
                        }
1154 4
                    } elseif ($arrayKey === 'height') {
1155 2
                        if (substr($arrayValue, -2) === 'px') {
1156 2
                            $arrayValue = substr($arrayValue, 0, -2);
1157
                        } else {
1158
                            $arrayValue = (new CssDimension($arrayValue))->height();
1159
                        }
1160
                    }
1161 4
                    $styleArray[$arrayKey] = $arrayValue;
1162
                }
1163
            }
1164
        }
1165
1166 16
        return $styleArray;
1167
    }
1168
1169
    private const BORDER_MAPPINGS = [
1170
        'dash-dot' => Border::BORDER_DASHDOT,
1171
        'dash-dot-dot' => Border::BORDER_DASHDOTDOT,
1172
        'dashed' => Border::BORDER_DASHED,
1173
        'dotted' => Border::BORDER_DOTTED,
1174
        'double' => Border::BORDER_DOUBLE,
1175
        'hair' => Border::BORDER_HAIR,
1176
        'medium' => Border::BORDER_MEDIUM,
1177
        'medium-dashed' => Border::BORDER_MEDIUMDASHED,
1178
        'medium-dash-dot' => Border::BORDER_MEDIUMDASHDOT,
1179
        'medium-dash-dot-dot' => Border::BORDER_MEDIUMDASHDOTDOT,
1180
        'none' => Border::BORDER_NONE,
1181
        'slant-dash-dot' => Border::BORDER_SLANTDASHDOT,
1182
        'solid' => Border::BORDER_THIN,
1183
        'thick' => Border::BORDER_THICK,
1184
    ];
1185
1186 15
    public static function getBorderMappings(): array
1187
    {
1188 15
        return self::BORDER_MAPPINGS;
1189
    }
1190
1191
    /**
1192
     * Map html border style to PhpSpreadsheet border style.
1193
     */
1194 3
    public function getBorderStyle(string $style): ?string
1195
    {
1196 3
        return self::BORDER_MAPPINGS[$style] ?? null;
1197
    }
1198
1199 3
    private function setBorderStyle(Style $cellStyle, string $styleValue, string $type): void
1200
    {
1201 3
        if (trim($styleValue) === Border::BORDER_NONE) {
1202 1
            $borderStyle = Border::BORDER_NONE;
1203 1
            $color = null;
1204
        } else {
1205 3
            $borderArray = explode(' ', $styleValue);
1206 3
            $borderCount = count($borderArray);
1207 3
            if ($borderCount >= 3) {
1208 3
                $borderStyle = $borderArray[1];
1209 3
                $color = $borderArray[2];
1210
            } else {
1211 1
                $borderStyle = $borderArray[0];
1212 1
                $color = $borderArray[1] ?? null;
1213
            }
1214
        }
1215
1216 3
        $cellStyle->applyFromArray([
1217 3
            'borders' => [
1218 3
                $type => [
1219 3
                    'borderStyle' => $this->getBorderStyle($borderStyle),
1220 3
                    'color' => ['rgb' => $this->getStyleColor($color)],
1221 3
                ],
1222 3
            ],
1223 3
        ]);
1224
    }
1225
1226
    /**
1227
     * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
1228
     */
1229 1
    public function listWorksheetInfo(string $filename): array
1230
    {
1231 1
        $info = [];
1232 1
        $spreadsheet = $this->newSpreadsheet();
1233 1
        $this->loadIntoExisting($filename, $spreadsheet);
1234 1
        foreach ($spreadsheet->getAllSheets() as $sheet) {
1235 1
            $newEntry = ['worksheetName' => $sheet->getTitle()];
1236 1
            $newEntry['lastColumnLetter'] = $sheet->getHighestDataColumn();
1237 1
            $newEntry['lastColumnIndex'] = Coordinate::columnIndexFromString($sheet->getHighestDataColumn()) - 1;
1238 1
            $newEntry['totalRows'] = $sheet->getHighestDataRow();
1239 1
            $newEntry['totalColumns'] = $newEntry['lastColumnIndex'] + 1;
1240 1
            $newEntry['sheetState'] = Worksheet::SHEETSTATE_VISIBLE;
1241 1
            $info[] = $newEntry;
1242
        }
1243 1
        $spreadsheet->disconnectWorksheets();
1244
1245 1
        return $info;
1246
    }
1247
}
1248