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

PHPOffice / PhpSpreadsheet / 20525786658

26 Dec 2025 04:35PM UTC coverage: 96.105% (+0.003%) from 96.102%
20525786658

Pull #4760

github

web-flow
Merge dc4582b9e into 212fac60e
Pull Request #4760: Set All Locale Variables at Once in a Threadsafe Manner

42 of 42 new or added lines in 4 files covered. (100.0%)

1 existing line in 1 file now uncovered.

45847 of 47705 relevant lines covered (96.11%)

380.24 hits per line

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

99.83
/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\Alignment;
22
use PhpOffice\PhpSpreadsheet\Style\Border;
23
use PhpOffice\PhpSpreadsheet\Style\Color;
24
use PhpOffice\PhpSpreadsheet\Style\Fill;
25
use PhpOffice\PhpSpreadsheet\Style\Font;
26
use PhpOffice\PhpSpreadsheet\Style\Style;
27
use PhpOffice\PhpSpreadsheet\Worksheet\Drawing;
28
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
29
use Throwable;
30

31
class Html extends BaseReader
32
{
33
    /**
34
     * Sample size to read to determine if it's HTML or not.
35
     */
36
    const TEST_SAMPLE_SIZE = 2048;
37

38
    private const STARTS_WITH_BOM = '/^(?:\xfe\xff|\xff\xfe|\xEF\xBB\xBF)/';
39

40
    private const DECLARES_CHARSET = '/\bcharset=/i';
41

42
    /**
43
     * Input encoding.
44
     */
45
    protected string $inputEncoding = 'ANSI';
46

47
    /**
48
     * Sheet index to read.
49
     */
50
    protected int $sheetIndex = 0;
51

52
    /**
53
     * Formats.
54
     */
55
    protected const FORMATS = [
56
        'h1' => [
57
            'font' => [
58
                'bold' => true,
59
                'size' => 24,
60
            ],
61
        ], //    Bold, 24pt
62
        'h2' => [
63
            'font' => [
64
                'bold' => true,
65
                'size' => 18,
66
            ],
67
        ], //    Bold, 18pt
68
        'h3' => [
69
            'font' => [
70
                'bold' => true,
71
                'size' => 13.5,
72
            ],
73
        ], //    Bold, 13.5pt
74
        'h4' => [
75
            'font' => [
76
                'bold' => true,
77
                'size' => 12,
78
            ],
79
        ], //    Bold, 12pt
80
        'h5' => [
81
            'font' => [
82
                'bold' => true,
83
                'size' => 10,
84
            ],
85
        ], //    Bold, 10pt
86
        'h6' => [
87
            'font' => [
88
                'bold' => true,
89
                'size' => 7.5,
90
            ],
91
        ], //    Bold, 7.5pt
92
        'a' => [
93
            'font' => [
94
                'underline' => true,
95
                'color' => [
96
                    'argb' => Color::COLOR_BLUE,
97
                ],
98
            ],
99
        ], //    Blue underlined
100
        'hr' => [
101
            'borders' => [
102
                'bottom' => [
103
                    'borderStyle' => Border::BORDER_THIN,
104
                    'color' => [
105
                        Color::COLOR_BLACK,
106
                    ],
107
                ],
108
            ],
109
        ], //    Bottom border
110
        'strong' => [
111
            'font' => [
112
                'bold' => true,
113
            ],
114
        ], //    Bold
115
        'b' => [
116
            'font' => [
117
                'bold' => true,
118
            ],
119
        ], //    Bold
120
        'i' => [
121
            'font' => [
122
                'italic' => true,
123
            ],
124
        ], //    Italic
125
        'em' => [
126
            'font' => [
127
                'italic' => true,
128
            ],
129
        ], //    Italic
130
    ];
131

132
    /** @var array<string, bool> */
133
    protected array $rowspan = [];
134

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

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

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

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

162
        fclose($this->fileHandle);
483✔
163

164
        return $startWithTag && $containsTags && $endsWithTag;
483✔
165
    }
166

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

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

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

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

186
        $blockSize = self::TEST_SAMPLE_SIZE;
481✔
187
        if ($size < $blockSize) {
481✔
188
            $blockSize = $size;
28✔
189
        }
190

191
        fseek($this->fileHandle, $size - $blockSize);
481✔
192

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

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

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

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

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

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

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

231
    protected int $tableLevel = 0;
232

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

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

244
        return $this->nestedColumn[$this->tableLevel];
508✔
245
    }
246

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

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

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

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

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

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

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

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

335
        return $cellContent;
1✔
336
    }
337

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

525
    private string $currentColumn = 'A';
526

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

561
    /** @param string[] $attributeArray */
562
    private function processDomElementTr(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
509✔
563
    {
564
        if ($child->nodeName === 'col') {
509✔
565
            $this->applyInlineStyle($sheet, -1, $this->currentColumn, $attributeArray);
446✔
566
            StringHelper::stringIncrement($this->currentColumn);
446✔
567
        } elseif ($child->nodeName === 'tr') {
509✔
568
            $column = $this->getTableStartColumn();
504✔
569
            $cellContent = '';
504✔
570
            $this->processDomElement($child, $sheet, $row, $column, $cellContent);
504✔
571

572
            if (isset($attributeArray['height'])) {
497✔
573
                $sheet->getRowDimension($row)->setRowHeight((float) $attributeArray['height']);
1✔
574
            }
575

576
            ++$row;
497✔
577
        } else {
578
            $this->processDomElementThTdOther($sheet, $row, $column, $cellContent, $child, $attributeArray);
509✔
579
        }
580
    }
581

582
    /** @param string[] $attributeArray */
583
    private function processDomElementThTdOther(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
509✔
584
    {
585
        if ($child->nodeName !== 'td' && $child->nodeName !== 'th') {
509✔
586
            $this->processDomElement($child, $sheet, $row, $column, $cellContent);
509✔
587
        } else {
588
            $this->processDomElementThTd($sheet, $row, $column, $cellContent, $child, $attributeArray);
504✔
589
        }
590
    }
591

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

607
    /** @param string[] $attributeArray */
608
    private function processDomElementWidth(Worksheet $sheet, string $column, array $attributeArray): void
497✔
609
    {
610
        if (isset($attributeArray['width'])) {
497✔
611
            $sheet->getColumnDimension($column)->setWidth((new CssDimension($attributeArray['width']))->width());
1✔
612
        }
613
    }
614

615
    /** @param string[] $attributeArray */
616
    private function processDomElementHeight(Worksheet $sheet, int $row, array $attributeArray): void
497✔
617
    {
618
        if (isset($attributeArray['height'])) {
497✔
619
            $sheet->getRowDimension($row)->setRowHeight((new CssDimension($attributeArray['height']))->height());
1✔
620
        }
621
    }
622

623
    /** @param string[] $attributeArray */
624
    private function processDomElementAlign(Worksheet $sheet, int $row, string $column, array $attributeArray): void
497✔
625
    {
626
        if (isset($attributeArray['align'])) {
497✔
627
            $sheet->getStyle($column . $row)->getAlignment()->setHorizontal($attributeArray['align']);
1✔
628
        }
629
    }
630

631
    /** @param string[] $attributeArray */
632
    private function processDomElementVAlign(Worksheet $sheet, int $row, string $column, array $attributeArray): void
497✔
633
    {
634
        if (isset($attributeArray['valign'])) {
497✔
635
            $sheet->getStyle($column . $row)->getAlignment()->setVertical($attributeArray['valign']);
1✔
636
        }
637
    }
638

639
    /** @param string[] $attributeArray */
640
    private function processDomElementDataFormat(Worksheet $sheet, int $row, string $column, array $attributeArray): void
497✔
641
    {
642
        if (isset($attributeArray['data-format'])) {
497✔
643
            $sheet->getStyle($column . $row)->getNumberFormat()->setFormatCode($attributeArray['data-format']);
1✔
644
        }
645
    }
646

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

656
        // apply inline style
657
        $this->applyInlineStyle($sheet, $row, $column, $attributeArray);
497✔
658

659
        /** @var string $cellContent */
660
        $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
497✔
661

662
        $this->processDomElementBgcolor($sheet, $row, $column, $attributeArray);
497✔
663
        $this->processDomElementWidth($sheet, $column, $attributeArray);
497✔
664
        $this->processDomElementHeight($sheet, $row, $attributeArray);
497✔
665
        $this->processDomElementAlign($sheet, $row, $column, $attributeArray);
497✔
666
        $this->processDomElementVAlign($sheet, $row, $column, $attributeArray);
497✔
667
        $this->processDomElementDataFormat($sheet, $row, $column, $attributeArray);
497✔
668

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

698
        StringHelper::stringIncrement($column);
497✔
699
    }
700

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

719
    /**
720
     * Loads PhpSpreadsheet from file into PhpSpreadsheet instance.
721
     */
722
    public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Spreadsheet
464✔
723
    {
724
        // Validate
725
        if (!$this->canRead($filename)) {
464✔
726
            throw new Exception($filename . ' is an Invalid HTML file.');
1✔
727
        }
728

729
        // Create a new DOM object
730
        $dom = new DOMDocument();
463✔
731

732
        // Reload the HTML file into the DOM object
733
        try {
734
            $convert = $this->getSecurityScannerOrThrow()->scanFile($filename);
463✔
735
            $convert = static::replaceNonAsciiIfNeeded($convert);
462✔
736
            $loaded = ($convert === null) ? false : $dom->loadHTML($convert);
462✔
737
        } catch (Throwable $e) {
2✔
738
            $loaded = false;
2✔
739
        }
740
        if ($loaded === false) {
463✔
741
            throw new Exception('Failed to load file ' . $filename . ' as a DOM Document', 0, $e ?? null);
2✔
742
        }
743
        self::loadProperties($dom, $spreadsheet);
461✔
744

745
        return $this->loadDocument($dom, $spreadsheet);
461✔
746
    }
747

748
    private static function loadProperties(DOMDocument $dom, Spreadsheet $spreadsheet): void
509✔
749
    {
750
        $properties = $spreadsheet->getProperties();
509✔
751
        foreach ($dom->getElementsByTagName('meta') as $meta) {
509✔
752
            $metaContent = (string) $meta->getAttribute('content');
456✔
753
            if ($metaContent !== '') {
456✔
754
                $metaName = (string) $meta->getAttribute('name');
448✔
755
                switch ($metaName) {
756
                    case 'author':
448✔
757
                        $properties->setCreator($metaContent);
446✔
758

759
                        break;
446✔
760
                    case 'category':
448✔
761
                        $properties->setCategory($metaContent);
1✔
762

763
                        break;
1✔
764
                    case 'company':
448✔
765
                        $properties->setCompany($metaContent);
1✔
766

767
                        break;
1✔
768
                    case 'created':
448✔
769
                        $properties->setCreated($metaContent);
446✔
770

771
                        break;
446✔
772
                    case 'description':
448✔
773
                        $properties->setDescription($metaContent);
1✔
774

775
                        break;
1✔
776
                    case 'keywords':
448✔
777
                        $properties->setKeywords($metaContent);
1✔
778

779
                        break;
1✔
780
                    case 'lastModifiedBy':
448✔
781
                        $properties->setLastModifiedBy($metaContent);
446✔
782

783
                        break;
446✔
784
                    case 'manager':
448✔
785
                        $properties->setManager($metaContent);
1✔
786

787
                        break;
1✔
788
                    case 'modified':
448✔
789
                        $properties->setModified($metaContent);
446✔
790

791
                        break;
446✔
792
                    case 'subject':
448✔
793
                        $properties->setSubject($metaContent);
1✔
794

795
                        break;
1✔
796
                    case 'title':
448✔
797
                        $properties->setTitle($metaContent);
444✔
798

799
                        break;
444✔
800
                    case 'viewport':
448✔
801
                        $properties->setViewport($metaContent);
1✔
802

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

823
    /** @param string[] $matches */
824
    private static function replaceNonAscii(array $matches): string
21✔
825
    {
826
        return '&#' . mb_ord($matches[0], 'UTF-8') . ';';
21✔
827
    }
828

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

840
        return $convert;
510✔
841
    }
842

843
    /**
844
     * Spreadsheet from content.
845
     */
846
    public function loadFromString(string $content, ?Spreadsheet $spreadsheet = null): Spreadsheet
49✔
847
    {
848
        //    Create a new DOM object
849
        $dom = new DOMDocument();
49✔
850

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

866
        return $this->loadDocument($dom, $spreadsheet);
48✔
867
    }
868

869
    /**
870
     * Loads PhpSpreadsheet from DOMDocument into PhpSpreadsheet instance.
871
     */
872
    private function loadDocument(DOMDocument $document, Spreadsheet $spreadsheet): Spreadsheet
509✔
873
    {
874
        while ($spreadsheet->getSheetCount() <= $this->sheetIndex) {
509✔
875
            $spreadsheet->createSheet();
2✔
876
        }
877
        $spreadsheet->setActiveSheetIndex($this->sheetIndex);
509✔
878

879
        // Discard white space
880
        $document->preserveWhiteSpace = false;
509✔
881

882
        $row = 0;
509✔
883
        $column = 'A';
509✔
884
        $content = '';
509✔
885
        $this->rowspan = [];
509✔
886
        $this->processDomElement($document, $spreadsheet->getActiveSheet(), $row, $column, $content);
509✔
887

888
        // Return
889
        return $spreadsheet;
502✔
890
    }
891

892
    /**
893
     * Get sheet index.
894
     */
895
    public function getSheetIndex(): int
1✔
896
    {
897
        return $this->sheetIndex;
1✔
898
    }
899

900
    /**
901
     * Set sheet index.
902
     *
903
     * @param int $sheetIndex Sheet index
904
     *
905
     * @return $this
906
     */
907
    public function setSheetIndex(int $sheetIndex): static
2✔
908
    {
909
        $this->sheetIndex = $sheetIndex;
2✔
910

911
        return $this;
2✔
912
    }
913

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

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

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

963
            if (!$styleName) {
18✔
964
                continue;
13✔
965
            }
966

967
            switch ($styleName) {
968
                case 'background':
18✔
969
                case 'background-color':
18✔
970
                    $styleColor = $this->getStyleColor($styleValueString);
3✔
971

972
                    if (!$styleColor) {
3✔
973
                        continue 2;
1✔
974
                    }
975

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

978
                    break;
3✔
979
                case 'color':
18✔
980
                    $styleColor = $this->getStyleColor($styleValueString);
4✔
981

982
                    if (!$styleColor) {
4✔
983
                        continue 2;
1✔
984
                    }
985

986
                    $cellStyle->applyFromArray(['font' => ['color' => ['rgb' => $styleColor]]]);
4✔
987

988
                    break;
4✔
989

990
                case 'border':
15✔
991
                    $this->setBorderStyle($cellStyle, $styleValueString, 'allBorders');
3✔
992

993
                    break;
3✔
994

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

998
                    break;
1✔
999

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

1003
                    break;
1✔
1004

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

1008
                    break;
1✔
1009

1010
                case 'border-right':
13✔
1011
                    $this->setBorderStyle($cellStyle, $styleValueString, 'right');
1✔
1012

1013
                    break;
1✔
1014

1015
                case 'font-size':
12✔
1016
                    $cellStyle->getFont()->setSize(
2✔
1017
                        (float) $styleValue
2✔
1018
                    );
2✔
1019

1020
                    break;
2✔
1021

1022
                case 'direction':
12✔
1023
                    if ($styleValue === 'rtl') {
1✔
1024
                        $cellStyle->getAlignment()
1✔
1025
                            ->setReadOrder(Alignment::READORDER_RTL);
1✔
1026
                    } elseif ($styleValue === 'ltr') {
1✔
1027
                        $cellStyle->getAlignment()
1✔
1028
                            ->setReadOrder(Alignment::READORDER_LTR);
1✔
1029
                    }
1030

1031
                    break;
1✔
1032

1033
                case 'font-weight':
12✔
1034
                    if ($styleValue === 'bold' || $styleValue >= 500) {
1✔
1035
                        $cellStyle->getFont()->setBold(true);
1✔
1036
                    }
1037

1038
                    break;
1✔
1039

1040
                case 'font-style':
12✔
1041
                    if ($styleValue === 'italic') {
1✔
1042
                        $cellStyle->getFont()->setItalic(true);
1✔
1043
                    }
1044

1045
                    break;
1✔
1046

1047
                case 'font-family':
12✔
1048
                    $cellStyle->getFont()->setName(str_replace('\'', '', $styleValueString));
2✔
1049

1050
                    break;
2✔
1051

1052
                case 'text-decoration':
12✔
1053
                    switch ($styleValue) {
1054
                        case 'underline':
1✔
1055
                            $cellStyle->getFont()->setUnderline(Font::UNDERLINE_SINGLE);
1✔
1056

1057
                            break;
1✔
1058
                        case 'line-through':
1✔
1059
                            $cellStyle->getFont()->setStrikethrough(true);
1✔
1060

1061
                            break;
1✔
1062
                    }
1063

1064
                    break;
1✔
1065

1066
                case 'text-align':
11✔
1067
                    $cellStyle->getAlignment()->setHorizontal($styleValueString);
2✔
1068

1069
                    break;
2✔
1070

1071
                case 'vertical-align':
11✔
1072
                    $cellStyle->getAlignment()->setVertical($styleValueString);
3✔
1073

1074
                    break;
3✔
1075

1076
                case 'width':
11✔
1077
                    if ($column !== '') {
3✔
1078
                        $sheet->getColumnDimension($column)->setWidth(
3✔
1079
                            (new CssDimension($styleValue ?? ''))->width()
3✔
1080
                        );
3✔
1081
                    }
1082

1083
                    break;
3✔
1084

1085
                case 'height':
9✔
1086
                    if ($row > 0) {
1✔
1087
                        $sheet->getRowDimension($row)->setRowHeight(
1✔
1088
                            (new CssDimension($styleValue ?? ''))->height()
1✔
1089
                        );
1✔
1090
                    }
1091

1092
                    break;
1✔
1093

1094
                case 'word-wrap':
8✔
1095
                    $cellStyle->getAlignment()->setWrapText(
1✔
1096
                        $styleValue === 'break-word'
1✔
1097
                    );
1✔
1098

1099
                    break;
1✔
1100

1101
                case 'text-indent':
8✔
1102
                    $indentDimension = new CssDimension($styleValueString);
3✔
1103
                    $indent = $indentDimension
3✔
1104
                        ->toUnit(CssDimension::UOM_PIXELS);
3✔
1105
                    $cellStyle->getAlignment()->setIndent(
3✔
1106
                        (int) ($indent / Alignment::INDENT_UNITS_TO_PIXELS)
3✔
1107
                    );
3✔
1108

1109
                    break;
3✔
1110
            }
1111
        }
1112
    }
1113

1114
    /**
1115
     * Check if has #, so we can get clean hex.
1116
     */
1117
    public function getStyleColor(?string $value): string
8✔
1118
    {
1119
        $value = (string) $value;
8✔
1120
        if (str_starts_with($value, '#')) {
8✔
1121
            return substr($value, 1);
6✔
1122
        }
1123

1124
        return HelperHtml::colourNameLookup($value);
4✔
1125
    }
1126

1127
    /** @param string[] $attributes */
1128
    private function insertImage(Worksheet $sheet, string $column, int $row, array $attributes): void
20✔
1129
    {
1130
        if (!isset($attributes['src'])) {
20✔
1131
            return;
1✔
1132
        }
1133
        $styleArray = self::getStyleArray($attributes);
19✔
1134

1135
        $src = $attributes['src'];
19✔
1136
        if (!str_starts_with($src, 'data:')) {
19✔
1137
            $src = urldecode($src);
14✔
1138
        }
1139
        $width = isset($attributes['width']) ? (float) $attributes['width'] : ($styleArray['width'] ?? null);
19✔
1140
        $height = isset($attributes['height']) ? (float) $attributes['height'] : ($styleArray['height'] ?? null);
19✔
1141
        $name = $attributes['alt'] ?? null;
19✔
1142

1143
        $drawing = new Drawing();
19✔
1144
        $drawing->setPath($src, false, allowExternal: $this->allowExternalImages);
19✔
1145
        if ($drawing->getPath() === '') {
12✔
1146
            return;
3✔
1147
        }
1148
        $drawing->setWorksheet($sheet);
9✔
1149
        $drawing->setCoordinates($column . $row);
9✔
1150
        $drawing->setOffsetX(0);
9✔
1151
        $drawing->setOffsetY(10);
9✔
1152
        $drawing->setResizeProportional(true);
9✔
1153

1154
        if ($name) {
9✔
1155
            $drawing->setName($name);
8✔
1156
        }
1157

1158
        /** @var null|scalar $width */
1159
        /** @var null|scalar $height */
1160
        if ($width) {
9✔
1161
            if ($height) {
6✔
1162
                $drawing->setWidthAndHeight((int) $width, (int) $height);
3✔
1163
            } else {
1164
                $drawing->setWidth((int) $width);
3✔
1165
            }
1166
        } elseif ($height) {
3✔
1167
            $drawing->setHeight((int) $height);
1✔
1168
        }
1169

1170
        $sheet->getColumnDimension($column)->setWidth(
9✔
1171
            $drawing->getWidth() / 6
9✔
1172
        );
9✔
1173

1174
        $sheet->getRowDimension($row)->setRowHeight(
9✔
1175
            $drawing->getHeight() * 0.9
9✔
1176
        );
9✔
1177

1178
        if (isset($styleArray['opacity'])) {
9✔
1179
            $opacity = $styleArray['opacity'];
1✔
1180
            if (is_numeric($opacity)) {
1✔
1181
                $drawing->setOpacity((int) ($opacity * 100000));
1✔
1182
            }
1183
        }
1184
    }
1185

1186
    /**
1187
     * @param string[] $attributes
1188
     *
1189
     * @return mixed[]
1190
     */
1191
    private static function getStyleArray(array $attributes): array
19✔
1192
    {
1193
        $styleArray = [];
19✔
1194
        if (isset($attributes['style'])) {
19✔
1195
            $styles = explode(';', $attributes['style']);
5✔
1196
            foreach ($styles as $style) {
5✔
1197
                $value = explode(':', $style);
5✔
1198
                if (count($value) === 2) {
5✔
1199
                    $arrayKey = trim($value[0]);
5✔
1200
                    $arrayValue = trim($value[1]);
5✔
1201
                    if ($arrayKey === 'width') {
5✔
1202
                        if (str_ends_with($arrayValue, 'px')) {
5✔
1203
                            $arrayValue = (string) (((float) substr($arrayValue, 0, -2)));
4✔
1204
                        } else {
1205
                            $arrayValue = (new CssDimension($arrayValue))->toUnit(CssDimension::UOM_PIXELS);
1✔
1206
                        }
1207
                    } elseif ($arrayKey === 'height') {
5✔
1208
                        if (str_ends_with($arrayValue, 'px')) {
3✔
1209
                            $arrayValue = substr($arrayValue, 0, -2);
2✔
1210
                        } else {
1211
                            $arrayValue = (new CssDimension($arrayValue))->toUnit(CssDimension::UOM_PIXELS);
1✔
1212
                        }
1213
                    }
1214
                    $styleArray[$arrayKey] = $arrayValue;
5✔
1215
                }
1216
            }
1217
        }
1218

1219
        return $styleArray;
19✔
1220
    }
1221

1222
    private const BORDER_MAPPINGS = [
1223
        'dash-dot' => Border::BORDER_DASHDOT,
1224
        'dash-dot-dot' => Border::BORDER_DASHDOTDOT,
1225
        'dashed' => Border::BORDER_DASHED,
1226
        'dotted' => Border::BORDER_DOTTED,
1227
        'double' => Border::BORDER_DOUBLE,
1228
        'hair' => Border::BORDER_HAIR,
1229
        'medium' => Border::BORDER_MEDIUM,
1230
        'medium-dashed' => Border::BORDER_MEDIUMDASHED,
1231
        'medium-dash-dot' => Border::BORDER_MEDIUMDASHDOT,
1232
        'medium-dash-dot-dot' => Border::BORDER_MEDIUMDASHDOTDOT,
1233
        'none' => Border::BORDER_NONE,
1234
        'slant-dash-dot' => Border::BORDER_SLANTDASHDOT,
1235
        'solid' => Border::BORDER_THIN,
1236
        'thick' => Border::BORDER_THICK,
1237
    ];
1238

1239
    /** @return array<string, string> */
1240
    public static function getBorderMappings(): array
15✔
1241
    {
1242
        return self::BORDER_MAPPINGS;
15✔
1243
    }
1244

1245
    /**
1246
     * Map html border style to PhpSpreadsheet border style.
1247
     */
1248
    public function getBorderStyle(string $style): ?string
3✔
1249
    {
1250
        return self::BORDER_MAPPINGS[$style] ?? null;
3✔
1251
    }
1252

1253
    private function setBorderStyle(Style $cellStyle, string $styleValue, string $type): void
3✔
1254
    {
1255
        if (trim($styleValue) === Border::BORDER_NONE) {
3✔
1256
            $borderStyle = Border::BORDER_NONE;
1✔
1257
            $color = null;
1✔
1258
        } else {
1259
            $borderArray = explode(' ', $styleValue);
3✔
1260
            $borderCount = count($borderArray);
3✔
1261
            if ($borderCount >= 3) {
3✔
1262
                $borderStyle = $borderArray[1];
3✔
1263
                $color = $borderArray[2];
3✔
1264
            } else {
1265
                $borderStyle = $borderArray[0];
1✔
1266
                $color = $borderArray[1] ?? null;
1✔
1267
            }
1268
        }
1269

1270
        $cellStyle->applyFromArray([
3✔
1271
            'borders' => [
3✔
1272
                $type => [
3✔
1273
                    'borderStyle' => $this->getBorderStyle($borderStyle),
3✔
1274
                    'color' => ['rgb' => $this->getStyleColor($color)],
3✔
1275
                ],
3✔
1276
            ],
3✔
1277
        ]);
3✔
1278
    }
1279

1280
    /**
1281
     * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
1282
     *
1283
     * @return array<int, array{worksheetName: string, lastColumnLetter: string, lastColumnIndex: int, totalRows: int, totalColumns: int, sheetState: string}>
1284
     */
1285
    public function listWorksheetInfo(string $filename): array
1✔
1286
    {
1287
        $info = [];
1✔
1288
        $spreadsheet = $this->newSpreadsheet();
1✔
1289
        $this->loadIntoExisting($filename, $spreadsheet);
1✔
1290
        foreach ($spreadsheet->getAllSheets() as $sheet) {
1✔
1291
            $newEntry = ['worksheetName' => $sheet->getTitle()];
1✔
1292
            $newEntry['lastColumnLetter'] = $sheet->getHighestDataColumn();
1✔
1293
            $newEntry['lastColumnIndex'] = Coordinate::columnIndexFromString($sheet->getHighestDataColumn()) - 1;
1✔
1294
            $newEntry['totalRows'] = $sheet->getHighestDataRow();
1✔
1295
            $newEntry['totalColumns'] = $newEntry['lastColumnIndex'] + 1;
1✔
1296
            $newEntry['sheetState'] = Worksheet::SHEETSTATE_VISIBLE;
1✔
1297
            $info[] = $newEntry;
1✔
1298
        }
1299
        $spreadsheet->disconnectWorksheets();
1✔
1300

1301
        return $info;
1✔
1302
    }
1303
}
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