• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

PHPOffice / PhpSpreadsheet / 17708030292

14 Sep 2025 07:21AM UTC coverage: 95.415% (+0.1%) from 95.304%
17708030292

Pull #4646

github

web-flow
Merge 9df8ceb5c into 996de1731
Pull Request #4646: Minor Improvements to Reader/Writer Coverage

7 of 9 new or added lines in 3 files covered. (77.78%)

6 existing lines in 1 file now uncovered.

40353 of 42292 relevant lines covered (95.42%)

347.8 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.45
/src/PhpSpreadsheet/Reader/Html.php
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 const 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
    /** @var array<string, bool> */
132
    protected array $rowspan = [];
133

134
    /**
135
     * Create a new HTML Reader instance.
136
     */
137
    public function __construct()
138
    {
139
        parent::__construct();
536✔
140
        $this->securityScanner = XmlScanner::getInstance($this);
536✔
141
    }
142

143
    /**
144
     * Validate that the current file is an HTML file.
145
     */
146
    public function canRead(string $filename): bool
147
    {
148
        // Check if file exists
149
        try {
150
            $this->openFile($filename);
509✔
151
        } catch (Exception) {
1✔
152
            return false;
1✔
153
        }
154

155
        $beginning = preg_replace(self::STARTS_WITH_BOM, '', $this->readBeginning()) ?? '';
508✔
156

157
        $startWithTag = self::startsWithTag($beginning);
508✔
158
        $containsTags = self::containsTags($beginning);
508✔
159
        $endsWithTag = self::endsWithTag($this->readEnding());
508✔
160

161
        fclose($this->fileHandle);
508✔
162

163
        return $startWithTag && $containsTags && $endsWithTag;
508✔
164
    }
165

166
    private function readBeginning(): string
167
    {
168
        fseek($this->fileHandle, 0);
508✔
169

170
        return (string) fread($this->fileHandle, self::TEST_SAMPLE_SIZE);
508✔
171
    }
172

173
    private function readEnding(): string
174
    {
175
        $meta = stream_get_meta_data($this->fileHandle);
508✔
176
        // Phpstan incorrectly flags following line for Php8.2-, corrected in 8.3
177
        $filename = $meta['uri']; //@phpstan-ignore-line
508✔
178

179
        clearstatcache(true, $filename);
508✔
180
        $size = (int) filesize($filename);
508✔
181
        if ($size === 0) {
508✔
182
            return '';
2✔
183
        }
184

185
        $blockSize = self::TEST_SAMPLE_SIZE;
506✔
186
        if ($size < $blockSize) {
506✔
187
            $blockSize = $size;
53✔
188
        }
189

190
        fseek($this->fileHandle, $size - $blockSize);
506✔
191

192
        return (string) fread($this->fileHandle, $blockSize);
506✔
193
    }
194

195
    private static function startsWithTag(string $data): bool
196
    {
197
        return str_starts_with(trim($data), '<');
508✔
198
    }
199

200
    private static function endsWithTag(string $data): bool
201
    {
202
        return str_ends_with(trim($data), '>');
508✔
203
    }
204

205
    private static function containsTags(string $data): bool
206
    {
207
        return strlen($data) !== strlen(strip_tags($data));
508✔
208
    }
209

210
    /**
211
     * Loads Spreadsheet from file.
212
     */
213
    public function loadSpreadsheetFromFile(string $filename): Spreadsheet
214
    {
215
        $spreadsheet = $this->newSpreadsheet();
488✔
216
        $spreadsheet->setValueBinder($this->valueBinder);
488✔
217

218
        // Load into this instance
219
        return $this->loadIntoExisting($filename, $spreadsheet);
488✔
220
    }
221

222
    /**
223
     * Data Array used for testing only, should write to
224
     * Spreadsheet object on completion of tests.
225
     *
226
     * @var mixed[][]
227
     */
228
    protected array $dataArray = [];
229

230
    protected int $tableLevel = 0;
231

232
    /** @var string[] */
233
    protected array $nestedColumn = ['A'];
234

235
    protected function setTableStartColumn(string $column): string
236
    {
237
        if ($this->tableLevel == 0) {
506✔
238
            $column = 'A';
506✔
239
        }
240
        ++$this->tableLevel;
506✔
241
        $this->nestedColumn[$this->tableLevel] = $column;
506✔
242

243
        return $this->nestedColumn[$this->tableLevel];
506✔
244
    }
245

246
    protected function getTableStartColumn(): string
247
    {
248
        return $this->nestedColumn[$this->tableLevel];
502✔
249
    }
250

251
    protected function releaseTableStartColumn(): string
252
    {
253
        --$this->tableLevel;
499✔
254

255
        return array_pop($this->nestedColumn) ?? '';
499✔
256
    }
257

258
    /**
259
     * Flush cell.
260
     *
261
     * @param string[] $attributeArray
262
     *
263
     * @param-out string $cellContent In one case, it can be bool
264
     */
265
    protected function flushCell(Worksheet $sheet, string $column, int|string $row, mixed &$cellContent, array $attributeArray): void
266
    {
267
        if (is_string($cellContent)) {
507✔
268
            //    Simple String content
269
            if (trim($cellContent) > '') {
507✔
270
                //    Only actually write it if there's content in the string
271
                //    Write to worksheet to be done here...
272
                //    ... we return the cell, so we can mess about with styles more easily
273

274
                // Set cell value explicitly if there is data-type attribute
275
                if (isset($attributeArray['data-type'])) {
484✔
276
                    $datatype = $attributeArray['data-type'];
5✔
277
                    if (in_array($datatype, [DataType::TYPE_STRING, DataType::TYPE_STRING2, DataType::TYPE_INLINE])) {
5✔
278
                        //Prevent to Excel treat string with beginning equal sign or convert big numbers to scientific number
279
                        if (str_starts_with($cellContent, '=')) {
5✔
280
                            $sheet->getCell($column . $row)
1✔
281
                                ->getStyle()
1✔
282
                                ->setQuotePrefix(true);
1✔
283
                        }
284
                    }
285
                    if ($datatype === DataType::TYPE_BOOL) {
5✔
286
                        // This is the case where we can set cellContent to bool rather than string
287
                        $cellContent = self::convertBoolean($cellContent); //* @phpstan-ignore-line
5✔
288
                        if (!is_bool($cellContent)) {
5✔
289
                            $attributeArray['data-type'] = DataType::TYPE_STRING;
1✔
290
                        }
291
                    }
292

293
                    //catching the Exception and ignoring the invalid data types
294
                    try {
295
                        $sheet->setCellValueExplicit($column . $row, $cellContent, $attributeArray['data-type']);
5✔
296
                    } catch (SpreadsheetException) {
1✔
297
                        $sheet->setCellValue($column . $row, $cellContent);
1✔
298
                    }
299
                } else {
300
                    $sheet->setCellValue($column . $row, $cellContent);
483✔
301
                }
302
                $this->dataArray[$row][$column] = $cellContent;
484✔
303
            }
304
        } else {
305
            //    We have a Rich Text run
306
            //    TODO
307
            $this->dataArray[$row][$column] = 'RICH TEXT: ' . StringHelper::convertToString($cellContent);
×
308
        }
309
        $cellContent = (string) '';
507✔
310
    }
311

312
    /** @var array<int, array<int, string>> */
313
    private static array $falseTrueArray = [];
314

315
    private static function convertBoolean(?string $cellContent): bool|string
316
    {
317
        if ($cellContent === '1') {
5✔
318
            return true;
1✔
319
        }
320
        if ($cellContent === '0' || $cellContent === '' || $cellContent === null) {
5✔
321
            return false;
1✔
322
        }
323
        if (empty(self::$falseTrueArray)) {
4✔
324
            $calc = Calculation::getInstance();
1✔
325
            self::$falseTrueArray = $calc->getFalseTrueArray();
1✔
326
        }
327
        if (in_array(mb_strtoupper($cellContent), self::$falseTrueArray[1], true)) {
4✔
328
            return true;
4✔
329
        }
330
        if (in_array(mb_strtoupper($cellContent), self::$falseTrueArray[0], true)) {
4✔
331
            return false;
4✔
332
        }
333

334
        return $cellContent;
1✔
335
    }
336

337
    private function processDomElementBody(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child): void
338
    {
339
        $attributeArray = [];
507✔
340
        /** @var DOMAttr $attribute */
341
        foreach (($child->attributes ?? []) as $attribute) {
507✔
342
            $attributeArray[$attribute->name] = $attribute->value;
493✔
343
        }
344

345
        if ($child->nodeName === 'body') {
507✔
346
            $row = 1;
507✔
347
            $column = 'A';
507✔
348
            $cellContent = '';
507✔
349
            $this->tableLevel = 0;
507✔
350
            $this->processDomElement($child, $sheet, $row, $column, $cellContent);
507✔
351
        } else {
352
            $this->processDomElementTitle($sheet, $row, $column, $cellContent, $child, $attributeArray);
507✔
353
        }
354
    }
355

356
    /** @param string[] $attributeArray */
357
    private function processDomElementTitle(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
358
    {
359
        if ($child->nodeName === 'title') {
507✔
360
            $this->processDomElement($child, $sheet, $row, $column, $cellContent);
461✔
361

362
            try {
363
                $sheet->setTitle($cellContent, true, true);
461✔
364
                $sheet->getParent()?->getProperties()?->setTitle($cellContent);
458✔
365
            } catch (SpreadsheetException) {
3✔
366
                // leave default title if too long or illegal chars
367
            }
368
            $cellContent = '';
461✔
369
        } else {
370
            $this->processDomElementSpanEtc($sheet, $row, $column, $cellContent, $child, $attributeArray);
507✔
371
        }
372
    }
373

374
    private const SPAN_ETC = ['span', 'div', 'font', 'i', 'em', 'strong', 'b'];
375

376
    /** @param string[] $attributeArray */
377
    private function processDomElementSpanEtc(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
378
    {
379
        if (in_array((string) $child->nodeName, self::SPAN_ETC, true)) {
507✔
380
            if (isset($attributeArray['class']) && $attributeArray['class'] === 'comment') {
446✔
381
                $sheet->getComment($column . $row)
9✔
382
                    ->getText()
9✔
383
                    ->createTextRun($child->textContent);
9✔
384
                if (isset($attributeArray['dir']) && $attributeArray['dir'] === 'rtl') {
9✔
385
                    $sheet->getComment($column . $row)->setTextboxDirection(Comment::TEXTBOX_DIRECTION_RTL);
1✔
386
                }
387
                if (isset($attributeArray['style'])) {
9✔
388
                    $alignStyle = $attributeArray['style'];
2✔
389
                    if (preg_match('/\btext-align:\s*(left|right|center|justify)\b/', (string) $alignStyle, $matches) === 1) {
2✔
390
                        $sheet->getComment($column . $row)->setAlignment($matches[1]);
2✔
391
                    }
392
                }
393
            } else {
394
                $this->processDomElement($child, $sheet, $row, $column, $cellContent);
446✔
395
            }
396

397
            if (isset(self::FORMATS[$child->nodeName])) {
446✔
398
                $sheet->getStyle($column . $row)->applyFromArray(self::FORMATS[$child->nodeName]);
2✔
399
            }
400
        } else {
401
            $this->processDomElementHr($sheet, $row, $column, $cellContent, $child, $attributeArray);
507✔
402
        }
403
    }
404

405
    /** @param string[] $attributeArray */
406
    private function processDomElementHr(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
407
    {
408
        if ($child->nodeName === 'hr') {
507✔
409
            $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
1✔
410
            ++$row;
1✔
411
            $sheet->getStyle($column . $row)->applyFromArray(self::FORMATS[$child->nodeName]);
1✔
412
            ++$row;
1✔
413
        }
414
        // fall through to br
415
        $this->processDomElementBr($sheet, $row, $column, $cellContent, $child, $attributeArray);
507✔
416
    }
417

418
    /** @param string[] $attributeArray */
419
    private function processDomElementBr(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
420
    {
421
        if ($child->nodeName === 'br' || $child->nodeName === 'hr') {
507✔
422
            if ($this->tableLevel > 0) {
4✔
423
                //    If we're inside a table, replace with a newline and set the cell to wrap
424
                $cellContent .= "\n";
4✔
425
                $sheet->getStyle($column . $row)->getAlignment()->setWrapText(true);
4✔
426
            } else {
427
                //    Otherwise flush our existing content and move the row cursor on
428
                $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
1✔
429
                ++$row;
1✔
430
            }
431
        } else {
432
            $this->processDomElementA($sheet, $row, $column, $cellContent, $child, $attributeArray);
507✔
433
        }
434
    }
435

436
    /** @param string[] $attributeArray */
437
    private function processDomElementA(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
438
    {
439
        if ($child->nodeName === 'a') {
507✔
440
            foreach ($attributeArray as $attributeName => $attributeValue) {
12✔
441
                switch ($attributeName) {
442
                    case 'href':
12✔
443
                        $sheet->getCell($column . $row)->getHyperlink()->setUrl($attributeValue);
3✔
444
                        $sheet->getStyle($column . $row)->applyFromArray(self::FORMATS[$child->nodeName]);
3✔
445

446
                        break;
3✔
447
                    case 'class':
10✔
448
                        if ($attributeValue === 'comment-indicator') {
9✔
449
                            break; // Ignore - it's just a red square.
9✔
450
                        }
451
                }
452
            }
453
            // no idea why this should be needed
454
            //$cellContent .= ' ';
455
            $this->processDomElement($child, $sheet, $row, $column, $cellContent);
12✔
456
        } else {
457
            $this->processDomElementH1Etc($sheet, $row, $column, $cellContent, $child, $attributeArray);
507✔
458
        }
459
    }
460

461
    private const H1_ETC = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ol', 'ul', 'p'];
462

463
    /** @param string[] $attributeArray */
464
    private function processDomElementH1Etc(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
465
    {
466
        if (in_array((string) $child->nodeName, self::H1_ETC, true)) {
507✔
467
            if ($this->tableLevel > 0) {
2✔
468
                //    If we're inside a table, replace with a newline
469
                $cellContent .= $cellContent ? "\n" : '';
1✔
470
                $sheet->getStyle($column . $row)->getAlignment()->setWrapText(true);
1✔
471
                $this->processDomElement($child, $sheet, $row, $column, $cellContent);
1✔
472
            } else {
473
                if ($cellContent > '') {
2✔
474
                    $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
1✔
475
                    ++$row;
1✔
476
                }
477
                $this->processDomElement($child, $sheet, $row, $column, $cellContent);
2✔
478
                $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
2✔
479

480
                if (isset(self::FORMATS[$child->nodeName])) {
2✔
481
                    $sheet->getStyle($column . $row)->applyFromArray(self::FORMATS[$child->nodeName]);
1✔
482
                }
483

484
                ++$row;
2✔
485
                $column = 'A';
2✔
486
            }
487
        } else {
488
            $this->processDomElementLi($sheet, $row, $column, $cellContent, $child, $attributeArray);
507✔
489
        }
490
    }
491

492
    /** @param string[] $attributeArray */
493
    private function processDomElementLi(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
494
    {
495
        if ($child->nodeName === 'li') {
507✔
496
            if ($this->tableLevel > 0) {
2✔
497
                //    If we're inside a table, replace with a newline
498
                $cellContent .= $cellContent ? "\n" : '';
1✔
499
                $this->processDomElement($child, $sheet, $row, $column, $cellContent);
1✔
500
            } else {
501
                if ($cellContent > '') {
2✔
502
                    $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
1✔
503
                }
504
                ++$row;
2✔
505
                $this->processDomElement($child, $sheet, $row, $column, $cellContent);
2✔
506
                $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
2✔
507
                $column = 'A';
2✔
508
            }
509
        } else {
510
            $this->processDomElementImg($sheet, $row, $column, $cellContent, $child, $attributeArray);
507✔
511
        }
512
    }
513

514
    /** @param string[] $attributeArray */
515
    private function processDomElementImg(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
516
    {
517
        if ($child->nodeName === 'img') {
507✔
518
            $this->insertImage($sheet, $column, $row, $attributeArray);
20✔
519
        } else {
520
            $this->processDomElementTable($sheet, $row, $column, $cellContent, $child, $attributeArray);
507✔
521
        }
522
    }
523

524
    private string $currentColumn = 'A';
525

526
    /** @param string[] $attributeArray */
527
    private function processDomElementTable(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
528
    {
529
        if ($child->nodeName === 'table') {
507✔
530
            if (isset($attributeArray['class'])) {
506✔
531
                $classes = explode(' ', $attributeArray['class']);
449✔
532
                $sheet->setShowGridlines(in_array('gridlines', $classes, true));
449✔
533
                $sheet->setPrintGridlines(in_array('gridlinesp', $classes, true));
449✔
534
            }
535
            if ('rtl' === ($attributeArray['dir'] ?? '')) {
506✔
536
                $sheet->setRightToLeft(true);
2✔
537
            }
538
            $this->currentColumn = 'A';
506✔
539
            $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
506✔
540
            $column = $this->setTableStartColumn($column);
506✔
541
            if ($this->tableLevel > 1 && $row > 1) {
506✔
542
                --$row;
2✔
543
            }
544
            $this->processDomElement($child, $sheet, $row, $column, $cellContent);
506✔
545
            $column = $this->releaseTableStartColumn();
499✔
546
            if ($this->tableLevel > 1) {
499✔
547
                StringHelper::stringIncrement($column);
2✔
548
            } else {
549
                ++$row;
499✔
550
            }
551
        } else {
552
            $this->processDomElementTr($sheet, $row, $column, $cellContent, $child, $attributeArray);
507✔
553
        }
554
    }
555

556
    /** @param string[] $attributeArray */
557
    private function processDomElementTr(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
558
    {
559
        if ($child->nodeName === 'col') {
507✔
560
            $this->applyInlineStyle($sheet, -1, $this->currentColumn, $attributeArray);
444✔
561
            StringHelper::stringIncrement($this->currentColumn);
444✔
562
        } elseif ($child->nodeName === 'tr') {
507✔
563
            $column = $this->getTableStartColumn();
502✔
564
            $cellContent = '';
502✔
565
            $this->processDomElement($child, $sheet, $row, $column, $cellContent);
502✔
566

567
            if (isset($attributeArray['height'])) {
495✔
568
                $sheet->getRowDimension($row)->setRowHeight((float) $attributeArray['height']);
1✔
569
            }
570

571
            ++$row;
495✔
572
        } else {
573
            $this->processDomElementThTdOther($sheet, $row, $column, $cellContent, $child, $attributeArray);
507✔
574
        }
575
    }
576

577
    /** @param string[] $attributeArray */
578
    private function processDomElementThTdOther(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
579
    {
580
        if ($child->nodeName !== 'td' && $child->nodeName !== 'th') {
507✔
581
            $this->processDomElement($child, $sheet, $row, $column, $cellContent);
507✔
582
        } else {
583
            $this->processDomElementThTd($sheet, $row, $column, $cellContent, $child, $attributeArray);
502✔
584
        }
585
    }
586

587
    /** @param string[] $attributeArray */
588
    private function processDomElementBgcolor(Worksheet $sheet, int $row, string $column, array $attributeArray): void
589
    {
590
        if (isset($attributeArray['bgcolor'])) {
495✔
591
            $sheet->getStyle("$column$row")->applyFromArray(
1✔
592
                [
1✔
593
                    'fill' => [
1✔
594
                        'fillType' => Fill::FILL_SOLID,
1✔
595
                        'color' => ['rgb' => $this->getStyleColor($attributeArray['bgcolor'])],
1✔
596
                    ],
1✔
597
                ]
1✔
598
            );
1✔
599
        }
600
    }
601

602
    /** @param string[] $attributeArray */
603
    private function processDomElementWidth(Worksheet $sheet, string $column, array $attributeArray): void
604
    {
605
        if (isset($attributeArray['width'])) {
495✔
606
            $sheet->getColumnDimension($column)->setWidth((new CssDimension($attributeArray['width']))->width());
1✔
607
        }
608
    }
609

610
    /** @param string[] $attributeArray */
611
    private function processDomElementHeight(Worksheet $sheet, int $row, array $attributeArray): void
612
    {
613
        if (isset($attributeArray['height'])) {
495✔
614
            $sheet->getRowDimension($row)->setRowHeight((new CssDimension($attributeArray['height']))->height());
1✔
615
        }
616
    }
617

618
    /** @param string[] $attributeArray */
619
    private function processDomElementAlign(Worksheet $sheet, int $row, string $column, array $attributeArray): void
620
    {
621
        if (isset($attributeArray['align'])) {
495✔
622
            $sheet->getStyle($column . $row)->getAlignment()->setHorizontal($attributeArray['align']);
1✔
623
        }
624
    }
625

626
    /** @param string[] $attributeArray */
627
    private function processDomElementVAlign(Worksheet $sheet, int $row, string $column, array $attributeArray): void
628
    {
629
        if (isset($attributeArray['valign'])) {
495✔
630
            $sheet->getStyle($column . $row)->getAlignment()->setVertical($attributeArray['valign']);
1✔
631
        }
632
    }
633

634
    /** @param string[] $attributeArray */
635
    private function processDomElementDataFormat(Worksheet $sheet, int $row, string $column, array $attributeArray): void
636
    {
637
        if (isset($attributeArray['data-format'])) {
495✔
638
            $sheet->getStyle($column . $row)->getNumberFormat()->setFormatCode($attributeArray['data-format']);
1✔
639
        }
640
    }
641

642
    /** @param string[] $attributeArray */
643
    private function processDomElementThTd(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
644
    {
645
        while (isset($this->rowspan[$column . $row])) {
502✔
646
            $temp = (string) $column;
3✔
647
            $column = StringHelper::stringIncrement($temp);
3✔
648
        }
649
        $this->processDomElement($child, $sheet, $row, $column, $cellContent);
502✔
650

651
        // apply inline style
652
        $this->applyInlineStyle($sheet, $row, $column, $attributeArray);
495✔
653

654
        /** @var string $cellContent */
655
        $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
495✔
656

657
        $this->processDomElementBgcolor($sheet, $row, $column, $attributeArray);
495✔
658
        $this->processDomElementWidth($sheet, $column, $attributeArray);
495✔
659
        $this->processDomElementHeight($sheet, $row, $attributeArray);
495✔
660
        $this->processDomElementAlign($sheet, $row, $column, $attributeArray);
495✔
661
        $this->processDomElementVAlign($sheet, $row, $column, $attributeArray);
495✔
662
        $this->processDomElementDataFormat($sheet, $row, $column, $attributeArray);
495✔
663

664
        if (isset($attributeArray['rowspan'], $attributeArray['colspan'])) {
495✔
665
            //create merging rowspan and colspan
666
            $columnTo = $column;
2✔
667
            for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) {
2✔
668
                StringHelper::stringIncrement($columnTo);
2✔
669
            }
670
            $range = $column . $row . ':' . $columnTo . ($row + (int) $attributeArray['rowspan'] - 1);
2✔
671
            foreach (Coordinate::extractAllCellReferencesInRange($range) as $value) {
2✔
672
                $this->rowspan[$value] = true;
2✔
673
            }
674
            $sheet->mergeCells($range);
2✔
675
            $column = $columnTo;
2✔
676
        } elseif (isset($attributeArray['rowspan'])) {
495✔
677
            //create merging rowspan
678
            $range = $column . $row . ':' . $column . ($row + (int) $attributeArray['rowspan'] - 1);
3✔
679
            foreach (Coordinate::extractAllCellReferencesInRange($range) as $value) {
3✔
680
                $this->rowspan[$value] = true;
3✔
681
            }
682
            $sheet->mergeCells($range);
3✔
683
        } elseif (isset($attributeArray['colspan'])) {
495✔
684
            //create merging colspan
685
            $columnTo = $column;
3✔
686
            for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) {
3✔
687
                StringHelper::stringIncrement($columnTo);
3✔
688
            }
689
            $sheet->mergeCells($column . $row . ':' . $columnTo . $row);
3✔
690
            $column = $columnTo;
3✔
691
        }
692

693
        StringHelper::stringIncrement($column);
495✔
694
    }
695

696
    protected function processDomElement(DOMNode $element, Worksheet $sheet, int &$row, string &$column, string &$cellContent): void
697
    {
698
        foreach ($element->childNodes as $child) {
507✔
699
            if ($child instanceof DOMText) {
507✔
700
                $domText = (string) preg_replace('/\s+/', ' ', trim($child->nodeValue ?? ''));
504✔
701
                if ($domText === "\u{a0}") {
504✔
702
                    $domText = '';
9✔
703
                }
704
                //    simply append the text if the cell content is a plain text string
705
                $cellContent .= $domText;
504✔
706
                //    but if we have a rich text run instead, we need to append it correctly
707
                //    TODO
708
            } elseif ($child instanceof DOMElement) {
507✔
709
                $this->processDomElementBody($sheet, $row, $column, $cellContent, $child);
507✔
710
            }
711
        }
712
    }
713

714
    /**
715
     * Loads PhpSpreadsheet from file into PhpSpreadsheet instance.
716
     */
717
    public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Spreadsheet
718
    {
719
        // Validate
720
        if (!$this->canRead($filename)) {
488✔
721
            throw new Exception($filename . ' is an Invalid HTML file.');
1✔
722
        }
723

724
        // Create a new DOM object
725
        $dom = new DOMDocument();
487✔
726

727
        // Reload the HTML file into the DOM object
728
        try {
729
            $convert = $this->getSecurityScannerOrThrow()->scanFile($filename);
487✔
730
            $convert = static::replaceNonAsciiIfNeeded($convert);
486✔
731
            $loaded = ($convert === null) ? false : $dom->loadHTML($convert);
486✔
732
        } catch (Throwable $e) {
2✔
733
            $loaded = false;
2✔
734
        }
735
        if ($loaded === false) {
487✔
736
            throw new Exception('Failed to load file ' . $filename . ' as a DOM Document', 0, $e ?? null);
2✔
737
        }
738
        self::loadProperties($dom, $spreadsheet);
485✔
739

740
        return $this->loadDocument($dom, $spreadsheet);
485✔
741
    }
742

743
    private static function loadProperties(DOMDocument $dom, Spreadsheet $spreadsheet): void
744
    {
745
        $properties = $spreadsheet->getProperties();
507✔
746
        foreach ($dom->getElementsByTagName('meta') as $meta) {
507✔
747
            $metaContent = (string) $meta->getAttribute('content');
454✔
748
            if ($metaContent !== '') {
454✔
749
                $metaName = (string) $meta->getAttribute('name');
446✔
750
                switch ($metaName) {
751
                    case 'author':
446✔
752
                        $properties->setCreator($metaContent);
444✔
753

754
                        break;
444✔
755
                    case 'category':
446✔
756
                        $properties->setCategory($metaContent);
1✔
757

758
                        break;
1✔
759
                    case 'company':
446✔
760
                        $properties->setCompany($metaContent);
1✔
761

762
                        break;
1✔
763
                    case 'created':
446✔
764
                        $properties->setCreated($metaContent);
444✔
765

766
                        break;
444✔
767
                    case 'description':
446✔
768
                        $properties->setDescription($metaContent);
1✔
769

770
                        break;
1✔
771
                    case 'keywords':
446✔
772
                        $properties->setKeywords($metaContent);
1✔
773

774
                        break;
1✔
775
                    case 'lastModifiedBy':
446✔
776
                        $properties->setLastModifiedBy($metaContent);
444✔
777

778
                        break;
444✔
779
                    case 'manager':
446✔
780
                        $properties->setManager($metaContent);
1✔
781

782
                        break;
1✔
783
                    case 'modified':
446✔
784
                        $properties->setModified($metaContent);
444✔
785

786
                        break;
444✔
787
                    case 'subject':
446✔
788
                        $properties->setSubject($metaContent);
1✔
789

790
                        break;
1✔
791
                    case 'title':
446✔
792
                        $properties->setTitle($metaContent);
442✔
793

794
                        break;
442✔
795
                    case 'viewport':
446✔
796
                        $properties->setViewport($metaContent);
1✔
797

798
                        break;
1✔
799
                    default:
800
                        if (preg_match('/^custom[.](bool|date|float|int|string)[.](.+)$/', $metaName, $matches) === 1) {
446✔
801
                            match ($matches[1]) {
1✔
802
                                'bool' => $properties->setCustomProperty($matches[2], (bool) $metaContent, Properties::PROPERTY_TYPE_BOOLEAN),
1✔
803
                                'float' => $properties->setCustomProperty($matches[2], (float) $metaContent, Properties::PROPERTY_TYPE_FLOAT),
1✔
804
                                'int' => $properties->setCustomProperty($matches[2], (int) $metaContent, Properties::PROPERTY_TYPE_INTEGER),
1✔
805
                                'date' => $properties->setCustomProperty($matches[2], $metaContent, Properties::PROPERTY_TYPE_DATE),
1✔
806
                                // string
807
                                default => $properties->setCustomProperty($matches[2], $metaContent, Properties::PROPERTY_TYPE_STRING),
1✔
808
                            };
1✔
809
                        }
810
                }
811
            }
812
        }
813
        if (!empty($dom->baseURI)) {
507✔
814
            $properties->setHyperlinkBase($dom->baseURI);
1✔
815
        }
816
    }
817

818
    /** @param string[] $matches */
819
    private static function replaceNonAscii(array $matches): string
820
    {
821
        return '&#' . mb_ord($matches[0], 'UTF-8') . ';';
21✔
822
    }
823

824
    protected static function replaceNonAsciiIfNeeded(string $convert): ?string
825
    {
826
        if (preg_match(self::STARTS_WITH_BOM, $convert) !== 1 && preg_match(self::DECLARES_CHARSET, $convert) !== 1) {
508✔
827
            $lowend = "\u{80}";
53✔
828
            $highend = "\u{10ffff}";
53✔
829
            $regexp = "/[$lowend-$highend]/u";
53✔
830
            /** @var callable $callback */
831
            $callback = [self::class, 'replaceNonAscii'];
53✔
832
            $convert = preg_replace_callback($regexp, $callback, $convert);
53✔
833
        }
834

835
        return $convert;
508✔
836
    }
837

838
    /**
839
     * Spreadsheet from content.
840
     */
841
    public function loadFromString(string $content, ?Spreadsheet $spreadsheet = null): Spreadsheet
842
    {
843
        //    Create a new DOM object
844
        $dom = new DOMDocument();
23✔
845

846
        //    Reload the HTML file into the DOM object
847
        try {
848
            $convert = $this->getSecurityScannerOrThrow()->scan($content);
23✔
849
            $convert = static::replaceNonAsciiIfNeeded($convert);
23✔
850
            $loaded = ($convert === null) ? false : $dom->loadHTML($convert);
22✔
851
        } catch (Throwable $e) {
1✔
852
            $loaded = false;
1✔
853
        }
854
        if ($loaded === false) {
23✔
855
            throw new Exception('Failed to load content as a DOM Document', 0, $e ?? null);
1✔
856
        }
857
        $spreadsheet = $spreadsheet ?? $this->newSpreadsheet();
22✔
858
        $spreadsheet->setValueBinder($this->valueBinder);
22✔
859
        self::loadProperties($dom, $spreadsheet);
22✔
860

861
        return $this->loadDocument($dom, $spreadsheet);
22✔
862
    }
863

864
    /**
865
     * Loads PhpSpreadsheet from DOMDocument into PhpSpreadsheet instance.
866
     */
867
    private function loadDocument(DOMDocument $document, Spreadsheet $spreadsheet): Spreadsheet
868
    {
869
        while ($spreadsheet->getSheetCount() <= $this->sheetIndex) {
507✔
870
            $spreadsheet->createSheet();
2✔
871
        }
872
        $spreadsheet->setActiveSheetIndex($this->sheetIndex);
507✔
873

874
        // Discard white space
875
        $document->preserveWhiteSpace = false;
507✔
876

877
        $row = 0;
507✔
878
        $column = 'A';
507✔
879
        $content = '';
507✔
880
        $this->rowspan = [];
507✔
881
        $this->processDomElement($document, $spreadsheet->getActiveSheet(), $row, $column, $content);
507✔
882

883
        // Return
884
        return $spreadsheet;
500✔
885
    }
886

887
    /**
888
     * Get sheet index.
889
     */
890
    public function getSheetIndex(): int
891
    {
892
        return $this->sheetIndex;
1✔
893
    }
894

895
    /**
896
     * Set sheet index.
897
     *
898
     * @param int $sheetIndex Sheet index
899
     *
900
     * @return $this
901
     */
902
    public function setSheetIndex(int $sheetIndex): static
903
    {
904
        $this->sheetIndex = $sheetIndex;
2✔
905

906
        return $this;
2✔
907
    }
908

909
    /**
910
     * Apply inline css inline style.
911
     *
912
     * NOTES :
913
     * Currently only intended for td & th element,
914
     * and only takes 'background-color' and 'color'; property with HEX color
915
     *
916
     * TODO :
917
     * - Implement to other propertie, such as border
918
     *
919
     * @param string[] $attributeArray
920
     */
921
    private function applyInlineStyle(Worksheet &$sheet, int $row, string $column, array $attributeArray): void
922
    {
923
        if (!isset($attributeArray['style'])) {
495✔
924
            return;
489✔
925
        }
926

927
        if ($row <= 0 || $column === '') {
17✔
928
            $cellStyle = new Style();
1✔
929
        } elseif (isset($attributeArray['rowspan'], $attributeArray['colspan'])) {
17✔
930
            $columnTo = $column;
1✔
931
            for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) {
1✔
932
                StringHelper::stringIncrement($columnTo);
1✔
933
            }
934
            $range = $column . $row . ':' . $columnTo . ($row + (int) $attributeArray['rowspan'] - 1);
1✔
935
            $cellStyle = $sheet->getStyle($range);
1✔
936
        } elseif (isset($attributeArray['rowspan'])) {
17✔
937
            $range = $column . $row . ':' . $column . ($row + (int) $attributeArray['rowspan'] - 1);
1✔
938
            $cellStyle = $sheet->getStyle($range);
1✔
939
        } elseif (isset($attributeArray['colspan'])) {
17✔
940
            $columnTo = $column;
1✔
941
            for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) {
1✔
942
                StringHelper::stringIncrement($columnTo);
1✔
943
            }
944
            $range = $column . $row . ':' . $columnTo . $row;
1✔
945
            $cellStyle = $sheet->getStyle($range);
1✔
946
        } else {
947
            $cellStyle = $sheet->getStyle($column . $row);
17✔
948
        }
949

950
        // add color styles (background & text) from dom element,currently support : td & th, using ONLY inline css style with RGB color
951
        $styles = explode(';', $attributeArray['style']);
17✔
952
        foreach ($styles as $st) {
17✔
953
            $value = explode(':', $st);
17✔
954
            $styleName = trim($value[0]);
17✔
955
            $styleValue = isset($value[1]) ? trim($value[1]) : null;
17✔
956
            $styleValueString = (string) $styleValue;
17✔
957

958
            if (!$styleName) {
17✔
959
                continue;
13✔
960
            }
961

962
            switch ($styleName) {
963
                case 'background':
17✔
964
                case 'background-color':
17✔
965
                    $styleColor = $this->getStyleColor($styleValueString);
3✔
966

967
                    if (!$styleColor) {
3✔
968
                        continue 2;
1✔
969
                    }
970

971
                    $cellStyle->applyFromArray(['fill' => ['fillType' => Fill::FILL_SOLID, 'color' => ['rgb' => $styleColor]]]);
3✔
972

973
                    break;
3✔
974
                case 'color':
17✔
975
                    $styleColor = $this->getStyleColor($styleValueString);
3✔
976

977
                    if (!$styleColor) {
3✔
978
                        continue 2;
1✔
979
                    }
980

981
                    $cellStyle->applyFromArray(['font' => ['color' => ['rgb' => $styleColor]]]);
3✔
982

983
                    break;
3✔
984

985
                case 'border':
14✔
986
                    $this->setBorderStyle($cellStyle, $styleValueString, 'allBorders');
3✔
987

988
                    break;
3✔
989

990
                case 'border-top':
12✔
991
                    $this->setBorderStyle($cellStyle, $styleValueString, 'top');
1✔
992

993
                    break;
1✔
994

995
                case 'border-bottom':
12✔
996
                    $this->setBorderStyle($cellStyle, $styleValueString, 'bottom');
1✔
997

998
                    break;
1✔
999

1000
                case 'border-left':
12✔
1001
                    $this->setBorderStyle($cellStyle, $styleValueString, 'left');
1✔
1002

1003
                    break;
1✔
1004

1005
                case 'border-right':
12✔
1006
                    $this->setBorderStyle($cellStyle, $styleValueString, 'right');
1✔
1007

1008
                    break;
1✔
1009

1010
                case 'font-size':
11✔
1011
                    $cellStyle->getFont()->setSize(
1✔
1012
                        (float) $styleValue
1✔
1013
                    );
1✔
1014

1015
                    break;
1✔
1016

1017
                case 'font-weight':
11✔
1018
                    if ($styleValue === 'bold' || $styleValue >= 500) {
1✔
1019
                        $cellStyle->getFont()->setBold(true);
1✔
1020
                    }
1021

1022
                    break;
1✔
1023

1024
                case 'font-style':
11✔
1025
                    if ($styleValue === 'italic') {
1✔
1026
                        $cellStyle->getFont()->setItalic(true);
1✔
1027
                    }
1028

1029
                    break;
1✔
1030

1031
                case 'font-family':
11✔
1032
                    $cellStyle->getFont()->setName(str_replace('\'', '', $styleValueString));
1✔
1033

1034
                    break;
1✔
1035

1036
                case 'text-decoration':
11✔
1037
                    switch ($styleValue) {
1038
                        case 'underline':
1✔
1039
                            $cellStyle->getFont()->setUnderline(Font::UNDERLINE_SINGLE);
1✔
1040

1041
                            break;
1✔
1042
                        case 'line-through':
1✔
1043
                            $cellStyle->getFont()->setStrikethrough(true);
1✔
1044

1045
                            break;
1✔
1046
                    }
1047

1048
                    break;
1✔
1049

1050
                case 'text-align':
10✔
1051
                    $cellStyle->getAlignment()->setHorizontal($styleValueString);
1✔
1052

1053
                    break;
1✔
1054

1055
                case 'vertical-align':
10✔
1056
                    $cellStyle->getAlignment()->setVertical($styleValueString);
2✔
1057

1058
                    break;
2✔
1059

1060
                case 'width':
10✔
1061
                    if ($column !== '') {
2✔
1062
                        $sheet->getColumnDimension($column)->setWidth(
2✔
1063
                            (new CssDimension($styleValue ?? ''))->width()
2✔
1064
                        );
2✔
1065
                    }
1066

1067
                    break;
2✔
1068

1069
                case 'height':
8✔
1070
                    if ($row > 0) {
1✔
1071
                        $sheet->getRowDimension($row)->setRowHeight(
1✔
1072
                            (new CssDimension($styleValue ?? ''))->height()
1✔
1073
                        );
1✔
1074
                    }
1075

1076
                    break;
1✔
1077

1078
                case 'word-wrap':
7✔
1079
                    $cellStyle->getAlignment()->setWrapText(
1✔
1080
                        $styleValue === 'break-word'
1✔
1081
                    );
1✔
1082

1083
                    break;
1✔
1084

1085
                case 'text-indent':
7✔
1086
                    $cellStyle->getAlignment()->setIndent(
2✔
1087
                        (int) str_replace(['px'], '', $styleValueString)
2✔
1088
                    );
2✔
1089

1090
                    break;
2✔
1091
            }
1092
        }
1093
    }
1094

1095
    /**
1096
     * Check if has #, so we can get clean hex.
1097
     */
1098
    public function getStyleColor(?string $value): string
1099
    {
1100
        $value = (string) $value;
7✔
1101
        if (str_starts_with($value, '#')) {
7✔
1102
            return substr($value, 1);
5✔
1103
        }
1104

1105
        return HelperHtml::colourNameLookup($value);
4✔
1106
    }
1107

1108
    /** @param string[] $attributes */
1109
    private function insertImage(Worksheet $sheet, string $column, int $row, array $attributes): void
1110
    {
1111
        if (!isset($attributes['src'])) {
20✔
1112
            return;
1✔
1113
        }
1114
        $styleArray = self::getStyleArray($attributes, null);
19✔
1115

1116
        $src = $attributes['src'];
19✔
1117
        if (substr($src, 0, 5) !== 'data:') {
19✔
1118
            $src = urldecode($src);
14✔
1119
        }
1120
        $width = isset($attributes['width']) ? (float) $attributes['width'] : ($styleArray['width'] ?? null);
19✔
1121
        $height = isset($attributes['height']) ? (float) $attributes['height'] : ($styleArray['height'] ?? null);
19✔
1122
        $name = $attributes['alt'] ?? null;
19✔
1123

1124
        $drawing = new Drawing();
19✔
1125
        $drawing->setPath($src, false, allowExternal: $this->allowExternalImages);
19✔
1126
        if ($drawing->getPath() === '') {
12✔
1127
            return;
3✔
1128
        }
1129
        $drawing->setWorksheet($sheet);
9✔
1130
        $drawing->setCoordinates($column . $row);
9✔
1131
        $drawing->setOffsetX(0);
9✔
1132
        $drawing->setOffsetY(10);
9✔
1133
        $drawing->setResizeProportional(true);
9✔
1134

1135
        if ($name) {
9✔
1136
            $drawing->setName($name);
8✔
1137
        }
1138

1139
        /** @var null|scalar $width */
1140
        /** @var null|scalar $height */
1141
        if ($width) {
9✔
1142
            if ($height) {
6✔
1143
                $drawing->setWidthAndHeight((int) $width, (int) $height);
3✔
1144
            } else {
1145
                $drawing->setWidth((int) $width);
3✔
1146
            }
1147
        } elseif ($height) {
3✔
1148
            $drawing->setHeight((int) $height);
1✔
1149
        }
1150

1151
        $sheet->getColumnDimension($column)->setWidth(
9✔
1152
            $drawing->getWidth() / 6
9✔
1153
        );
9✔
1154

1155
        $sheet->getRowDimension($row)->setRowHeight(
9✔
1156
            $drawing->getHeight() * 0.9
9✔
1157
        );
9✔
1158

1159
        if (isset($styleArray['opacity'])) {
9✔
1160
            $opacity = $styleArray['opacity'];
1✔
1161
            if (is_numeric($opacity)) {
1✔
1162
                $drawing->setOpacity((int) ($opacity * 100000));
1✔
1163
            }
1164
        }
1165
    }
1166

1167
    /**
1168
     * @param string[] $attributes
1169
     *
1170
     * @return mixed[]
1171
     */
1172
    private static function getStyleArray(array $attributes, ?string $defaultAbsoluteUnit = CssDimension::ABSOLUTE_DEFAULT_UNIT): array
1173
    {
1174
        $styleArray = [];
19✔
1175
        if (isset($attributes['style'])) {
19✔
1176
            $styles = explode(';', $attributes['style']);
5✔
1177
            foreach ($styles as $style) {
5✔
1178
                $value = explode(':', $style);
5✔
1179
                if (count($value) === 2) {
5✔
1180
                    $arrayKey = trim($value[0]);
5✔
1181
                    $arrayValue = trim($value[1]);
5✔
1182
                    if ($arrayKey === 'width') {
5✔
1183
                        if (substr($arrayValue, -2) === 'px') {
5✔
1184
                            $arrayValue = (string) (((float) substr($arrayValue, 0, -2)));
5✔
1185
                        } else {
NEW
1186
                            $arrayValue = (new CssDimension($arrayValue, $defaultAbsoluteUnit))->width();
×
1187
                        }
1188
                    } elseif ($arrayKey === 'height') {
5✔
1189
                        if (substr($arrayValue, -2) === 'px') {
3✔
1190
                            $arrayValue = substr($arrayValue, 0, -2);
3✔
1191
                        } else {
NEW
1192
                            $arrayValue = (new CssDimension($arrayValue, $defaultAbsoluteUnit))->height();
×
1193
                        }
1194
                    }
1195
                    $styleArray[$arrayKey] = $arrayValue;
5✔
1196
                }
1197
            }
1198
        }
1199

1200
        return $styleArray;
19✔
1201
    }
1202

1203
    private const BORDER_MAPPINGS = [
1204
        'dash-dot' => Border::BORDER_DASHDOT,
1205
        'dash-dot-dot' => Border::BORDER_DASHDOTDOT,
1206
        'dashed' => Border::BORDER_DASHED,
1207
        'dotted' => Border::BORDER_DOTTED,
1208
        'double' => Border::BORDER_DOUBLE,
1209
        'hair' => Border::BORDER_HAIR,
1210
        'medium' => Border::BORDER_MEDIUM,
1211
        'medium-dashed' => Border::BORDER_MEDIUMDASHED,
1212
        'medium-dash-dot' => Border::BORDER_MEDIUMDASHDOT,
1213
        'medium-dash-dot-dot' => Border::BORDER_MEDIUMDASHDOTDOT,
1214
        'none' => Border::BORDER_NONE,
1215
        'slant-dash-dot' => Border::BORDER_SLANTDASHDOT,
1216
        'solid' => Border::BORDER_THIN,
1217
        'thick' => Border::BORDER_THICK,
1218
    ];
1219

1220
    /** @return array<string, string> */
1221
    public static function getBorderMappings(): array
1222
    {
1223
        return self::BORDER_MAPPINGS;
15✔
1224
    }
1225

1226
    /**
1227
     * Map html border style to PhpSpreadsheet border style.
1228
     */
1229
    public function getBorderStyle(string $style): ?string
1230
    {
1231
        return self::BORDER_MAPPINGS[$style] ?? null;
3✔
1232
    }
1233

1234
    private function setBorderStyle(Style $cellStyle, string $styleValue, string $type): void
1235
    {
1236
        if (trim($styleValue) === Border::BORDER_NONE) {
3✔
1237
            $borderStyle = Border::BORDER_NONE;
1✔
1238
            $color = null;
1✔
1239
        } else {
1240
            $borderArray = explode(' ', $styleValue);
3✔
1241
            $borderCount = count($borderArray);
3✔
1242
            if ($borderCount >= 3) {
3✔
1243
                $borderStyle = $borderArray[1];
3✔
1244
                $color = $borderArray[2];
3✔
1245
            } else {
1246
                $borderStyle = $borderArray[0];
1✔
1247
                $color = $borderArray[1] ?? null;
1✔
1248
            }
1249
        }
1250

1251
        $cellStyle->applyFromArray([
3✔
1252
            'borders' => [
3✔
1253
                $type => [
3✔
1254
                    'borderStyle' => $this->getBorderStyle($borderStyle),
3✔
1255
                    'color' => ['rgb' => $this->getStyleColor($color)],
3✔
1256
                ],
3✔
1257
            ],
3✔
1258
        ]);
3✔
1259
    }
1260

1261
    /**
1262
     * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
1263
     *
1264
     * @return array<int, array{worksheetName: string, lastColumnLetter: string, lastColumnIndex: int, totalRows: int, totalColumns: int, sheetState: string}>
1265
     */
1266
    public function listWorksheetInfo(string $filename): array
1267
    {
1268
        $info = [];
1✔
1269
        $spreadsheet = $this->newSpreadsheet();
1✔
1270
        $this->loadIntoExisting($filename, $spreadsheet);
1✔
1271
        foreach ($spreadsheet->getAllSheets() as $sheet) {
1✔
1272
            $newEntry = ['worksheetName' => $sheet->getTitle()];
1✔
1273
            $newEntry['lastColumnLetter'] = $sheet->getHighestDataColumn();
1✔
1274
            $newEntry['lastColumnIndex'] = Coordinate::columnIndexFromString($sheet->getHighestDataColumn()) - 1;
1✔
1275
            $newEntry['totalRows'] = $sheet->getHighestDataRow();
1✔
1276
            $newEntry['totalColumns'] = $newEntry['lastColumnIndex'] + 1;
1✔
1277
            $newEntry['sheetState'] = Worksheet::SHEETSTATE_VISIBLE;
1✔
1278
            $info[] = $newEntry;
1✔
1279
        }
1280
        $spreadsheet->disconnectWorksheets();
1✔
1281

1282
        return $info;
1✔
1283
    }
1284
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc