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

PHPOffice / PhpSpreadsheet / 20683156902

03 Jan 2026 09:27PM UTC coverage: 96.169% (+0.002%) from 96.167%
20683156902

Pull #4771

github

web-flow
Merge b60ffd09d into 0544868f9
Pull Request #4771: Fix Some Hyperlink Problems

51 of 52 new or added lines in 6 files covered. (98.08%)

1 existing line in 1 file now uncovered.

46035 of 47869 relevant lines covered (96.17%)

386.05 hits per line

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

99.84
/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 LibXMLError;
11
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
12
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
13
use PhpOffice\PhpSpreadsheet\Cell\DataType;
14
use PhpOffice\PhpSpreadsheet\Comment;
15
use PhpOffice\PhpSpreadsheet\Document\Properties;
16
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
17
use PhpOffice\PhpSpreadsheet\Helper\Dimension as CssDimension;
18
use PhpOffice\PhpSpreadsheet\Helper\Html as HelperHtml;
19
use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner;
20
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
21
use PhpOffice\PhpSpreadsheet\Spreadsheet;
22
use PhpOffice\PhpSpreadsheet\Style\Alignment;
23
use PhpOffice\PhpSpreadsheet\Style\Border;
24
use PhpOffice\PhpSpreadsheet\Style\Color;
25
use PhpOffice\PhpSpreadsheet\Style\Fill;
26
use PhpOffice\PhpSpreadsheet\Style\Font;
27
use PhpOffice\PhpSpreadsheet\Style\Style;
28
use PhpOffice\PhpSpreadsheet\Worksheet\Drawing;
29
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
30
use Throwable;
31

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

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

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

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

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

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

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

136
    /**
137
     * Default setting uses current setting of libxml_use_internal_errors.
138
     * It will probably change to 'true' in a future release.
139
     */
140
    protected ?bool $suppressLoadWarnings = null;
141

142
    /** @var LibXMLError[] */
143
    protected array $libxmlMessages = [];
144

145
    /**
146
     * Suppress load warning messages, keeping them available
147
     * in $this->libxmlMessages().
148
     */
149
    public function setSuppressLoadWarnings(?bool $suppressLoadWarnings): self
4✔
150
    {
151
        $this->suppressLoadWarnings = $suppressLoadWarnings;
4✔
152

153
        return $this;
4✔
154
    }
155

156
    /** @return LibXMLError[] */
157
    public function getLibxmlMessages(): array
5✔
158
    {
159
        return $this->libxmlMessages;
5✔
160
    }
161

162
    /**
163
     * Create a new HTML Reader instance.
164
     */
165
    public function __construct()
540✔
166
    {
167
        parent::__construct();
540✔
168
        $this->securityScanner = XmlScanner::getInstance($this);
540✔
169
    }
170

171
    /**
172
     * Validate that the current file is an HTML file.
173
     */
174
    public function canRead(string $filename): bool
486✔
175
    {
176
        // Check if file exists
177
        try {
178
            $this->openFile($filename);
486✔
179
        } catch (Exception) {
1✔
180
            return false;
1✔
181
        }
182

183
        $beginning = preg_replace(self::STARTS_WITH_BOM, '', $this->readBeginning()) ?? '';
485✔
184

185
        $startWithTag = self::startsWithTag($beginning);
485✔
186
        $containsTags = self::containsTags($beginning);
485✔
187
        $endsWithTag = self::endsWithTag($this->readEnding());
485✔
188

189
        fclose($this->fileHandle);
485✔
190

191
        return $startWithTag && $containsTags && $endsWithTag;
485✔
192
    }
193

194
    private function readBeginning(): string
485✔
195
    {
196
        fseek($this->fileHandle, 0);
485✔
197

198
        return (string) fread($this->fileHandle, self::TEST_SAMPLE_SIZE);
485✔
199
    }
200

201
    private function readEnding(): string
485✔
202
    {
203
        $meta = stream_get_meta_data($this->fileHandle);
485✔
204
        // Phpstan incorrectly flags following line for Php8.2-, corrected in 8.3
205
        $filename = $meta['uri']; //@phpstan-ignore-line
485✔
206

207
        clearstatcache(true, $filename);
485✔
208
        $size = (int) filesize($filename);
485✔
209
        if ($size === 0) {
485✔
210
            return '';
2✔
211
        }
212

213
        $blockSize = self::TEST_SAMPLE_SIZE;
483✔
214
        if ($size < $blockSize) {
483✔
215
            $blockSize = $size;
30✔
216
        }
217

218
        fseek($this->fileHandle, $size - $blockSize);
483✔
219

220
        return (string) fread($this->fileHandle, $blockSize);
483✔
221
    }
222

223
    private static function startsWithTag(string $data): bool
485✔
224
    {
225
        return str_starts_with(trim($data), '<');
485✔
226
    }
227

228
    private static function endsWithTag(string $data): bool
485✔
229
    {
230
        return str_ends_with(trim($data), '>');
485✔
231
    }
232

233
    private static function containsTags(string $data): bool
485✔
234
    {
235
        return strlen($data) !== strlen(strip_tags($data));
485✔
236
    }
237

238
    /**
239
     * Loads Spreadsheet from file.
240
     */
241
    public function loadSpreadsheetFromFile(string $filename): Spreadsheet
466✔
242
    {
243
        $spreadsheet = $this->newSpreadsheet();
466✔
244
        $spreadsheet->setValueBinder($this->valueBinder);
466✔
245

246
        // Load into this instance
247
        return $this->loadIntoExisting($filename, $spreadsheet);
466✔
248
    }
249

250
    /**
251
     * Data Array used for testing only, should write to
252
     * Spreadsheet object on completion of tests.
253
     *
254
     * @deprecated 5.4.0 No replacement.
255
     *
256
     * @var mixed[][]
257
     */
258
    protected array $dataArray = [];
259

260
    protected int $tableLevel = 0;
261

262
    /** @var string[] */
263
    protected array $nestedColumn = ['A'];
264

265
    protected function setTableStartColumn(string $column): string
511✔
266
    {
267
        if ($this->tableLevel == 0) {
511✔
268
            $column = 'A';
511✔
269
        }
270
        ++$this->tableLevel;
511✔
271
        $this->nestedColumn[$this->tableLevel] = $column;
511✔
272

273
        return $this->nestedColumn[$this->tableLevel];
511✔
274
    }
275

276
    protected function getTableStartColumn(): string
505✔
277
    {
278
        return $this->nestedColumn[$this->tableLevel];
505✔
279
    }
280

281
    protected function releaseTableStartColumn(): string
504✔
282
    {
283
        --$this->tableLevel;
504✔
284

285
        return array_pop($this->nestedColumn) ?? '';
504✔
286
    }
287

288
    /**
289
     * Flush cell.
290
     *
291
     * @param string[] $attributeArray
292
     *
293
     * @param-out string $cellContent In one case, it can be bool
294
     */
295
    protected function flushCell(Worksheet $sheet, string $column, int|string $row, mixed &$cellContent, array $attributeArray): void
512✔
296
    {
297
        if (is_string($cellContent)) {
512✔
298
            //    Simple String content
299
            if (trim($cellContent) > '') {
512✔
300
                //    Only actually write it if there's content in the string
301
                //    Write to worksheet to be done here...
302
                //    ... we return the cell, so we can mess about with styles more easily
303

304
                // Set cell value explicitly if there is data-type attribute
305
                if (isset($attributeArray['data-type'])) {
487✔
306
                    $datatype = $attributeArray['data-type'];
5✔
307
                    if (in_array($datatype, [DataType::TYPE_STRING, DataType::TYPE_STRING2, DataType::TYPE_INLINE])) {
5✔
308
                        //Prevent to Excel treat string with beginning equal sign or convert big numbers to scientific number
309
                        if (str_starts_with($cellContent, '=')) {
5✔
310
                            $sheet->getCell($column . $row)
1✔
311
                                ->getStyle()
1✔
312
                                ->setQuotePrefix(true);
1✔
313
                        }
314
                    }
315
                    if ($datatype === DataType::TYPE_BOOL) {
5✔
316
                        // This is the case where we can set cellContent to bool rather than string
317
                        $cellContent = self::convertBoolean($cellContent); //* @phpstan-ignore-line
5✔
318
                        if (!is_bool($cellContent)) {
5✔
319
                            $attributeArray['data-type'] = DataType::TYPE_STRING;
1✔
320
                        }
321
                    }
322

323
                    //catching the Exception and ignoring the invalid data types
324
                    $hyperlink = null;
5✔
325
                    if ($sheet->hyperlinkExists($column . $row)) {
5✔
NEW
UNCOV
326
                        $hyperlink = $sheet->getHyperlink($column . $row);
×
327
                    }
328

329
                    try {
330
                        $sheet->setCellValueExplicit($column . $row, $cellContent, $attributeArray['data-type']);
5✔
331
                    } catch (SpreadsheetException) {
1✔
332
                        $sheet->setCellValue($column . $row, $cellContent);
1✔
333
                    }
334
                    $sheet->setHyperlink($column . $row, $hyperlink);
5✔
335
                } else {
336
                    $hyperlink = null;
486✔
337
                    if ($sheet->hyperlinkExists($column . $row)) {
486✔
338
                        $hyperlink = $sheet->getHyperlink($column . $row);
3✔
339
                    }
340
                    $sheet->setCellValue($column . $row, $cellContent);
486✔
341
                    $sheet->setHyperlink($column . $row, $hyperlink);
486✔
342
                }
343
                $this->dataArray[$row][$column] = $cellContent; // @phpstan-ignore-line
487✔
344
            }
345
        } else {
346
            //    We have a Rich Text run.
347
            //    I don't actually see any way to reach this line.
348
            //    TODO
349
            // @phpstan-ignore-next-line
350
            $this->dataArray[$row][$column] = 'RICH TEXT: ' . StringHelper::convertToString($cellContent); // @codeCoverageIgnore
351
        }
352
        $cellContent = (string) '';
512✔
353
    }
354

355
    /** @var array<int, array<int, string>> */
356
    private static array $falseTrueArray = [];
357

358
    private static function convertBoolean(?string $cellContent): bool|string
5✔
359
    {
360
        if ($cellContent === '1') {
5✔
361
            return true;
1✔
362
        }
363
        if ($cellContent === '0' || $cellContent === '' || $cellContent === null) {
5✔
364
            return false;
1✔
365
        }
366
        if (empty(self::$falseTrueArray)) {
4✔
367
            $calc = Calculation::getInstance();
1✔
368
            self::$falseTrueArray = $calc->getFalseTrueArray();
1✔
369
        }
370
        if (in_array(mb_strtoupper($cellContent), self::$falseTrueArray[1], true)) {
4✔
371
            return true;
4✔
372
        }
373
        if (in_array(mb_strtoupper($cellContent), self::$falseTrueArray[0], true)) {
4✔
374
            return false;
4✔
375
        }
376

377
        return $cellContent;
1✔
378
    }
379

380
    private function processDomElementBody(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child): void
512✔
381
    {
382
        $attributeArray = [];
512✔
383
        /** @var DOMAttr $attribute */
384
        foreach (($child->attributes ?? []) as $attribute) {
512✔
385
            $attributeArray[$attribute->name] = $attribute->value;
496✔
386
        }
387

388
        if ($child->nodeName === 'body') {
512✔
389
            $row = 1;
512✔
390
            $column = 'A';
512✔
391
            $cellContent = '';
512✔
392
            $this->tableLevel = 0;
512✔
393
            $this->processDomElement($child, $sheet, $row, $column, $cellContent);
512✔
394
        } else {
395
            $this->processDomElementTitle($sheet, $row, $column, $cellContent, $child, $attributeArray);
512✔
396
        }
397
    }
398

399
    /** @param string[] $attributeArray */
400
    private function processDomElementTitle(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
512✔
401
    {
402
        if ($child->nodeName === 'title') {
512✔
403
            $this->processDomElement($child, $sheet, $row, $column, $cellContent);
464✔
404

405
            try {
406
                $sheet->setTitle($cellContent, true, true);
464✔
407
                $sheet->getParent()?->getProperties()?->setTitle($cellContent);
460✔
408
            } catch (SpreadsheetException) {
4✔
409
                // leave default title if too long or illegal chars
410
            }
411
            $cellContent = '';
464✔
412
        } else {
413
            $this->processDomElementSpanEtc($sheet, $row, $column, $cellContent, $child, $attributeArray);
512✔
414
        }
415
    }
416

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

419
    /** @param string[] $attributeArray */
420
    private function processDomElementSpanEtc(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
512✔
421
    {
422
        if (in_array((string) $child->nodeName, self::SPAN_ETC, true)) {
512✔
423
            if (isset($attributeArray['class']) && $attributeArray['class'] === 'comment') {
448✔
424
                $sheet->getComment($column . $row)
9✔
425
                    ->getText()
9✔
426
                    ->createTextRun($child->textContent);
9✔
427
                if (isset($attributeArray['dir']) && $attributeArray['dir'] === 'rtl') {
9✔
428
                    $sheet->getComment($column . $row)->setTextboxDirection(Comment::TEXTBOX_DIRECTION_RTL);
1✔
429
                }
430
                if (isset($attributeArray['style'])) {
9✔
431
                    $alignStyle = $attributeArray['style'];
2✔
432
                    if (preg_match('/\btext-align:\s*(left|right|center|justify)\b/', (string) $alignStyle, $matches) === 1) {
2✔
433
                        $sheet->getComment($column . $row)->setAlignment($matches[1]);
2✔
434
                    }
435
                }
436
            } else {
437
                $this->processDomElement($child, $sheet, $row, $column, $cellContent);
448✔
438
            }
439

440
            if (isset(self::FORMATS[$child->nodeName])) {
448✔
441
                $sheet->getStyle($column . $row)->applyFromArray(self::FORMATS[$child->nodeName]);
2✔
442
            }
443
        } else {
444
            $this->processDomElementHr($sheet, $row, $column, $cellContent, $child, $attributeArray);
512✔
445
        }
446
    }
447

448
    /** @param string[] $attributeArray */
449
    private function processDomElementHr(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
512✔
450
    {
451
        if ($child->nodeName === 'hr') {
512✔
452
            $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
1✔
453
            ++$row;
1✔
454
            $sheet->getStyle($column . $row)->applyFromArray(self::FORMATS[$child->nodeName]);
1✔
455
            ++$row;
1✔
456
        }
457
        // fall through to br
458
        $this->processDomElementBr($sheet, $row, $column, $cellContent, $child, $attributeArray);
512✔
459
    }
460

461
    /** @param string[] $attributeArray */
462
    private function processDomElementBr(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
512✔
463
    {
464
        if ($child->nodeName === 'br' || $child->nodeName === 'hr') {
512✔
465
            if ($this->tableLevel > 0) {
4✔
466
                //    If we're inside a table, replace with a newline and set the cell to wrap
467
                $cellContent .= "\n";
4✔
468
                $sheet->getStyle($column . $row)->getAlignment()->setWrapText(true);
4✔
469
            } else {
470
                //    Otherwise flush our existing content and move the row cursor on
471
                $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
1✔
472
                ++$row;
1✔
473
            }
474
        } else {
475
            $this->processDomElementA($sheet, $row, $column, $cellContent, $child, $attributeArray);
512✔
476
        }
477
    }
478

479
    /** @param string[] $attributeArray */
480
    private function processDomElementA(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
512✔
481
    {
482
        if ($child->nodeName === 'a') {
512✔
483
            foreach ($attributeArray as $attributeName => $attributeValue) {
12✔
484
                switch ($attributeName) {
485
                    case 'href':
12✔
486
                        $sheet->getCell($column . $row)->getHyperlink()->setUrl($attributeValue);
3✔
487
                        $sheet->getStyle($column . $row)->applyFromArray(self::FORMATS[$child->nodeName]);
3✔
488

489
                        break;
3✔
490
                    case 'class':
10✔
491
                        if ($attributeValue === 'comment-indicator') {
9✔
492
                            break; // Ignore - it's just a red square.
9✔
493
                        }
494
                }
495
            }
496
            // no idea why this should be needed
497
            //$cellContent .= ' ';
498
            $this->processDomElement($child, $sheet, $row, $column, $cellContent);
12✔
499
        } else {
500
            $this->processDomElementH1Etc($sheet, $row, $column, $cellContent, $child, $attributeArray);
512✔
501
        }
502
    }
503

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

506
    /** @param string[] $attributeArray */
507
    private function processDomElementH1Etc(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
512✔
508
    {
509
        if (in_array((string) $child->nodeName, self::H1_ETC, true)) {
512✔
510
            if ($this->tableLevel > 0) {
2✔
511
                //    If we're inside a table, replace with a newline
512
                $cellContent .= $cellContent ? "\n" : '';
1✔
513
                $sheet->getStyle($column . $row)->getAlignment()->setWrapText(true);
1✔
514
                $this->processDomElement($child, $sheet, $row, $column, $cellContent);
1✔
515
            } else {
516
                if ($cellContent > '') {
2✔
517
                    $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
1✔
518
                    ++$row;
1✔
519
                }
520
                $this->processDomElement($child, $sheet, $row, $column, $cellContent);
2✔
521
                $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
2✔
522

523
                if (isset(self::FORMATS[$child->nodeName])) {
2✔
524
                    $sheet->getStyle($column . $row)->applyFromArray(self::FORMATS[$child->nodeName]);
1✔
525
                }
526

527
                ++$row;
2✔
528
                $column = 'A';
2✔
529
            }
530
        } else {
531
            $this->processDomElementLi($sheet, $row, $column, $cellContent, $child, $attributeArray);
512✔
532
        }
533
    }
534

535
    /** @param string[] $attributeArray */
536
    private function processDomElementLi(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
512✔
537
    {
538
        if ($child->nodeName === 'li') {
512✔
539
            if ($this->tableLevel > 0) {
2✔
540
                //    If we're inside a table, replace with a newline
541
                $cellContent .= $cellContent ? "\n" : '';
1✔
542
                $this->processDomElement($child, $sheet, $row, $column, $cellContent);
1✔
543
            } else {
544
                if ($cellContent > '') {
2✔
545
                    $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
1✔
546
                }
547
                ++$row;
2✔
548
                $this->processDomElement($child, $sheet, $row, $column, $cellContent);
2✔
549
                $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
2✔
550
                $column = 'A';
2✔
551
            }
552
        } else {
553
            $this->processDomElementImg($sheet, $row, $column, $cellContent, $child, $attributeArray);
512✔
554
        }
555
    }
556

557
    /** @param string[] $attributeArray */
558
    private function processDomElementImg(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
512✔
559
    {
560
        if ($child->nodeName === 'img') {
512✔
561
            $this->insertImage($sheet, $column, $row, $attributeArray);
20✔
562
        } else {
563
            $this->processDomElementTable($sheet, $row, $column, $cellContent, $child, $attributeArray);
512✔
564
        }
565
    }
566

567
    private string $currentColumn = 'A';
568

569
    /** @param string[] $attributeArray */
570
    private function processDomElementTable(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
512✔
571
    {
572
        if ($child->nodeName === 'table') {
512✔
573
            if (isset($attributeArray['class'])) {
511✔
574
                $classes = explode(' ', $attributeArray['class']);
451✔
575
                $sheet->setShowGridlines(in_array('gridlines', $classes, true));
451✔
576
                $sheet->setPrintGridlines(in_array('gridlinesp', $classes, true));
451✔
577
            }
578
            if (isset($attributeArray['data-printarea'])) {
511✔
579
                $sheet->getPageSetup()
1✔
580
                    ->setPrintArea($attributeArray['data-printarea']);
1✔
581
            }
582
            if ('rtl' === ($attributeArray['dir'] ?? '')) {
511✔
583
                $sheet->setRightToLeft(true);
2✔
584
            }
585
            $this->currentColumn = 'A';
511✔
586
            $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
511✔
587
            $column = $this->setTableStartColumn($column);
511✔
588
            if ($this->tableLevel > 1 && $row > 1) {
511✔
589
                --$row;
2✔
590
            }
591
            $this->processDomElement($child, $sheet, $row, $column, $cellContent);
511✔
592
            $column = $this->releaseTableStartColumn();
504✔
593
            if ($this->tableLevel > 1) {
504✔
594
                StringHelper::stringIncrement($column);
2✔
595
            } else {
596
                ++$row;
504✔
597
            }
598
        } else {
599
            $this->processDomElementTr($sheet, $row, $column, $cellContent, $child, $attributeArray);
512✔
600
        }
601
    }
602

603
    /** @param string[] $attributeArray */
604
    private function processDomElementTr(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
512✔
605
    {
606
        if ($child->nodeName === 'col') {
512✔
607
            $this->applyInlineStyle($sheet, -1, $this->currentColumn, $attributeArray);
446✔
608
            StringHelper::stringIncrement($this->currentColumn);
446✔
609
        } elseif ($child->nodeName === 'tr') {
512✔
610
            $column = $this->getTableStartColumn();
505✔
611
            $cellContent = '';
505✔
612
            $this->processDomElement($child, $sheet, $row, $column, $cellContent);
505✔
613

614
            if (isset($attributeArray['height'])) {
498✔
615
                $sheet->getRowDimension($row)->setRowHeight((float) $attributeArray['height']);
1✔
616
            }
617

618
            ++$row;
498✔
619
        } else {
620
            $this->processDomElementThTdOther($sheet, $row, $column, $cellContent, $child, $attributeArray);
512✔
621
        }
622
    }
623

624
    /** @param string[] $attributeArray */
625
    private function processDomElementThTdOther(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
512✔
626
    {
627
        if ($child->nodeName !== 'td' && $child->nodeName !== 'th') {
512✔
628
            $this->processDomElement($child, $sheet, $row, $column, $cellContent);
512✔
629
        } else {
630
            $this->processDomElementThTd($sheet, $row, $column, $cellContent, $child, $attributeArray);
505✔
631
        }
632
    }
633

634
    /** @param string[] $attributeArray */
635
    private function processDomElementBgcolor(Worksheet $sheet, int $row, string $column, array $attributeArray): void
498✔
636
    {
637
        if (isset($attributeArray['bgcolor'])) {
498✔
638
            $sheet->getStyle("$column$row")->applyFromArray(
1✔
639
                [
1✔
640
                    'fill' => [
1✔
641
                        'fillType' => Fill::FILL_SOLID,
1✔
642
                        'color' => ['rgb' => $this->getStyleColor($attributeArray['bgcolor'])],
1✔
643
                    ],
1✔
644
                ]
1✔
645
            );
1✔
646
        }
647
    }
648

649
    /** @param string[] $attributeArray */
650
    private function processDomElementWidth(Worksheet $sheet, string $column, array $attributeArray): void
498✔
651
    {
652
        if (isset($attributeArray['width'])) {
498✔
653
            $sheet->getColumnDimension($column)->setWidth((new CssDimension($attributeArray['width']))->width());
1✔
654
        }
655
    }
656

657
    /** @param string[] $attributeArray */
658
    private function processDomElementHeight(Worksheet $sheet, int $row, array $attributeArray): void
498✔
659
    {
660
        if (isset($attributeArray['height'])) {
498✔
661
            $sheet->getRowDimension($row)->setRowHeight((new CssDimension($attributeArray['height']))->height());
1✔
662
        }
663
    }
664

665
    /** @param string[] $attributeArray */
666
    private function processDomElementAlign(Worksheet $sheet, int $row, string $column, array $attributeArray): void
498✔
667
    {
668
        if (isset($attributeArray['align'])) {
498✔
669
            $sheet->getStyle($column . $row)->getAlignment()->setHorizontal($attributeArray['align']);
1✔
670
        }
671
    }
672

673
    /** @param string[] $attributeArray */
674
    private function processDomElementVAlign(Worksheet $sheet, int $row, string $column, array $attributeArray): void
498✔
675
    {
676
        if (isset($attributeArray['valign'])) {
498✔
677
            $sheet->getStyle($column . $row)->getAlignment()->setVertical($attributeArray['valign']);
1✔
678
        }
679
    }
680

681
    /** @param string[] $attributeArray */
682
    private function processDomElementDataFormat(Worksheet $sheet, int $row, string $column, array $attributeArray): void
498✔
683
    {
684
        if (isset($attributeArray['data-format'])) {
498✔
685
            $sheet->getStyle($column . $row)->getNumberFormat()->setFormatCode($attributeArray['data-format']);
1✔
686
        }
687
    }
688

689
    /** @param string[] $attributeArray */
690
    private function processDomElementThTd(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
505✔
691
    {
692
        while (isset($this->rowspan[$column . $row])) {
505✔
693
            $temp = (string) $column;
3✔
694
            $column = StringHelper::stringIncrement($temp);
3✔
695
        }
696
        $this->processDomElement($child, $sheet, $row, $column, $cellContent);
505✔
697

698
        // apply inline style
699
        $this->applyInlineStyle($sheet, $row, $column, $attributeArray);
498✔
700

701
        /** @var string $cellContent */
702
        $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
498✔
703

704
        $this->processDomElementBgcolor($sheet, $row, $column, $attributeArray);
498✔
705
        $this->processDomElementWidth($sheet, $column, $attributeArray);
498✔
706
        $this->processDomElementHeight($sheet, $row, $attributeArray);
498✔
707
        $this->processDomElementAlign($sheet, $row, $column, $attributeArray);
498✔
708
        $this->processDomElementVAlign($sheet, $row, $column, $attributeArray);
498✔
709
        $this->processDomElementDataFormat($sheet, $row, $column, $attributeArray);
498✔
710

711
        if (isset($attributeArray['rowspan'], $attributeArray['colspan'])) {
498✔
712
            //create merging rowspan and colspan
713
            $columnTo = $column;
2✔
714
            for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) {
2✔
715
                StringHelper::stringIncrement($columnTo);
2✔
716
            }
717
            $range = $column . $row . ':' . $columnTo . ($row + (int) $attributeArray['rowspan'] - 1);
2✔
718
            foreach (Coordinate::extractAllCellReferencesInRange($range) as $value) {
2✔
719
                $this->rowspan[$value] = true;
2✔
720
            }
721
            $sheet->mergeCells($range);
2✔
722
            $column = $columnTo;
2✔
723
        } elseif (isset($attributeArray['rowspan'])) {
498✔
724
            //create merging rowspan
725
            $range = $column . $row . ':' . $column . ($row + (int) $attributeArray['rowspan'] - 1);
3✔
726
            foreach (Coordinate::extractAllCellReferencesInRange($range) as $value) {
3✔
727
                $this->rowspan[$value] = true;
3✔
728
            }
729
            $sheet->mergeCells($range);
3✔
730
        } elseif (isset($attributeArray['colspan'])) {
498✔
731
            //create merging colspan
732
            $columnTo = $column;
3✔
733
            for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) {
3✔
734
                StringHelper::stringIncrement($columnTo);
3✔
735
            }
736
            $sheet->mergeCells($column . $row . ':' . $columnTo . $row);
3✔
737
            $column = $columnTo;
3✔
738
        }
739

740
        StringHelper::stringIncrement($column);
498✔
741
    }
742

743
    protected function processDomElement(DOMNode $element, Worksheet $sheet, int &$row, string &$column, string &$cellContent): void
512✔
744
    {
745
        foreach ($element->childNodes as $child) {
512✔
746
            if ($child instanceof DOMText) {
512✔
747
                $domText = (string) preg_replace('/\s+/', ' ', trim($child->nodeValue ?? ''));
508✔
748
                if ($domText === "\u{a0}") {
508✔
749
                    $domText = '';
17✔
750
                }
751
                //    simply append the text if the cell content is a plain text string
752
                $cellContent .= $domText;
508✔
753
                //    but if we have a rich text run instead, we need to append it correctly
754
                //    TODO
755
            } elseif ($child instanceof DOMElement) {
512✔
756
                $this->processDomElementBody($sheet, $row, $column, $cellContent, $child);
512✔
757
            }
758
        }
759
    }
760

761
    /**
762
     * Loads PhpSpreadsheet from file into PhpSpreadsheet instance.
763
     */
764
    public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Spreadsheet
466✔
765
    {
766
        // Validate
767
        if (!$this->canRead($filename)) {
466✔
768
            throw new Exception($filename . ' is an Invalid HTML file.');
1✔
769
        }
770

771
        // Create a new DOM object
772
        $dom = new DOMDocument();
465✔
773

774
        // Reload the HTML file into the DOM object
775
        if (is_bool($this->suppressLoadWarnings)) {
465✔
776
            $useErrors = libxml_use_internal_errors($this->suppressLoadWarnings);
2✔
777
        } else {
778
            $useErrors = null;
463✔
779
        }
780

781
        try {
782
            $convert = $this->getSecurityScannerOrThrow()->scanFile($filename);
465✔
783
            $convert = static::replaceNonAsciiIfNeeded($convert);
464✔
784
            $loaded = ($convert === null) ? false : $dom->loadHTML($convert);
464✔
785
        } catch (Throwable $e) {
2✔
786
            $loaded = false;
2✔
787
        } finally {
788
            $this->libxmlMessages = libxml_get_errors();
465✔
789
            if (is_bool($useErrors)) {
465✔
790
                libxml_use_internal_errors($useErrors);
2✔
791
            }
792
        }
793
        if ($loaded === false) {
465✔
794
            throw new Exception('Failed to load file ' . $filename . ' as a DOM Document', 0, $e ?? null);
2✔
795
        }
796
        self::loadProperties($dom, $spreadsheet);
463✔
797

798
        return $this->loadDocument($dom, $spreadsheet);
463✔
799
    }
800

801
    private static function loadProperties(DOMDocument $dom, Spreadsheet $spreadsheet): void
512✔
802
    {
803
        $properties = $spreadsheet->getProperties();
512✔
804
        foreach ($dom->getElementsByTagName('meta') as $meta) {
512✔
805
            $metaContent = (string) $meta->getAttribute('content');
457✔
806
            if ($metaContent !== '') {
457✔
807
                $metaName = (string) $meta->getAttribute('name');
449✔
808
                switch ($metaName) {
809
                    case 'author':
449✔
810
                        $properties->setCreator($metaContent);
446✔
811

812
                        break;
446✔
813
                    case 'category':
449✔
814
                        $properties->setCategory($metaContent);
1✔
815

816
                        break;
1✔
817
                    case 'company':
449✔
818
                        $properties->setCompany($metaContent);
1✔
819

820
                        break;
1✔
821
                    case 'created':
449✔
822
                        $properties->setCreated($metaContent);
446✔
823

824
                        break;
446✔
825
                    case 'description':
449✔
826
                        $properties->setDescription($metaContent);
1✔
827

828
                        break;
1✔
829
                    case 'keywords':
449✔
830
                        $properties->setKeywords($metaContent);
1✔
831

832
                        break;
1✔
833
                    case 'lastModifiedBy':
449✔
834
                        $properties->setLastModifiedBy($metaContent);
446✔
835

836
                        break;
446✔
837
                    case 'manager':
449✔
838
                        $properties->setManager($metaContent);
1✔
839

840
                        break;
1✔
841
                    case 'modified':
449✔
842
                        $properties->setModified($metaContent);
446✔
843

844
                        break;
446✔
845
                    case 'subject':
449✔
846
                        $properties->setSubject($metaContent);
1✔
847

848
                        break;
1✔
849
                    case 'title':
449✔
850
                        $properties->setTitle($metaContent);
444✔
851

852
                        break;
444✔
853
                    case 'viewport':
449✔
854
                        $properties->setViewport($metaContent);
1✔
855

856
                        break;
1✔
857
                    default:
858
                        if (preg_match('/^custom[.](bool|date|float|int|string)[.](.+)$/', $metaName, $matches) === 1) {
449✔
859
                            match ($matches[1]) {
1✔
860
                                'bool' => $properties->setCustomProperty($matches[2], (bool) $metaContent, Properties::PROPERTY_TYPE_BOOLEAN),
1✔
861
                                'float' => $properties->setCustomProperty($matches[2], (float) $metaContent, Properties::PROPERTY_TYPE_FLOAT),
1✔
862
                                'int' => $properties->setCustomProperty($matches[2], (int) $metaContent, Properties::PROPERTY_TYPE_INTEGER),
1✔
863
                                'date' => $properties->setCustomProperty($matches[2], $metaContent, Properties::PROPERTY_TYPE_DATE),
1✔
864
                                // string
865
                                default => $properties->setCustomProperty($matches[2], $metaContent, Properties::PROPERTY_TYPE_STRING),
1✔
866
                            };
1✔
867
                        }
868
                }
869
            }
870
        }
871
        if (!empty($dom->baseURI)) {
512✔
872
            $properties->setHyperlinkBase($dom->baseURI);
1✔
873
        }
874
    }
875

876
    /** @param string[] $matches */
877
    private static function replaceNonAscii(array $matches): string
21✔
878
    {
879
        return '&#' . mb_ord($matches[0], 'UTF-8') . ';';
21✔
880
    }
881

882
    /** @internal */
883
    protected static function replaceNonAsciiIfNeeded(string $convert): ?string
513✔
884
    {
885
        if (preg_match(self::STARTS_WITH_BOM, $convert) !== 1 && preg_match(self::DECLARES_CHARSET, $convert) !== 1) {
513✔
886
            $lowend = "\u{80}";
55✔
887
            $highend = "\u{10ffff}";
55✔
888
            $regexp = "/[$lowend-$highend]/u";
55✔
889
            /** @var callable $callback */
890
            $callback = [self::class, 'replaceNonAscii'];
55✔
891
            $convert = preg_replace_callback($regexp, $callback, $convert);
55✔
892
        }
893

894
        return $convert;
513✔
895
    }
896

897
    /**
898
     * Spreadsheet from content.
899
     */
900
    public function loadFromString(string $content, ?Spreadsheet $spreadsheet = null): Spreadsheet
50✔
901
    {
902
        //    Create a new DOM object
903
        $dom = new DOMDocument();
50✔
904

905
        //    Reload the HTML file into the DOM object
906
        if (is_bool($this->suppressLoadWarnings)) {
50✔
907
            $useErrors = libxml_use_internal_errors($this->suppressLoadWarnings);
2✔
908
        } else {
909
            $useErrors = null;
48✔
910
        }
911

912
        try {
913
            $convert = $this->getSecurityScannerOrThrow()->scan($content);
50✔
914
            $convert = static::replaceNonAsciiIfNeeded($convert);
50✔
915
            $loaded = ($convert === null) ? false : $dom->loadHTML($convert);
49✔
916
        } catch (Throwable $e) {
1✔
917
            $loaded = false;
1✔
918
        } finally {
919
            $this->libxmlMessages = libxml_get_errors();
50✔
920
            if (is_bool($useErrors)) {
50✔
921
                libxml_use_internal_errors($useErrors);
2✔
922
            }
923
        }
924
        if ($loaded === false) {
50✔
925
            throw new Exception('Failed to load content as a DOM Document', 0, $e ?? null);
1✔
926
        }
927
        $spreadsheet = $spreadsheet ?? $this->newSpreadsheet();
49✔
928
        $spreadsheet->setValueBinder($this->valueBinder);
49✔
929
        self::loadProperties($dom, $spreadsheet);
49✔
930

931
        return $this->loadDocument($dom, $spreadsheet);
49✔
932
    }
933

934
    /**
935
     * Loads PhpSpreadsheet from DOMDocument into PhpSpreadsheet instance.
936
     */
937
    private function loadDocument(DOMDocument $document, Spreadsheet $spreadsheet): Spreadsheet
512✔
938
    {
939
        while ($spreadsheet->getSheetCount() <= $this->sheetIndex) {
512✔
940
            $spreadsheet->createSheet();
2✔
941
        }
942
        $spreadsheet->setActiveSheetIndex($this->sheetIndex);
512✔
943

944
        // Discard white space
945
        $document->preserveWhiteSpace = false;
512✔
946

947
        $row = 0;
512✔
948
        $column = 'A';
512✔
949
        $content = '';
512✔
950
        $this->rowspan = [];
512✔
951
        $this->processDomElement($document, $spreadsheet->getActiveSheet(), $row, $column, $content);
512✔
952

953
        // Return
954
        return $spreadsheet;
505✔
955
    }
956

957
    /**
958
     * Get sheet index.
959
     */
960
    public function getSheetIndex(): int
1✔
961
    {
962
        return $this->sheetIndex;
1✔
963
    }
964

965
    /**
966
     * Set sheet index.
967
     *
968
     * @param int $sheetIndex Sheet index
969
     *
970
     * @return $this
971
     */
972
    public function setSheetIndex(int $sheetIndex): static
2✔
973
    {
974
        $this->sheetIndex = $sheetIndex;
2✔
975

976
        return $this;
2✔
977
    }
978

979
    /**
980
     * Apply inline css inline style.
981
     *
982
     * NOTES :
983
     * Currently only intended for td & th element,
984
     * and only takes 'background-color' and 'color'; property with HEX color
985
     *
986
     * TODO :
987
     * - Implement to other properties, such as border
988
     *
989
     * @param string[] $attributeArray
990
     */
991
    private function applyInlineStyle(Worksheet &$sheet, int $row, string $column, array $attributeArray): void
498✔
992
    {
993
        if (!isset($attributeArray['style'])) {
498✔
994
            return;
491✔
995
        }
996

997
        if ($row <= 0 || $column === '') {
18✔
998
            $cellStyle = new Style();
2✔
999
        } elseif (isset($attributeArray['rowspan'], $attributeArray['colspan'])) {
18✔
1000
            $columnTo = $column;
1✔
1001
            for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) {
1✔
1002
                StringHelper::stringIncrement($columnTo);
1✔
1003
            }
1004
            $range = $column . $row . ':' . $columnTo . ($row + (int) $attributeArray['rowspan'] - 1);
1✔
1005
            $cellStyle = $sheet->getStyle($range);
1✔
1006
        } elseif (isset($attributeArray['rowspan'])) {
18✔
1007
            $range = $column . $row . ':' . $column . ($row + (int) $attributeArray['rowspan'] - 1);
1✔
1008
            $cellStyle = $sheet->getStyle($range);
1✔
1009
        } elseif (isset($attributeArray['colspan'])) {
18✔
1010
            $columnTo = $column;
1✔
1011
            for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) {
1✔
1012
                StringHelper::stringIncrement($columnTo);
1✔
1013
            }
1014
            $range = $column . $row . ':' . $columnTo . $row;
1✔
1015
            $cellStyle = $sheet->getStyle($range);
1✔
1016
        } else {
1017
            $cellStyle = $sheet->getStyle($column . $row);
18✔
1018
        }
1019

1020
        // add color styles (background & text) from dom element,currently support : td & th, using ONLY inline css style with RGB color
1021
        $styles = explode(';', $attributeArray['style']);
18✔
1022
        foreach ($styles as $st) {
18✔
1023
            $value = explode(':', $st);
18✔
1024
            $styleName = trim($value[0]);
18✔
1025
            $styleValue = isset($value[1]) ? trim($value[1]) : null;
18✔
1026
            $styleValueString = (string) $styleValue;
18✔
1027

1028
            if (!$styleName) {
18✔
1029
                continue;
13✔
1030
            }
1031

1032
            switch ($styleName) {
1033
                case 'background':
18✔
1034
                case 'background-color':
18✔
1035
                    $styleColor = $this->getStyleColor($styleValueString);
3✔
1036

1037
                    if (!$styleColor) {
3✔
1038
                        continue 2;
1✔
1039
                    }
1040

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

1043
                    break;
3✔
1044
                case 'color':
18✔
1045
                    $styleColor = $this->getStyleColor($styleValueString);
4✔
1046

1047
                    if (!$styleColor) {
4✔
1048
                        continue 2;
1✔
1049
                    }
1050

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

1053
                    break;
4✔
1054

1055
                case 'border':
15✔
1056
                    $this->setBorderStyle($cellStyle, $styleValueString, 'allBorders');
3✔
1057

1058
                    break;
3✔
1059

1060
                case 'border-top':
13✔
1061
                    $this->setBorderStyle($cellStyle, $styleValueString, 'top');
1✔
1062

1063
                    break;
1✔
1064

1065
                case 'border-bottom':
13✔
1066
                    $this->setBorderStyle($cellStyle, $styleValueString, 'bottom');
1✔
1067

1068
                    break;
1✔
1069

1070
                case 'border-left':
13✔
1071
                    $this->setBorderStyle($cellStyle, $styleValueString, 'left');
1✔
1072

1073
                    break;
1✔
1074

1075
                case 'border-right':
13✔
1076
                    $this->setBorderStyle($cellStyle, $styleValueString, 'right');
1✔
1077

1078
                    break;
1✔
1079

1080
                case 'font-size':
12✔
1081
                    $cellStyle->getFont()->setSize(
2✔
1082
                        (float) $styleValue
2✔
1083
                    );
2✔
1084

1085
                    break;
2✔
1086

1087
                case 'direction':
12✔
1088
                    if ($styleValue === 'rtl') {
1✔
1089
                        $cellStyle->getAlignment()
1✔
1090
                            ->setReadOrder(Alignment::READORDER_RTL);
1✔
1091
                    } elseif ($styleValue === 'ltr') {
1✔
1092
                        $cellStyle->getAlignment()
1✔
1093
                            ->setReadOrder(Alignment::READORDER_LTR);
1✔
1094
                    }
1095

1096
                    break;
1✔
1097

1098
                case 'font-weight':
12✔
1099
                    if ($styleValue === 'bold' || $styleValue >= 500) {
1✔
1100
                        $cellStyle->getFont()->setBold(true);
1✔
1101
                    }
1102

1103
                    break;
1✔
1104

1105
                case 'font-style':
12✔
1106
                    if ($styleValue === 'italic') {
1✔
1107
                        $cellStyle->getFont()->setItalic(true);
1✔
1108
                    }
1109

1110
                    break;
1✔
1111

1112
                case 'font-family':
12✔
1113
                    $cellStyle->getFont()->setName(str_replace('\'', '', $styleValueString));
2✔
1114

1115
                    break;
2✔
1116

1117
                case 'text-decoration':
12✔
1118
                    switch ($styleValue) {
1119
                        case 'underline':
1✔
1120
                            $cellStyle->getFont()->setUnderline(Font::UNDERLINE_SINGLE);
1✔
1121

1122
                            break;
1✔
1123
                        case 'line-through':
1✔
1124
                            $cellStyle->getFont()->setStrikethrough(true);
1✔
1125

1126
                            break;
1✔
1127
                    }
1128

1129
                    break;
1✔
1130

1131
                case 'text-align':
11✔
1132
                    $cellStyle->getAlignment()->setHorizontal($styleValueString);
2✔
1133

1134
                    break;
2✔
1135

1136
                case 'vertical-align':
11✔
1137
                    $cellStyle->getAlignment()->setVertical($styleValueString);
3✔
1138

1139
                    break;
3✔
1140

1141
                case 'width':
11✔
1142
                    if ($column !== '') {
3✔
1143
                        $sheet->getColumnDimension($column)->setWidth(
3✔
1144
                            (new CssDimension($styleValue ?? ''))->width()
3✔
1145
                        );
3✔
1146
                    }
1147

1148
                    break;
3✔
1149

1150
                case 'height':
9✔
1151
                    if ($row > 0) {
1✔
1152
                        $sheet->getRowDimension($row)->setRowHeight(
1✔
1153
                            (new CssDimension($styleValue ?? ''))->height()
1✔
1154
                        );
1✔
1155
                    }
1156

1157
                    break;
1✔
1158

1159
                case 'word-wrap':
8✔
1160
                    $cellStyle->getAlignment()->setWrapText(
1✔
1161
                        $styleValue === 'break-word'
1✔
1162
                    );
1✔
1163

1164
                    break;
1✔
1165

1166
                case 'text-indent':
8✔
1167
                    $indentDimension = new CssDimension($styleValueString);
3✔
1168
                    $indent = $indentDimension
3✔
1169
                        ->toUnit(CssDimension::UOM_PIXELS);
3✔
1170
                    $cellStyle->getAlignment()->setIndent(
3✔
1171
                        (int) ($indent / Alignment::INDENT_UNITS_TO_PIXELS)
3✔
1172
                    );
3✔
1173

1174
                    break;
3✔
1175
            }
1176
        }
1177
    }
1178

1179
    /**
1180
     * Check if has #, so we can get clean hex.
1181
     */
1182
    public function getStyleColor(?string $value): string
8✔
1183
    {
1184
        $value = (string) $value;
8✔
1185
        if (str_starts_with($value, '#')) {
8✔
1186
            return substr($value, 1);
6✔
1187
        }
1188

1189
        return HelperHtml::colourNameLookup($value);
4✔
1190
    }
1191

1192
    /** @param string[] $attributes */
1193
    private function insertImage(Worksheet $sheet, string $column, int $row, array $attributes): void
20✔
1194
    {
1195
        if (!isset($attributes['src'])) {
20✔
1196
            return;
1✔
1197
        }
1198
        $styleArray = self::getStyleArray($attributes);
19✔
1199

1200
        $src = $attributes['src'];
19✔
1201
        if (!str_starts_with($src, 'data:')) {
19✔
1202
            $src = urldecode($src);
14✔
1203
        }
1204
        $width = isset($attributes['width']) ? (float) $attributes['width'] : ($styleArray['width'] ?? null);
19✔
1205
        $height = isset($attributes['height']) ? (float) $attributes['height'] : ($styleArray['height'] ?? null);
19✔
1206
        $name = $attributes['alt'] ?? null;
19✔
1207

1208
        $drawing = new Drawing();
19✔
1209
        $drawing->setPath($src, false, allowExternal: $this->allowExternalImages);
19✔
1210
        if ($drawing->getPath() === '') {
12✔
1211
            return;
3✔
1212
        }
1213
        $drawing->setWorksheet($sheet);
9✔
1214
        $drawing->setCoordinates($column . $row);
9✔
1215
        $drawing->setOffsetX(0);
9✔
1216
        $drawing->setOffsetY(10);
9✔
1217
        $drawing->setResizeProportional(true);
9✔
1218

1219
        if ($name) {
9✔
1220
            $drawing->setName($name);
8✔
1221
        }
1222

1223
        /** @var null|scalar $width */
1224
        /** @var null|scalar $height */
1225
        if ($width) {
9✔
1226
            if ($height) {
6✔
1227
                $drawing->setWidthAndHeight((int) $width, (int) $height);
3✔
1228
            } else {
1229
                $drawing->setWidth((int) $width);
3✔
1230
            }
1231
        } elseif ($height) {
3✔
1232
            $drawing->setHeight((int) $height);
1✔
1233
        }
1234

1235
        $sheet->getColumnDimension($column)->setWidth(
9✔
1236
            $drawing->getWidth() / 6
9✔
1237
        );
9✔
1238

1239
        $sheet->getRowDimension($row)->setRowHeight(
9✔
1240
            $drawing->getHeight() * 0.9
9✔
1241
        );
9✔
1242

1243
        if (isset($styleArray['opacity'])) {
9✔
1244
            $opacity = $styleArray['opacity'];
1✔
1245
            if (is_numeric($opacity)) {
1✔
1246
                $drawing->setOpacity((int) ($opacity * 100000));
1✔
1247
            }
1248
        }
1249
    }
1250

1251
    /**
1252
     * @param string[] $attributes
1253
     *
1254
     * @return mixed[]
1255
     */
1256
    private static function getStyleArray(array $attributes): array
19✔
1257
    {
1258
        $styleArray = [];
19✔
1259
        if (isset($attributes['style'])) {
19✔
1260
            $styles = explode(';', $attributes['style']);
5✔
1261
            foreach ($styles as $style) {
5✔
1262
                $value = explode(':', $style);
5✔
1263
                if (count($value) === 2) {
5✔
1264
                    $arrayKey = trim($value[0]);
5✔
1265
                    $arrayValue = trim($value[1]);
5✔
1266
                    if ($arrayKey === 'width') {
5✔
1267
                        if (str_ends_with($arrayValue, 'px')) {
5✔
1268
                            $arrayValue = (string) (((float) substr($arrayValue, 0, -2)));
4✔
1269
                        } else {
1270
                            $arrayValue = (new CssDimension($arrayValue))->toUnit(CssDimension::UOM_PIXELS);
1✔
1271
                        }
1272
                    } elseif ($arrayKey === 'height') {
5✔
1273
                        if (str_ends_with($arrayValue, 'px')) {
3✔
1274
                            $arrayValue = substr($arrayValue, 0, -2);
2✔
1275
                        } else {
1276
                            $arrayValue = (new CssDimension($arrayValue))->toUnit(CssDimension::UOM_PIXELS);
1✔
1277
                        }
1278
                    }
1279
                    $styleArray[$arrayKey] = $arrayValue;
5✔
1280
                }
1281
            }
1282
        }
1283

1284
        return $styleArray;
19✔
1285
    }
1286

1287
    private const BORDER_MAPPINGS = [
1288
        'dash-dot' => Border::BORDER_DASHDOT,
1289
        'dash-dot-dot' => Border::BORDER_DASHDOTDOT,
1290
        'dashed' => Border::BORDER_DASHED,
1291
        'dotted' => Border::BORDER_DOTTED,
1292
        'double' => Border::BORDER_DOUBLE,
1293
        'hair' => Border::BORDER_HAIR,
1294
        'medium' => Border::BORDER_MEDIUM,
1295
        'medium-dashed' => Border::BORDER_MEDIUMDASHED,
1296
        'medium-dash-dot' => Border::BORDER_MEDIUMDASHDOT,
1297
        'medium-dash-dot-dot' => Border::BORDER_MEDIUMDASHDOTDOT,
1298
        'none' => Border::BORDER_NONE,
1299
        'slant-dash-dot' => Border::BORDER_SLANTDASHDOT,
1300
        'solid' => Border::BORDER_THIN,
1301
        'thick' => Border::BORDER_THICK,
1302
    ];
1303

1304
    /** @return array<string, string> */
1305
    public static function getBorderMappings(): array
15✔
1306
    {
1307
        return self::BORDER_MAPPINGS;
15✔
1308
    }
1309

1310
    /**
1311
     * Map html border style to PhpSpreadsheet border style.
1312
     */
1313
    public function getBorderStyle(string $style): ?string
3✔
1314
    {
1315
        return self::BORDER_MAPPINGS[$style] ?? null;
3✔
1316
    }
1317

1318
    private function setBorderStyle(Style $cellStyle, string $styleValue, string $type): void
3✔
1319
    {
1320
        if (trim($styleValue) === Border::BORDER_NONE) {
3✔
1321
            $borderStyle = Border::BORDER_NONE;
1✔
1322
            $color = null;
1✔
1323
        } else {
1324
            $borderArray = explode(' ', $styleValue);
3✔
1325
            $borderCount = count($borderArray);
3✔
1326
            if ($borderCount >= 3) {
3✔
1327
                $borderStyle = $borderArray[1];
3✔
1328
                $color = $borderArray[2];
3✔
1329
            } else {
1330
                $borderStyle = $borderArray[0];
1✔
1331
                $color = $borderArray[1] ?? null;
1✔
1332
            }
1333
        }
1334

1335
        $cellStyle->applyFromArray([
3✔
1336
            'borders' => [
3✔
1337
                $type => [
3✔
1338
                    'borderStyle' => $this->getBorderStyle($borderStyle),
3✔
1339
                    'color' => ['rgb' => $this->getStyleColor($color)],
3✔
1340
                ],
3✔
1341
            ],
3✔
1342
        ]);
3✔
1343
    }
1344

1345
    /**
1346
     * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
1347
     *
1348
     * @return array<int, array{worksheetName: string, lastColumnLetter: string, lastColumnIndex: int, totalRows: int, totalColumns: int, sheetState: string}>
1349
     */
1350
    public function listWorksheetInfo(string $filename): array
1✔
1351
    {
1352
        $info = [];
1✔
1353
        $spreadsheet = $this->newSpreadsheet();
1✔
1354
        $this->loadIntoExisting($filename, $spreadsheet);
1✔
1355
        foreach ($spreadsheet->getAllSheets() as $sheet) {
1✔
1356
            $newEntry = ['worksheetName' => $sheet->getTitle()];
1✔
1357
            $newEntry['lastColumnLetter'] = $sheet->getHighestDataColumn();
1✔
1358
            $newEntry['lastColumnIndex'] = Coordinate::columnIndexFromString($sheet->getHighestDataColumn()) - 1;
1✔
1359
            $newEntry['totalRows'] = $sheet->getHighestDataRow();
1✔
1360
            $newEntry['totalColumns'] = $newEntry['lastColumnIndex'] + 1;
1✔
1361
            $newEntry['sheetState'] = Worksheet::SHEETSTATE_VISIBLE;
1✔
1362
            $info[] = $newEntry;
1✔
1363
        }
1364
        $spreadsheet->disconnectWorksheets();
1✔
1365

1366
        return $info;
1✔
1367
    }
1368
}
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