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

PHPOffice / PhpSpreadsheet / 21815518402

09 Feb 2026 07:02AM UTC coverage: 96.313% (+0.04%) from 96.273%
21815518402

Pull #4799

github

web-flow
Merge fb6e56a22 into 95b826a3c
Pull Request #4799: Ods Writer Support Some Number Formats

660 of 664 new or added lines in 6 files covered. (99.4%)

30 existing lines in 3 files now uncovered.

47128 of 48932 relevant lines covered (96.31%)

383.08 hits per line

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

86.59
/src/PhpSpreadsheet/Reader/Ods.php
1
<?php
2

3
namespace PhpOffice\PhpSpreadsheet\Reader;
4

5
use Composer\Pcre\Preg;
6
use DateTime;
7
use DateTimeZone;
8
use DOMAttr;
9
use DOMDocument;
10
use DOMElement;
11
use DOMNode;
12
use DOMText;
13
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
14
use PhpOffice\PhpSpreadsheet\Cell\DataType;
15
use PhpOffice\PhpSpreadsheet\Helper\Dimension as HelperDimension;
16
use PhpOffice\PhpSpreadsheet\Reader\Ods\AutoFilter;
17
use PhpOffice\PhpSpreadsheet\Reader\Ods\DefinedNames;
18
use PhpOffice\PhpSpreadsheet\Reader\Ods\FormulaTranslator;
19
use PhpOffice\PhpSpreadsheet\Reader\Ods\PageSettings;
20
use PhpOffice\PhpSpreadsheet\Reader\Ods\Properties as DocumentProperties;
21
use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner;
22
use PhpOffice\PhpSpreadsheet\RichText\RichText;
23
use PhpOffice\PhpSpreadsheet\Shared\Date;
24
use PhpOffice\PhpSpreadsheet\Shared\File;
25
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
26
use PhpOffice\PhpSpreadsheet\Spreadsheet;
27
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
28
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
29
use Throwable;
30
use XMLReader;
31
use ZipArchive;
32

33
class Ods extends BaseReader
34
{
35
    const INITIAL_FILE = 'content.xml';
36

37
    /**
38
     * Create a new Ods Reader instance.
39
     */
40
    public function __construct()
116✔
41
    {
42
        parent::__construct();
116✔
43
        $this->securityScanner = XmlScanner::getInstance($this);
116✔
44
    }
45

46
    /**
47
     * Can the current IReader read the file?
48
     */
49
    public function canRead(string $filename): bool
21✔
50
    {
51
        $mimeType = 'UNKNOWN';
21✔
52

53
        // Load file
54

55
        if (File::testFileNoThrow($filename, '')) {
21✔
56
            $zip = new ZipArchive();
5✔
57
            if ($zip->open($filename) === true) {
5✔
58
                // check if it is an OOXML archive
59
                $stat = $zip->statName('mimetype');
5✔
60
                if (!empty($stat) && ($stat['size'] <= 255)) {
5✔
61
                    $mimeType = $zip->getFromName($stat['name']);
4✔
62
                } elseif ($zip->statName('META-INF/manifest.xml')) {
1✔
63
                    $xml = simplexml_load_string(
1✔
64
                        $this->getSecurityScannerOrThrow()
1✔
65
                            ->scan(
1✔
66
                                $zip->getFromName(
1✔
67
                                    'META-INF/manifest.xml'
1✔
68
                                )
1✔
69
                            )
1✔
70
                    );
1✔
71
                    if ($xml !== false) {
1✔
72
                        $namespacesContent = $xml->getNamespaces(true);
1✔
73
                        if (isset($namespacesContent['manifest'])) {
1✔
74
                            $manifest = $xml->children($namespacesContent['manifest']);
1✔
75
                            foreach ($manifest as $manifestDataSet) {
1✔
76
                                $manifestAttributes = $manifestDataSet->attributes($namespacesContent['manifest']);
1✔
77
                                if ($manifestAttributes && $manifestAttributes->{'full-path'} == '/') {
1✔
78
                                    $mimeType = (string) $manifestAttributes->{'media-type'};
1✔
79

80
                                    break;
1✔
81
                                }
82
                            }
83
                        }
84
                    }
85
                }
86

87
                $zip->close();
5✔
88
            }
89
        }
90

91
        return $mimeType === 'application/vnd.oasis.opendocument.spreadsheet';
21✔
92
    }
93

94
    /**
95
     * Reads names of the worksheets from a file, without parsing the whole file to a PhpSpreadsheet object.
96
     *
97
     * @return string[]
98
     */
99
    public function listWorksheetNames(string $filename): array
6✔
100
    {
101
        File::assertFile($filename, self::INITIAL_FILE);
6✔
102

103
        $worksheetNames = [];
2✔
104

105
        $xml = new XMLReader();
2✔
106
        $xml->xml(
2✔
107
            $this->getSecurityScannerOrThrow()
2✔
108
                ->scanFile(
2✔
109
                    'zip://' . realpath($filename) . '#' . self::INITIAL_FILE
2✔
110
                )
2✔
111
        );
2✔
112
        $xml->setParserProperty(2, true);
2✔
113

114
        // Step into the first level of content of the XML
115
        $xml->read();
2✔
116
        while ($xml->read()) {
2✔
117
            // Quickly jump through to the office:body node
118
            while ($xml->name !== 'office:body') {
2✔
119
                if ($xml->isEmptyElement) {
2✔
120
                    $xml->read();
2✔
121
                } else {
122
                    $xml->next();
2✔
123
                }
124
            }
125
            // Now read each node until we find our first table:table node
126
            while ($xml->read()) {
2✔
127
                $xmlName = $xml->name;
2✔
128
                if ($xmlName == 'table:table' && $xml->nodeType == XMLReader::ELEMENT) {
2✔
129
                    // Loop through each table:table node reading the table:name attribute for each worksheet name
130
                    do {
131
                        $worksheetName = $xml->getAttribute('table:name');
2✔
132
                        if (!empty($worksheetName)) {
2✔
133
                            $worksheetNames[] = $worksheetName;
2✔
134
                        }
135
                        $xml->next();
2✔
136
                    } while ($xml->name == 'table:table' && $xml->nodeType == XMLReader::ELEMENT);
2✔
137
                }
138
            }
139
        }
140

141
        return $worksheetNames;
2✔
142
    }
143

144
    /**
145
     * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
146
     *
147
     * @return array<int, array{worksheetName: string, lastColumnLetter: string, lastColumnIndex: int, totalRows: int, totalColumns: int, sheetState: string}>
148
     */
149
    public function listWorksheetInfo(string $filename): array
8✔
150
    {
151
        File::assertFile($filename, self::INITIAL_FILE);
8✔
152

153
        $worksheetInfo = [];
4✔
154

155
        $xml = new XMLReader();
4✔
156
        $xml->xml(
4✔
157
            $this->getSecurityScannerOrThrow()
4✔
158
                ->scanFile(
4✔
159
                    'zip://' . realpath($filename) . '#' . self::INITIAL_FILE
4✔
160
                )
4✔
161
        );
4✔
162
        $xml->setParserProperty(2, true);
4✔
163

164
        // Step into the first level of content of the XML
165
        $xml->read();
4✔
166
        $tableVisibility = [];
4✔
167
        $lastTableStyle = '';
4✔
168

169
        while ($xml->read()) {
4✔
170
            if ($xml->name === 'style:style') {
4✔
171
                $styleType = $xml->getAttribute('style:family');
4✔
172
                if ($styleType === 'table') {
4✔
173
                    $lastTableStyle = $xml->getAttribute('style:name');
4✔
174
                }
175
            } elseif ($xml->name === 'style:table-properties') {
4✔
176
                $visibility = $xml->getAttribute('table:display');
4✔
177
                $tableVisibility[$lastTableStyle] = ($visibility === 'false') ? Worksheet::SHEETSTATE_HIDDEN : Worksheet::SHEETSTATE_VISIBLE;
4✔
178
            } elseif ($xml->name == 'table:table' && $xml->nodeType == XMLReader::ELEMENT) {
4✔
179
                $worksheetNames[] = $xml->getAttribute('table:name');
4✔
180

181
                $styleName = $xml->getAttribute('table:style-name') ?? '';
4✔
182
                $visibility = $tableVisibility[$styleName] ?? '';
4✔
183
                $tmpInfo = [
4✔
184
                    'worksheetName' => (string) $xml->getAttribute('table:name'),
4✔
185
                    'lastColumnLetter' => 'A',
4✔
186
                    'lastColumnIndex' => 0,
4✔
187
                    'totalRows' => 0,
4✔
188
                    'totalColumns' => 0,
4✔
189
                    'sheetState' => $visibility,
4✔
190
                ];
4✔
191

192
                // Loop through each child node of the table:table element reading
193
                $currRow = 0;
4✔
194
                do {
195
                    $xml->read();
4✔
196
                    if ($xml->name == 'table:table-row' && $xml->nodeType == XMLReader::ELEMENT) {
4✔
197
                        $rowspan = $xml->getAttribute('table:number-rows-repeated');
4✔
198
                        $rowspan = empty($rowspan) ? 1 : (int) $rowspan;
4✔
199
                        $currRow += $rowspan;
4✔
200
                        $currCol = 0;
4✔
201
                        // Step into the row
202
                        $xml->read();
4✔
203
                        do {
204
                            $doread = true;
4✔
205
                            if ($xml->name == 'table:table-cell' && $xml->nodeType == XMLReader::ELEMENT) {
4✔
206
                                $mergeSize = $xml->getAttribute('table:number-columns-repeated');
4✔
207
                                $mergeSize = empty($mergeSize) ? 1 : (int) $mergeSize;
4✔
208
                                $currCol += $mergeSize;
4✔
209
                                if (!$xml->isEmptyElement) {
4✔
210
                                    $tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCol);
4✔
211
                                    $tmpInfo['totalRows'] = $currRow;
4✔
212
                                    $xml->next();
4✔
213
                                    $doread = false;
4✔
214
                                }
215
                            } elseif ($xml->name == 'table:covered-table-cell' && $xml->nodeType == XMLReader::ELEMENT) {
2✔
216
                                $mergeSize = $xml->getAttribute('table:number-columns-repeated');
2✔
217
                                $currCol += (int) $mergeSize;
2✔
218
                            }
219
                            if ($doread) {
4✔
220
                                $xml->read();
3✔
221
                            }
222
                        } while ($xml->name != 'table:table-row');
4✔
223
                    }
224
                } while ($xml->name != 'table:table');
4✔
225

226
                $tmpInfo['lastColumnIndex'] = $tmpInfo['totalColumns'] - 1;
4✔
227
                $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1);
4✔
228
                $worksheetInfo[] = $tmpInfo;
4✔
229
            }
230
        }
231

232
        return $worksheetInfo;
4✔
233
    }
234

235
    /**
236
     * Loads PhpSpreadsheet from file.
237
     */
238
    protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
81✔
239
    {
240
        $spreadsheet = $this->newSpreadsheet();
81✔
241
        $spreadsheet->setValueBinder($this->valueBinder);
81✔
242
        $spreadsheet->removeSheetByIndex(0);
81✔
243

244
        // Load into this instance
245
        return $this->loadIntoExisting($filename, $spreadsheet);
81✔
246
    }
247

248
    /**
249
     * Loads PhpSpreadsheet from file into PhpSpreadsheet instance.
250
     */
251
    public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Spreadsheet
85✔
252
    {
253
        File::assertFile($filename, self::INITIAL_FILE);
85✔
254

255
        $zip = new ZipArchive();
81✔
256
        $zip->open($filename);
81✔
257

258
        // Meta
259

260
        $xml = @simplexml_load_string(
81✔
261
            $this->getSecurityScannerOrThrow()
81✔
262
                ->scan($zip->getFromName('meta.xml'))
81✔
263
        );
81✔
264
        if ($xml === false) {
81✔
265
            throw new Exception('Unable to read data from {$pFilename}');
1✔
266
        }
267

268
        /** @var array{meta?: string, office?: string, dc?: string} */
269
        $namespacesMeta = $xml->getNamespaces(true);
80✔
270

271
        (new DocumentProperties($spreadsheet))->load($xml, $namespacesMeta);
80✔
272

273
        // Styles
274

275
        $dom = new DOMDocument('1.01', 'UTF-8');
80✔
276
        $dom->loadXML(
80✔
277
            $this->getSecurityScannerOrThrow()
80✔
278
                ->scan($zip->getFromName('styles.xml'))
80✔
279
        );
80✔
280

281
        $pageSettings = new PageSettings($dom);
80✔
282

283
        // Main Content
284

285
        $dom = new DOMDocument('1.01', 'UTF-8');
80✔
286
        $dom->loadXML(
80✔
287
            $this->getSecurityScannerOrThrow()
80✔
288
                ->scan($zip->getFromName(self::INITIAL_FILE))
80✔
289
        );
80✔
290

291
        $officeNs = (string) $dom->lookupNamespaceUri('office');
80✔
292
        $tableNs = (string) $dom->lookupNamespaceUri('table');
80✔
293
        $textNs = (string) $dom->lookupNamespaceUri('text');
80✔
294
        $xlinkNs = (string) $dom->lookupNamespaceUri('xlink');
80✔
295
        $styleNs = (string) $dom->lookupNamespaceUri('style');
80✔
296

297
        $pageSettings->readStyleCrossReferences($dom);
80✔
298

299
        $autoFilterReader = new AutoFilter($spreadsheet, $tableNs);
80✔
300
        $definedNameReader = new DefinedNames($spreadsheet, $tableNs);
80✔
301
        $columnWidths = [];
80✔
302
        $automaticStyle0 = $dom->getElementsByTagNameNS($officeNs, 'automatic-styles')->item(0);
80✔
303
        $automaticStyles = ($automaticStyle0 === null) ? [] : $automaticStyle0->getElementsByTagNameNS($styleNs, 'style');
80✔
304
        foreach ($automaticStyles as $automaticStyle) {
80✔
305
            $styleName = $automaticStyle->getAttributeNS($styleNs, 'name');
80✔
306
            $styleFamily = $automaticStyle->getAttributeNS($styleNs, 'family');
80✔
307
            if ($styleFamily === 'table-column') {
80✔
308
                $tcprops = $automaticStyle->getElementsByTagNameNS($styleNs, 'table-column-properties');
48✔
309
                $tcprop = $tcprops->item(0);
48✔
310
                if ($tcprop !== null) {
48✔
311
                    $columnWidth = $tcprop->getAttributeNs($styleNs, 'column-width');
48✔
312
                    $columnWidths[$styleName] = $columnWidth;
48✔
313
                }
314
            }
315
        }
316

317
        // Content
318
        $item0 = $dom->getElementsByTagNameNS($officeNs, 'body')->item(0);
80✔
319
        $spreadsheets = ($item0 === null) ? [] : $item0->getElementsByTagNameNS($officeNs, 'spreadsheet');
80✔
320

321
        foreach ($spreadsheets as $workbookData) {
80✔
322
            /** @var DOMElement $workbookData */
323
            $tables = $workbookData->getElementsByTagNameNS($tableNs, 'table');
80✔
324

325
            $worksheetID = 0;
80✔
326
            $sheetCreated = false;
80✔
327
            foreach ($tables as $worksheetDataSet) {
80✔
328
                /** @var DOMElement $worksheetDataSet */
329
                $worksheetName = $worksheetDataSet->getAttributeNS($tableNs, 'name');
80✔
330

331
                // Check loadSheetsOnly
332
                if (
333
                    $this->loadSheetsOnly !== null
80✔
334
                    && $worksheetName
335
                    && !in_array($worksheetName, $this->loadSheetsOnly)
80✔
336
                ) {
337
                    continue;
5✔
338
                }
339

340
                $worksheetStyleName = $worksheetDataSet->getAttributeNS($tableNs, 'style-name');
77✔
341

342
                // Create sheet
343
                $spreadsheet->createSheet();
77✔
344
                $sheetCreated = true;
77✔
345
                $spreadsheet->setActiveSheetIndex($worksheetID);
77✔
346

347
                if ($worksheetName || is_numeric($worksheetName)) {
77✔
348
                    // Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in
349
                    // formula cells... during the load, all formulae should be correct, and we're simply
350
                    // bringing the worksheet name in line with the formula, not the reverse
351
                    $spreadsheet->getActiveSheet()->setTitle((string) $worksheetName, false, false);
77✔
352
                }
353

354
                // Go through every child of table element
355
                $rowID = 1;
77✔
356
                $tableColumnIndex = 1;
77✔
357
                foreach ($worksheetDataSet->childNodes as $childNode) {
77✔
358
                    /** @var DOMElement $childNode */
359

360
                    // Filter elements which are not under the "table" ns
361
                    if ($childNode->namespaceURI != $tableNs) {
77✔
362
                        continue;
46✔
363
                    }
364

365
                    $key = self::extractNodeName($childNode->nodeName);
77✔
366

367
                    switch ($key) {
368
                        case 'table-header-rows':
77✔
369
                        case 'table-rows':
77✔
370
                            $this->processTableHeaderRows(
1✔
371
                                $childNode,
1✔
372
                                $tableNs,
1✔
373
                                $rowID,
1✔
374
                                $worksheetName,
1✔
375
                                $officeNs,
1✔
376
                                $textNs,
1✔
377
                                $xlinkNs,
1✔
378
                                $spreadsheet
1✔
379
                            );
1✔
380

381
                            break;
1✔
382
                        case 'table-row-group':
77✔
383
                            $this->processTableRowGroup(
1✔
384
                                $childNode,
1✔
385
                                $tableNs,
1✔
386
                                $rowID,
1✔
387
                                $worksheetName,
1✔
388
                                $officeNs,
1✔
389
                                $textNs,
1✔
390
                                $xlinkNs,
1✔
391
                                $spreadsheet
1✔
392
                            );
1✔
393

394
                            break;
1✔
395
                        case 'table-header-columns':
77✔
396
                        case 'table-columns':
77✔
397
                            $this->processTableHeaderColumns(
×
398
                                $childNode,
×
399
                                $tableNs,
×
400
                                $columnWidths,
×
401
                                $tableColumnIndex,
×
402
                                $spreadsheet
×
403
                            );
×
404

405
                            break;
×
406
                        case 'table-column-group':
77✔
407
                            $this->processTableColumnGroup(
×
408
                                $childNode,
×
409
                                $tableNs,
×
410
                                $columnWidths,
×
411
                                $tableColumnIndex,
×
412
                                $spreadsheet
×
413
                            );
×
414

415
                            break;
×
416
                        case 'table-column':
77✔
417
                            $this->processTableColumn(
45✔
418
                                $childNode,
45✔
419
                                $tableNs,
45✔
420
                                $columnWidths,
45✔
421
                                $tableColumnIndex,
45✔
422
                                $spreadsheet
45✔
423
                            );
45✔
424

425
                            break;
45✔
426
                        case 'table-row':
76✔
427
                            $this->processTableRow(
76✔
428
                                $childNode,
76✔
429
                                $tableNs,
76✔
430
                                $rowID,
76✔
431
                                $worksheetName,
76✔
432
                                $officeNs,
76✔
433
                                $textNs,
76✔
434
                                $xlinkNs,
76✔
435
                                $spreadsheet
76✔
436
                            );
76✔
437

438
                            break;
76✔
439
                    }
440
                }
441
                $pageSettings->setVisibilityForWorksheet(
77✔
442
                    $spreadsheet->getActiveSheet(),
77✔
443
                    $worksheetStyleName
77✔
444
                );
77✔
445
                $pageSettings->setPrintSettingsForWorksheet(
77✔
446
                    $spreadsheet->getActiveSheet(),
77✔
447
                    $worksheetStyleName
77✔
448
                );
77✔
449
                ++$worksheetID;
77✔
450
            }
451
            if ($this->createBlankSheetIfNoneRead && !$sheetCreated) {
80✔
452
                $spreadsheet->createSheet();
1✔
453
            }
454

455
            $autoFilterReader->read($workbookData);
80✔
456
            $definedNameReader->read($workbookData);
80✔
457
        }
458
        $spreadsheet->setActiveSheetIndex(0);
80✔
459

460
        if ($zip->locateName('settings.xml') !== false) {
78✔
461
            $this->processSettings($zip, $spreadsheet);
71✔
462
        }
463

464
        // Return
465
        return $spreadsheet;
78✔
466
    }
467

468
    private function processTableHeaderRows(
1✔
469
        DOMElement $childNode,
470
        string $tableNs,
471
        int &$rowID,
472
        string $worksheetName,
473
        string $officeNs,
474
        string $textNs,
475
        string $xlinkNs,
476
        Spreadsheet $spreadsheet
477
    ): void {
478
        foreach ($childNode->childNodes as $grandchildNode) {
1✔
479
            /** @var DOMElement $grandchildNode */
480
            $grandkey = self::extractNodeName($grandchildNode->nodeName);
1✔
481
            switch ($grandkey) {
482
                case 'table-row':
1✔
483
                    $this->processTableRow(
1✔
484
                        $grandchildNode,
1✔
485
                        $tableNs,
1✔
486
                        $rowID,
1✔
487
                        $worksheetName,
1✔
488
                        $officeNs,
1✔
489
                        $textNs,
1✔
490
                        $xlinkNs,
1✔
491
                        $spreadsheet
1✔
492
                    );
1✔
493

494
                    break;
1✔
495
            }
496
        }
497
    }
498

499
    private function processTableRowGroup(
1✔
500
        DOMElement $childNode,
501
        string $tableNs,
502
        int &$rowID,
503
        string $worksheetName,
504
        string $officeNs,
505
        string $textNs,
506
        string $xlinkNs,
507
        Spreadsheet $spreadsheet
508
    ): void {
509
        foreach ($childNode->childNodes as $grandchildNode) {
1✔
510
            /** @var DOMElement $grandchildNode */
511
            $grandkey = self::extractNodeName($grandchildNode->nodeName);
1✔
512
            switch ($grandkey) {
513
                case 'table-row':
1✔
514
                    $this->processTableRow(
1✔
515
                        $grandchildNode,
1✔
516
                        $tableNs,
1✔
517
                        $rowID,
1✔
518
                        $worksheetName,
1✔
519
                        $officeNs,
1✔
520
                        $textNs,
1✔
521
                        $xlinkNs,
1✔
522
                        $spreadsheet
1✔
523
                    );
1✔
524

525
                    break;
1✔
526
                case 'table-header-rows':
×
527
                case 'table-rows':
×
528
                    $this->processTableHeaderRows(
×
529
                        $grandchildNode,
×
530
                        $tableNs,
×
531
                        $rowID,
×
532
                        $worksheetName,
×
533
                        $officeNs,
×
534
                        $textNs,
×
535
                        $xlinkNs,
×
536
                        $spreadsheet
×
537
                    );
×
538

539
                    break;
×
540
                case 'table-row-group':
×
541
                    $this->processTableRowGroup(
×
542
                        $grandchildNode,
×
543
                        $tableNs,
×
544
                        $rowID,
×
545
                        $worksheetName,
×
546
                        $officeNs,
×
547
                        $textNs,
×
548
                        $xlinkNs,
×
549
                        $spreadsheet
×
550
                    );
×
551

552
                    break;
×
553
            }
554
        }
555
    }
556

557
    private function processTableRow(
76✔
558
        DOMElement $childNode,
559
        string $tableNs,
560
        int &$rowID,
561
        string $worksheetName,
562
        string $officeNs,
563
        string $textNs,
564
        string $xlinkNs,
565
        Spreadsheet $spreadsheet
566
    ): void {
567
        if ($childNode->hasAttributeNS($tableNs, 'number-rows-repeated')) {
76✔
568
            $rowRepeats = (int) $childNode->getAttributeNS($tableNs, 'number-rows-repeated');
26✔
569
        } else {
570
            $rowRepeats = 1;
76✔
571
        }
572

573
        $columnID = 'A';
76✔
574
        /** @var DOMElement|DOMText $cellData */
575
        foreach ($childNode->childNodes as $cellData) {
76✔
576
            if ($cellData instanceof DOMText) {
76✔
577
                continue; // should just be whitespace
2✔
578
            }
579
            if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) {
76✔
580
                if ($cellData->hasAttributeNS($tableNs, 'number-columns-repeated')) {
2✔
581
                    $colRepeats = (int) $cellData->getAttributeNS($tableNs, 'number-columns-repeated');
1✔
582
                } else {
583
                    $colRepeats = 1;
2✔
584
                }
585

586
                for ($i = 0; $i < $colRepeats; ++$i) {
2✔
587
                    StringHelper::stringIncrement($columnID);
2✔
588
                }
589

590
                continue;
2✔
591
            }
592

593
            // Initialize variables
594
            $formatting = $hyperlink = null;
76✔
595
            $hasCalculatedValue = false;
76✔
596
            $cellDataFormula = '';
76✔
597
            $cellDataType = '';
76✔
598
            $cellDataRef = '';
76✔
599

600
            if ($cellData->hasAttributeNS($tableNs, 'formula')) {
76✔
601
                $cellDataFormula = $cellData->getAttributeNS($tableNs, 'formula');
30✔
602
                $hasCalculatedValue = true;
30✔
603
            }
604
            if ($cellData->hasAttributeNS($tableNs, 'number-matrix-columns-spanned')) {
76✔
605
                if ($cellData->hasAttributeNS($tableNs, 'number-matrix-rows-spanned')) {
12✔
606
                    $cellDataType = 'array';
12✔
607
                    $arrayRow = (int) $cellData->getAttributeNS($tableNs, 'number-matrix-rows-spanned');
12✔
608
                    $arrayCol = (int) $cellData->getAttributeNS($tableNs, 'number-matrix-columns-spanned');
12✔
609
                    $lastRow = $rowID + $arrayRow - 1;
12✔
610
                    $lastCol = $columnID;
12✔
611
                    while ($arrayCol > 1) {
12✔
612
                        StringHelper::stringIncrement($lastCol);
7✔
613
                        --$arrayCol;
7✔
614
                    }
615
                    $cellDataRef = "$columnID$rowID:$lastCol$lastRow";
12✔
616
                }
617
            }
618

619
            // Annotations
620
            $annotation = $cellData->getElementsByTagNameNS($officeNs, 'annotation');
76✔
621

622
            if ($annotation->length > 0 && $annotation->item(0) !== null) {
76✔
623
                $textNode = $annotation->item(0)->getElementsByTagNameNS($textNs, 'p');
11✔
624
                $textNodeLength = $textNode->length;
11✔
625
                $newLineOwed = false;
11✔
626
                for ($textNodeIndex = 0; $textNodeIndex < $textNodeLength; ++$textNodeIndex) {
11✔
627
                    $textNodeItem = $textNode->item($textNodeIndex);
11✔
628
                    if ($textNodeItem !== null) {
11✔
629
                        $text = $this->scanElementForText($textNodeItem);
11✔
630
                        if ($newLineOwed) {
11✔
631
                            $spreadsheet->getActiveSheet()
1✔
632
                                ->getComment($columnID . $rowID)
1✔
633
                                ->getText()
1✔
634
                                ->createText("\n");
1✔
635
                        }
636
                        $newLineOwed = true;
11✔
637

638
                        $spreadsheet->getActiveSheet()
11✔
639
                            ->getComment($columnID . $rowID)
11✔
640
                            ->getText()
11✔
641
                            ->createText(
11✔
642
                                $this->parseRichText($text)
11✔
643
                            );
11✔
644
                    }
645
                }
646
            }
647

648
            // Content
649

650
            /** @var DOMElement[] $paragraphs */
651
            $paragraphs = [];
76✔
652

653
            foreach ($cellData->childNodes as $item) {
76✔
654
                /** @var DOMElement $item */
655

656
                // Filter text:p elements
657
                if ($item->nodeName == 'text:p') {
76✔
658
                    $paragraphs[] = $item;
76✔
659
                }
660
            }
661

662
            if (count($paragraphs) > 0) {
76✔
663
                $dataValue = null;
76✔
664
                // Consolidate if there are multiple p records (maybe with spans as well)
665
                $dataArray = [];
76✔
666

667
                // Text can have multiple text:p and within those, multiple text:span.
668
                // text:p newlines, but text:span does not.
669
                // Also, here we assume there is no text data is span fields are specified, since
670
                // we have no way of knowing proper positioning anyway.
671

672
                foreach ($paragraphs as $pData) {
76✔
673
                    $dataArray[] = $this->scanElementForText($pData);
76✔
674
                }
675
                $allCellDataText = implode("\n", $dataArray);
76✔
676

677
                $type = $cellData->getAttributeNS($officeNs, 'value-type');
76✔
678

679
                switch ($type) {
680
                    case 'string':
76✔
681
                        $type = DataType::TYPE_STRING;
49✔
682
                        $dataValue = $allCellDataText;
49✔
683

684
                        foreach ($paragraphs as $paragraph) {
49✔
685
                            $link = $paragraph->getElementsByTagNameNS($textNs, 'a');
49✔
686
                            if ($link->length > 0 && $link->item(0) !== null) {
49✔
687
                                $hyperlink = $link->item(0)->getAttributeNS($xlinkNs, 'href');
7✔
688
                            }
689
                        }
690

691
                        break;
49✔
692
                    case 'boolean':
52✔
693
                        $type = DataType::TYPE_BOOL;
9✔
694
                        $dataValue = ($cellData->getAttributeNS($officeNs, 'boolean-value') === 'true') ? true : false;
9✔
695

696
                        break;
9✔
697
                    case 'percentage':
50✔
698
                        if (!str_contains($allCellDataText, '.')) {
4✔
699
                            $formatting = NumberFormat::FORMAT_PERCENTAGE;
4✔
700
                        } elseif (substr($allCellDataText, -3, 1) === '.') {
4✔
NEW
UNCOV
701
                            $formatting = NumberFormat::FORMAT_PERCENTAGE_0;
×
702
                        } else {
703
                            $formatting = NumberFormat::FORMAT_PERCENTAGE_00;
4✔
704
                        }
705
                        $type = DataType::TYPE_NUMERIC;
4✔
706
                        $dataValue = (float) $cellData->getAttributeNS($officeNs, 'value');
4✔
707

708
                        break;
4✔
709
                    case 'currency':
50✔
710
                        $type = DataType::TYPE_NUMERIC;
4✔
711
                        $dataValue = (float) $cellData->getAttributeNS($officeNs, 'value');
4✔
712
                        $currency = $cellData->getAttributeNS($officeNs, 'currency');
4✔
713
                        if ($currency === 'USD') {
4✔
714
                            $typeValue = 'currency';
4✔
715
                            $formatting = str_contains($allCellDataText, '.') ? NumberFormat::FORMAT_CURRENCY_USD : NumberFormat::FORMAT_CURRENCY_USD_INTEGER;
4✔
716
                        } elseif ($currency === 'EUR' || str_contains($allCellDataText, '€')) {
4✔
717
                            $typeValue = 'currency';
4✔
718
                            $formatting = str_contains($allCellDataText, '.') ? NumberFormat::FORMAT_CURRENCY_EUR : NumberFormat::FORMAT_CURRENCY_EUR_INTEGER;
4✔
719
                        }
720

721
                        break;
4✔
722
                    case 'float':
46✔
723
                        $type = DataType::TYPE_NUMERIC;
45✔
724
                        $dataValue = (float) $cellData->getAttributeNS($officeNs, 'value');
45✔
725
                        if (str_starts_with($allCellDataText, '$')) {
45✔
NEW
726
                            $formatting = str_contains($allCellDataText, '.') ? NumberFormat::FORMAT_CURRENCY_USD : NumberFormat::FORMAT_CURRENCY_USD_INTEGER;
×
727
                        } else {
728
                            if ($dataValue === floor($dataValue)) {
45✔
729
                                // do nothing
730
                            } elseif (substr($allCellDataText, -2, 1) === '.') {
19✔
NEW
731
                                $formatting = NumberFormat::FORMAT_NUMBER_0;
×
732
                            } elseif (substr($allCellDataText, -3, 1) === '.') {
19✔
733
                                $formatting = NumberFormat::FORMAT_NUMBER_00;
8✔
734
                            }
735
                            if (floor($dataValue) == $dataValue) {
45✔
736
                                if ($dataValue == (int) $dataValue) {
41✔
737
                                    $dataValue = (int) $dataValue;
41✔
738
                                }
739
                            }
740
                        }
741

742
                        break;
45✔
743
                    case 'date':
11✔
744
                        $type = DataType::TYPE_NUMERIC;
9✔
745
                        $value = $cellData->getAttributeNS($officeNs, 'date-value');
9✔
746
                        $dataValue = Date::convertIsoDate($value);
9✔
747

748
                        if (Preg::isMatch('/^\d\d\d\d-\d\d-\d\d$/', $allCellDataText)) {
9✔
NEW
UNCOV
749
                            $formatting = 'yyyy-mm-dd';
×
750
                        } elseif (Preg::isMatch('/^\d\d?-[a-zA-Z]+-\d\d\d\d$/', $allCellDataText)) {
9✔
751
                            $formatting = 'd-mmm-yyyy';
6✔
752
                        } elseif ($dataValue != floor($dataValue)) {
9✔
753
                            $formatting = NumberFormat::FORMAT_DATE_XLSX15
6✔
754
                                . ' '
6✔
755
                                . NumberFormat::FORMAT_DATE_TIME4;
6✔
756
                        } else {
757
                            $formatting = NumberFormat::FORMAT_DATE_XLSX15;
9✔
758
                        }
759

760
                        break;
9✔
761
                    case 'time':
8✔
762
                        $type = DataType::TYPE_NUMERIC;
7✔
763

764
                        $timeValue = $cellData->getAttributeNS($officeNs, 'time-value');
7✔
765
                        $minus = '';
7✔
766
                        if (str_starts_with($timeValue, '-')) {
7✔
767
                            $minus = '-';
1✔
768
                            $timeValue = substr($timeValue, 1);
1✔
769
                        }
770
                        $timeArray = sscanf($timeValue, 'PT%dH%dM%dS');
7✔
771
                        if (is_array($timeArray)) {
7✔
772
                            /** @var array{int, int, int} $timeArray */
773
                            $days = intdiv($timeArray[0], 24);
7✔
774
                            $hours = $timeArray[0] % 24;
7✔
775
                            $dt = new DateTime("1899-12-30 $hours:{$timeArray[1]}:{$timeArray[2]}", new DateTimeZone('UTC'));
7✔
776
                            $dt->modify("+$days days");
7✔
777
                            $dataValue = Date::PHPToExcel($dt);
7✔
778
                            if ($minus === '-') {
7✔
779
                                $dataValue *= -1;
1✔
780
                                $formatting = '[hh]:mm:ss';
1✔
781
                            } else {
782
                                $formatting = NumberFormat::FORMAT_DATE_TIME4;
7✔
783
                            }
784
                        }
785

786
                        break;
7✔
787
                    default:
788
                        $dataValue = null;
1✔
789
                }
790
            } else {
791
                $type = DataType::TYPE_NULL;
44✔
792
                $dataValue = null;
44✔
793
            }
794

795
            if ($hasCalculatedValue) {
76✔
796
                $type = DataType::TYPE_FORMULA;
30✔
797
                $cellDataFormula = substr($cellDataFormula, strpos($cellDataFormula, ':=') + 1);
30✔
798
                $cellDataFormula = FormulaTranslator::convertToExcelFormulaValue($cellDataFormula);
30✔
799
            }
800

801
            if ($cellData->hasAttributeNS($tableNs, 'number-columns-repeated')) {
76✔
802
                $colRepeats = (int) $cellData->getAttributeNS($tableNs, 'number-columns-repeated');
39✔
803
            } else {
804
                $colRepeats = 1;
76✔
805
            }
806

807
            if ($type !== null) { // @phpstan-ignore-line
76✔
808
                for ($i = 0; $i < $colRepeats; ++$i) {
76✔
809
                    if ($i > 0) {
76✔
810
                        StringHelper::stringIncrement($columnID);
39✔
811
                    }
812

813
                    if ($type !== DataType::TYPE_NULL) {
76✔
814
                        for ($rowAdjust = 0; $rowAdjust < $rowRepeats; ++$rowAdjust) {
76✔
815
                            $rID = $rowID + $rowAdjust;
76✔
816

817
                            $cell = $spreadsheet->getActiveSheet()
76✔
818
                                ->getCell($columnID . $rID);
76✔
819

820
                            // Set value
821
                            if ($hasCalculatedValue) {
76✔
822
                                $cell->setValueExplicit($cellDataFormula, $type);
30✔
823
                                if ($cellDataType === 'array') {
30✔
824
                                    $cell->setFormulaAttributes(['t' => 'array', 'ref' => $cellDataRef]);
12✔
825
                                }
826
                            } elseif ($type !== '' || $dataValue !== null) {
71✔
827
                                $cell->setValueExplicit($dataValue, $type);
71✔
828
                            }
829

830
                            if ($hasCalculatedValue) {
76✔
831
                                $cell->setCalculatedValue($dataValue, $type === DataType::TYPE_NUMERIC);
30✔
832
                            }
833

834
                            // Set other properties
835
                            if ($formatting !== null) {
76✔
836
                                $spreadsheet->getActiveSheet()
14✔
837
                                    ->getStyle($columnID . $rID)
14✔
838
                                    ->getNumberFormat()
14✔
839
                                    ->setFormatCode($formatting);
14✔
840
                            } else {
841
                                $spreadsheet->getActiveSheet()
75✔
842
                                    ->getStyle($columnID . $rID)
75✔
843
                                    ->getNumberFormat()
75✔
844
                                    ->setFormatCode(NumberFormat::FORMAT_GENERAL);
75✔
845
                            }
846

847
                            if ($hyperlink !== null) {
76✔
848
                                if ($hyperlink[0] === '#') {
7✔
849
                                    $hyperlink = 'sheet://' . substr($hyperlink, 1);
1✔
850
                                }
851
                                $cell->getHyperlink()
7✔
852
                                    ->setUrl($hyperlink);
7✔
853
                            }
854
                        }
855
                    }
856
                }
857
            }
858

859
            // Merged cells
860
            $this->processMergedCells($cellData, $tableNs, $type, $columnID, $rowID, $spreadsheet);
76✔
861

862
            StringHelper::stringIncrement($columnID);
76✔
863
        }
864
        $rowID += $rowRepeats;
76✔
865
    }
866

867
    private static function extractNodeName(string $key): string
77✔
868
    {
869
        // Remove ns from node name
870
        if (str_contains($key, ':')) {
77✔
871
            $keyChunks = explode(':', $key);
77✔
872
            $key = array_pop($keyChunks);
77✔
873
        }
874

875
        return $key;
77✔
876
    }
877

878
    /**
879
     * @param string[] $columnWidths
880
     */
UNCOV
881
    private function processTableHeaderColumns(
×
882
        DOMElement $childNode,
883
        string $tableNs,
884
        array $columnWidths,
885
        int &$tableColumnIndex,
886
        Spreadsheet $spreadsheet
887
    ): void {
888
        foreach ($childNode->childNodes as $grandchildNode) {
×
889
            /** @var DOMElement $grandchildNode */
890
            $grandkey = self::extractNodeName($grandchildNode->nodeName);
×
891
            switch ($grandkey) {
892
                case 'table-column':
×
893
                    $this->processTableColumn(
×
UNCOV
894
                        $grandchildNode,
×
895
                        $tableNs,
×
UNCOV
896
                        $columnWidths,
×
UNCOV
897
                        $tableColumnIndex,
×
UNCOV
898
                        $spreadsheet
×
UNCOV
899
                    );
×
900

UNCOV
901
                    break;
×
902
            }
903
        }
904
    }
905

906
    /**
907
     * @param string[] $columnWidths
908
     */
UNCOV
909
    private function processTableColumnGroup(
×
910
        DOMElement $childNode,
911
        string $tableNs,
912
        array $columnWidths,
913
        int &$tableColumnIndex,
914
        Spreadsheet $spreadsheet
915
    ): void {
916
        foreach ($childNode->childNodes as $grandchildNode) {
×
917
            /** @var DOMElement $grandchildNode */
918
            $grandkey = self::extractNodeName($grandchildNode->nodeName);
×
919
            switch ($grandkey) {
920
                case 'table-column':
×
921
                    $this->processTableColumn(
×
UNCOV
922
                        $grandchildNode,
×
923
                        $tableNs,
×
924
                        $columnWidths,
×
925
                        $tableColumnIndex,
×
926
                        $spreadsheet
×
927
                    );
×
928

929
                    break;
×
930
                case 'table-header-columns':
×
931
                case 'table-columns':
×
932
                    $this->processTableHeaderColumns(
×
UNCOV
933
                        $grandchildNode,
×
934
                        $tableNs,
×
935
                        $columnWidths,
×
936
                        $tableColumnIndex,
×
937
                        $spreadsheet
×
938
                    );
×
939

940
                    break;
×
941
                case 'table-column-group':
×
942
                    $this->processTableColumnGroup(
×
UNCOV
943
                        $grandchildNode,
×
944
                        $tableNs,
×
UNCOV
945
                        $columnWidths,
×
UNCOV
946
                        $tableColumnIndex,
×
UNCOV
947
                        $spreadsheet
×
UNCOV
948
                    );
×
949

UNCOV
950
                    break;
×
951
            }
952
        }
953
    }
954

955
    /**
956
     * @param string[] $columnWidths
957
     */
958
    private function processTableColumn(
45✔
959
        DOMElement $childNode,
960
        string $tableNs,
961
        array $columnWidths,
962
        int &$tableColumnIndex,
963
        Spreadsheet $spreadsheet
964
    ): void {
965
        if ($childNode->hasAttributeNS($tableNs, 'number-columns-repeated')) {
45✔
966
            $rowRepeats = (int) $childNode->getAttributeNS($tableNs, 'number-columns-repeated');
40✔
967
        } else {
968
            $rowRepeats = 1;
19✔
969
        }
970
        $tableStyleName = $childNode->getAttributeNS($tableNs, 'style-name');
45✔
971
        if (isset($columnWidths[$tableStyleName])) {
45✔
972
            $columnWidth = new HelperDimension($columnWidths[$tableStyleName]);
45✔
973
            $tableColumnString = Coordinate::stringFromColumnIndex($tableColumnIndex);
45✔
974
            for ($rowRepeats2 = $rowRepeats; $rowRepeats2 > 0; --$rowRepeats2) {
45✔
975
                /** @var string $tableColumnString */
976
                $spreadsheet->getActiveSheet()
45✔
977
                    ->getColumnDimension($tableColumnString)
45✔
978
                    ->setWidth($columnWidth->toUnit('cm'), 'cm');
45✔
979
                StringHelper::stringIncrement($tableColumnString);
45✔
980
            }
981
        }
982
        $tableColumnIndex += $rowRepeats;
45✔
983
    }
984

985
    private function processSettings(ZipArchive $zip, Spreadsheet $spreadsheet): void
71✔
986
    {
987
        $dom = new DOMDocument('1.01', 'UTF-8');
71✔
988
        $dom->loadXML(
71✔
989
            $this->getSecurityScannerOrThrow()
71✔
990
                ->scan($zip->getFromName('settings.xml'))
71✔
991
        );
71✔
992
        $configNs = (string) $dom->lookupNamespaceUri('config');
71✔
993
        $officeNs = (string) $dom->lookupNamespaceUri('office');
71✔
994
        $settings = $dom->getElementsByTagNameNS($officeNs, 'settings')
71✔
995
            ->item(0);
71✔
996
        if ($settings !== null) {
71✔
997
            $this->lookForActiveSheet($settings, $spreadsheet, $configNs);
71✔
998
            $this->lookForSelectedCells($settings, $spreadsheet, $configNs);
71✔
999
        }
1000
    }
1001

1002
    private function lookForActiveSheet(DOMElement $settings, Spreadsheet $spreadsheet, string $configNs): void
71✔
1003
    {
1004
        /** @var DOMElement $t */
1005
        foreach ($settings->getElementsByTagNameNS($configNs, 'config-item') as $t) {
71✔
1006
            if ($t->getAttributeNs($configNs, 'name') === 'ActiveTable') {
70✔
1007
                try {
1008
                    $spreadsheet->setActiveSheetIndexByName($t->nodeValue ?? '');
70✔
1009
                } catch (Throwable) {
2✔
1010
                    // do nothing
1011
                }
1012

1013
                break;
70✔
1014
            }
1015
        }
1016
    }
1017

1018
    private function lookForSelectedCells(DOMElement $settings, Spreadsheet $spreadsheet, string $configNs): void
71✔
1019
    {
1020
        /** @var DOMElement $t */
1021
        foreach ($settings->getElementsByTagNameNS($configNs, 'config-item-map-named') as $t) {
71✔
1022
            if ($t->getAttributeNs($configNs, 'name') === 'Tables') {
70✔
1023
                foreach ($t->getElementsByTagNameNS($configNs, 'config-item-map-entry') as $ws) {
70✔
1024
                    $setRow = $setCol = '';
70✔
1025
                    $wsname = $ws->getAttributeNs($configNs, 'name');
70✔
1026
                    foreach ($ws->getElementsByTagNameNS($configNs, 'config-item') as $configItem) {
70✔
1027
                        $attrName = $configItem->getAttributeNs($configNs, 'name');
70✔
1028
                        if ($attrName === 'CursorPositionX') {
70✔
1029
                            $setCol = $configItem->nodeValue;
70✔
1030
                        }
1031
                        if ($attrName === 'CursorPositionY') {
70✔
1032
                            $setRow = $configItem->nodeValue;
70✔
1033
                        }
1034
                    }
1035
                    $this->setSelected($spreadsheet, $wsname, "$setCol", "$setRow");
70✔
1036
                }
1037

1038
                break;
70✔
1039
            }
1040
        }
1041
    }
1042

1043
    private function setSelected(Spreadsheet $spreadsheet, string $wsname, string $setCol, string $setRow): void
70✔
1044
    {
1045
        if (is_numeric($setCol) && is_numeric($setRow)) {
70✔
1046
            $sheet = $spreadsheet->getSheetByName($wsname);
70✔
1047
            if ($sheet !== null) {
70✔
1048
                $sheet->setSelectedCells([(int) $setCol + 1, (int) $setRow + 1]);
69✔
1049
            }
1050
        }
1051
    }
1052

1053
    /**
1054
     * Recursively scan element.
1055
     */
1056
    protected function scanElementForText(DOMNode $element): string
76✔
1057
    {
1058
        $str = '';
76✔
1059
        foreach ($element->childNodes as $child) {
76✔
1060
            /** @var DOMNode $child */
1061
            if ($child->nodeType == XML_TEXT_NODE) {
76✔
1062
                $str .= $child->nodeValue;
76✔
1063
            } elseif ($child->nodeType == XML_ELEMENT_NODE && $child->nodeName == 'text:line-break') {
18✔
1064
                $str .= "\n";
1✔
1065
            } elseif ($child->nodeType == XML_ELEMENT_NODE && $child->nodeName == 'text:s') {
18✔
1066
                // It's a space
1067

1068
                // Multiple spaces?
1069
                $attributes = $child->attributes;
6✔
1070
                /** @var ?DOMAttr $cAttr */
1071
                $cAttr = ($attributes === null) ? null : $attributes->getNamedItem('c');
6✔
1072
                $multiplier = self::getMultiplier($cAttr);
6✔
1073
                $str .= str_repeat(' ', $multiplier);
6✔
1074
            }
1075

1076
            if ($child->hasChildNodes()) {
76✔
1077
                $str .= $this->scanElementForText($child);
16✔
1078
            }
1079
        }
1080

1081
        return $str;
76✔
1082
    }
1083

1084
    private static function getMultiplier(?DOMAttr $cAttr): int
6✔
1085
    {
1086
        if ($cAttr) {
6✔
1087
            $multiplier = (int) $cAttr->nodeValue;
6✔
1088
        } else {
1089
            $multiplier = 1;
6✔
1090
        }
1091

1092
        return $multiplier;
6✔
1093
    }
1094

1095
    private function parseRichText(string $is): RichText
11✔
1096
    {
1097
        $value = new RichText();
11✔
1098
        $value->createText($is);
11✔
1099

1100
        return $value;
11✔
1101
    }
1102

1103
    private function processMergedCells(
76✔
1104
        DOMElement $cellData,
1105
        string $tableNs,
1106
        string $type,
1107
        string $columnID,
1108
        int $rowID,
1109
        Spreadsheet $spreadsheet
1110
    ): void {
1111
        if (
1112
            $cellData->hasAttributeNS($tableNs, 'number-columns-spanned')
76✔
1113
            || $cellData->hasAttributeNS($tableNs, 'number-rows-spanned')
76✔
1114
        ) {
1115
            if (($type !== DataType::TYPE_NULL) || ($this->readDataOnly === false)) {
18✔
1116
                $columnTo = $columnID;
18✔
1117

1118
                if ($cellData->hasAttributeNS($tableNs, 'number-columns-spanned')) {
18✔
1119
                    $columnIndex = Coordinate::columnIndexFromString($columnID);
17✔
1120
                    $columnIndex += (int) $cellData->getAttributeNS($tableNs, 'number-columns-spanned');
17✔
1121
                    $columnIndex -= 2;
17✔
1122

1123
                    $columnTo = Coordinate::stringFromColumnIndex($columnIndex + 1);
17✔
1124
                }
1125

1126
                $rowTo = $rowID;
18✔
1127

1128
                if ($cellData->hasAttributeNS($tableNs, 'number-rows-spanned')) {
18✔
1129
                    $rowTo = $rowTo + (int) $cellData->getAttributeNS($tableNs, 'number-rows-spanned') - 1;
18✔
1130
                }
1131

1132
                $cellRange = $columnID . $rowID . ':' . $columnTo . $rowTo;
18✔
1133
                $spreadsheet->getActiveSheet()->mergeCells($cellRange, Worksheet::MERGE_CELL_CONTENT_HIDE);
18✔
1134
            }
1135
        }
1136
    }
1137
}
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