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

PHPOffice / PhpSpreadsheet / 23373348239

21 Mar 2026 05:53AM UTC coverage: 96.906% (+0.002%) from 96.904%
23373348239

Pull #4831

github

web-flow
Merge 3e24ec381 into 4a8884bf0
Pull Request #4831: Xlsx Writer Support Data URI for Images

6 of 6 new or added lines in 2 files covered. (100.0%)

37 existing lines in 1 file now uncovered.

47740 of 49264 relevant lines covered (96.91%)

384.11 hits per line

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

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

3
namespace PhpOffice\PhpSpreadsheet\Reader;
4

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

40
class Ods extends BaseReader
41
{
42
    const INITIAL_FILE = 'content.xml';
43

44
    /**
45
     * Create a new Ods Reader instance.
46
     */
47
    public function __construct()
140✔
48
    {
49
        parent::__construct();
140✔
50
        $this->securityScanner = XmlScanner::getInstance($this);
140✔
51
    }
52

53
    /**
54
     * Can the current IReader read the file?
55
     */
56
    public function canRead(string $filename): bool
21✔
57
    {
58
        $mimeType = 'UNKNOWN';
21✔
59

60
        // Load file
61

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

87
                                    break;
1✔
88
                                }
89
                            }
90
                        }
91
                    }
92
                }
93

94
                $zip->close();
5✔
95
            }
96
        }
97

98
        return $mimeType === 'application/vnd.oasis.opendocument.spreadsheet';
21✔
99
    }
100

101
    /**
102
     * Reads names of the worksheets from a file, without parsing the whole file to a PhpSpreadsheet object.
103
     *
104
     * @return string[]
105
     */
106
    public function listWorksheetNames(string $filename): array
6✔
107
    {
108
        File::assertFile($filename, self::INITIAL_FILE);
6✔
109

110
        $worksheetNames = [];
2✔
111

112
        $xml = new XMLReader();
2✔
113
        $xml->xml(
2✔
114
            $this->getSecurityScannerOrThrow()
2✔
115
                ->scanFile(
2✔
116
                    'zip://' . realpath($filename) . '#' . self::INITIAL_FILE
2✔
117
                )
2✔
118
        );
2✔
119
        $xml->setParserProperty(2, true);
2✔
120

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

148
        return $worksheetNames;
2✔
149
    }
150

151
    /**
152
     * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
153
     *
154
     * @return array<int, array{
155
     *   worksheetName: string,
156
     *   lastColumnLetter: string,
157
     *   lastColumnIndex: int,
158
     *   totalRows: int,
159
     *   totalColumns: int,
160
     *   sheetState: string
161
     * }>
162
     */
163
    public function listWorksheetInfo(string $filename): array
8✔
164
    {
165
        File::assertFile($filename, self::INITIAL_FILE);
8✔
166

167
        $worksheetInfo = [];
4✔
168

169
        $xml = new XMLReader();
4✔
170
        $xml->xml(
4✔
171
            $this->getSecurityScannerOrThrow()
4✔
172
                ->scanFile(
4✔
173
                    'zip://' . realpath($filename) . '#' . self::INITIAL_FILE
4✔
174
                )
4✔
175
        );
4✔
176
        $xml->setParserProperty(2, true);
4✔
177

178
        // Step into the first level of content of the XML
179
        $xml->read();
4✔
180
        $tableVisibility = [];
4✔
181
        $lastTableStyle = '';
4✔
182

183
        while ($xml->read()) {
4✔
184
            if ($xml->name === 'style:style') {
4✔
185
                $styleType = $xml->getAttribute('style:family');
4✔
186
                if ($styleType === 'table') {
4✔
187
                    $lastTableStyle = $xml->getAttribute('style:name');
4✔
188
                }
189
            } elseif ($xml->name === 'style:table-properties') {
4✔
190
                $visibility = $xml->getAttribute('table:display');
4✔
191
                $tableVisibility[$lastTableStyle] = ($visibility === 'false') ? Worksheet::SHEETSTATE_HIDDEN : Worksheet::SHEETSTATE_VISIBLE;
4✔
192
            } elseif ($xml->name == 'table:table' && $xml->nodeType == XMLReader::ELEMENT) {
4✔
193
                $worksheetNames[] = $xml->getAttribute('table:name');
4✔
194

195
                $styleName = $xml->getAttribute('table:style-name') ?? '';
4✔
196
                $visibility = $tableVisibility[$styleName] ?? '';
4✔
197
                $tmpInfo = [
4✔
198
                    'worksheetName' => (string) $xml->getAttribute('table:name'),
4✔
199
                    'lastColumnLetter' => 'A',
4✔
200
                    'lastColumnIndex' => 0,
4✔
201
                    'totalRows' => 0,
4✔
202
                    'totalColumns' => 0,
4✔
203
                    'sheetState' => $visibility,
4✔
204
                ];
4✔
205

206
                // Loop through each child node of the table:table element reading
207
                $currRow = 0;
4✔
208
                do {
209
                    $xml->read();
4✔
210
                    if ($xml->name == 'table:table-row' && $xml->nodeType == XMLReader::ELEMENT) {
4✔
211
                        $rowspan = $xml->getAttribute('table:number-rows-repeated');
4✔
212
                        $rowspan = empty($rowspan) ? 1 : (int) $rowspan;
4✔
213
                        $currRow += $rowspan;
4✔
214
                        $currCol = 0;
4✔
215
                        // Step into the row
216
                        $xml->read();
4✔
217
                        do {
218
                            $doread = true;
4✔
219
                            if ($xml->name == 'table:table-cell' && $xml->nodeType == XMLReader::ELEMENT) {
4✔
220
                                $mergeSize = $xml->getAttribute('table:number-columns-repeated');
4✔
221
                                $mergeSize = empty($mergeSize) ? 1 : (int) $mergeSize;
4✔
222
                                $currCol += $mergeSize;
4✔
223
                                if (!$xml->isEmptyElement) {
4✔
224
                                    $tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCol);
4✔
225
                                    $tmpInfo['totalRows'] = $currRow;
4✔
226
                                    $xml->next();
4✔
227
                                    $doread = false;
4✔
228
                                }
229
                            } elseif ($xml->name == 'table:covered-table-cell' && $xml->nodeType == XMLReader::ELEMENT) {
2✔
230
                                $mergeSize = $xml->getAttribute('table:number-columns-repeated');
2✔
231
                                $currCol += (int) $mergeSize;
2✔
232
                            }
233
                            if ($doread) {
4✔
234
                                $xml->read();
3✔
235
                            }
236
                        } while ($xml->name != 'table:table-row');
4✔
237
                    }
238
                } while ($xml->name != 'table:table');
4✔
239

240
                $tmpInfo['lastColumnIndex'] = $tmpInfo['totalColumns'] - 1;
4✔
241
                $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1, true);
4✔
242
                $worksheetInfo[] = $tmpInfo;
4✔
243
            }
244
        }
245

246
        return $worksheetInfo;
4✔
247
    }
248

249
    /**
250
     * Loads PhpSpreadsheet from file.
251
     */
252
    protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
104✔
253
    {
254
        $spreadsheet = $this->newSpreadsheet();
104✔
255
        $spreadsheet->setValueBinder($this->valueBinder);
104✔
256
        $spreadsheet->removeSheetByIndex(0);
104✔
257

258
        // Load into this instance
259
        return $this->loadIntoExisting($filename, $spreadsheet);
104✔
260
    }
261

262
    /** @var array<string,
263
     *  array{
264
     *     font?:array{
265
     *       autoColor?: true,
266
     *       bold?: true,
267
     *       color?: array{rgb: string},
268
     *       italic?: true,
269
     *       name?: non-empty-string,
270
     *       size?: float|int,
271
     *       strikethrough?: true,
272
     *       underline?: 'double'|'single',
273
     *    },
274
     *    fill?:array{
275
     *      fillType?: string,
276
     *      startColor?: array{rgb: string},
277
     *    },
278
     *    alignment?:array{
279
     *      horizontal?: string,
280
     *      readOrder?: int,
281
     *      shrinkToFit?: bool,
282
     *      textRotation?: int,
283
     *      vertical?: string,
284
     *      wrapText?: bool,
285
     *    },
286
     *    protection?:array{
287
     *      locked?: string,
288
     *      hidden?: string,
289
     *    },
290
     *    borders?:array{
291
     *      bottom?: array{borderStyle:string, color:array{rgb: string}},
292
     *      left?: array{borderStyle:string, color:array{rgb: string}},
293
     *      right?: array{borderStyle:string, color:array{rgb: string}},
294
     *      top?: array{borderStyle:string, color:array{rgb: string}},
295
     *      diagonal?: array{borderStyle:string, color:array{rgb: string}},
296
     *      diagonalDirection?: int,
297
     *    },
298
     *    numberFormat?:array{formatCode: string},
299
     *  }>
300
     */
301
    private array $allStyles;
302

303
    /** @var string[] */
304
    private array $numberFormats;
305

306
    private int $highestDataIndex;
307

308
    /**
309
     * Loads PhpSpreadsheet from file into PhpSpreadsheet instance.
310
     */
311
    public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Spreadsheet
109✔
312
    {
313
        File::assertFile($filename, self::INITIAL_FILE);
109✔
314

315
        $zip = new ZipArchive();
105✔
316
        $zip->open($filename);
105✔
317

318
        // Meta
319

320
        $xml = @simplexml_load_string(
105✔
321
            $this->getSecurityScannerOrThrow()
105✔
322
                ->scan($zip->getFromName('meta.xml'))
105✔
323
        );
105✔
324
        if ($xml === false) {
105✔
325
            throw new Exception('Unable to read data from {$pFilename}');
1✔
326
        }
327

328
        /** @var array{meta?: string, office?: string, dc?: string} */
329
        $namespacesMeta = $xml->getNamespaces(true);
104✔
330

331
        (new DocumentProperties($spreadsheet))->load($xml, $namespacesMeta);
104✔
332

333
        // Styles
334

335
        $this->allStyles = $this->numberFormats = [];
104✔
336
        $dom = new DOMDocument('1.01', 'UTF-8');
104✔
337
        $dom->loadXML(
104✔
338
            $this->getSecurityScannerOrThrow()
104✔
339
                ->scan($zip->getFromName('styles.xml'))
104✔
340
        );
104✔
341
        $officeNs = (string) $dom->lookupNamespaceUri('office');
104✔
342
        $styleNs = (string) $dom->lookupNamespaceUri('style');
104✔
343
        $fontNs = (string) $dom->lookupNamespaceUri('fo');
104✔
344
        $numberNs = (string) $dom->lookupNamespaceUri('number');
104✔
345
        $tableNs = (string) $dom->lookupNamespaceUri('table');
104✔
346
        $textNs = (string) $dom->lookupNamespaceUri('text');
104✔
347
        $xlinkNs = (string) $dom->lookupNamespaceUri('xlink');
104✔
348

349
        $automaticStyle0 = $this->readDataOnly ? null : $dom->getElementsByTagNameNS($officeNs, 'styles')->item(0);
104✔
350
        $this->processSomeNumberFormats($automaticStyle0, $numberNs, $styleNs);
104✔
351
        $automaticStyles = ($automaticStyle0 === null) ? [] : $automaticStyle0->getElementsByTagNameNS($styleNs, 'default-style');
104✔
352
        foreach ($automaticStyles as $automaticStyle) {
104✔
353
            $styleFamily = $automaticStyle->getAttributeNS($styleNs, 'family');
96✔
354
            if ($styleFamily === 'table-cell') {
96✔
355
                $fonts = [];
91✔
356
                foreach ($automaticStyle->getElementsByTagNameNS($styleNs, 'text-properties') as $textProperty) {
91✔
357
                    $fonts = $this->getFontStyles($textProperty, $styleNs, $fontNs);
91✔
358
                }
359
                if (!empty($fonts)) {
91✔
360
                    $spreadsheet->getDefaultStyle()
91✔
361
                        ->getFont()
91✔
362
                        ->applyFromArray($fonts);
91✔
363
                }
364
            }
365
        }
366
        $automaticStyles = ($automaticStyle0 === null) ? [] : $automaticStyle0->getElementsByTagNameNS($styleNs, 'style');
104✔
367
        foreach ($automaticStyles as $automaticStyle) {
104✔
368
            $styleName = $automaticStyle->getAttributeNS($styleNs, 'name');
101✔
369
            $styleFamily = $automaticStyle->getAttributeNS($styleNs, 'family');
101✔
370
            if ($styleFamily === 'table-cell') {
101✔
371
                $fills = $fonts = [];
101✔
372
                foreach ($automaticStyle->getElementsByTagNameNS($styleNs, 'text-properties') as $textProperty) {
101✔
373
                    $fonts = $this->getFontStyles($textProperty, $styleNs, $fontNs);
53✔
374
                }
375
                foreach ($automaticStyle->getElementsByTagNameNS($styleNs, 'table-cell-properties') as $tableCellProperty) {
101✔
376
                    $fills = $this->getFillStyles($tableCellProperty, $fontNs);
101✔
377
                }
378
                if ($styleName !== '') {
101✔
379
                    if (!empty($fonts)) {
101✔
380
                        $this->allStyles[$styleName]['font'] = $fonts;
53✔
381
                        if ($styleName === 'Default') {
53✔
382
                            $spreadsheet->getDefaultStyle()
15✔
383
                                ->getFont()
15✔
384
                                ->applyFromArray($fonts);
15✔
385
                        }
386
                    }
387
                    if (!empty($fills)) {
101✔
388
                        $this->allStyles[$styleName]['fill'] = $fills;
93✔
389
                        if ($styleName === 'Default') {
93✔
390
                            $spreadsheet->getDefaultStyle()
57✔
391
                                ->getFill()
57✔
392
                                ->applyFromArray($fills);
57✔
393
                        }
394
                    }
395
                }
396
            }
397
        }
398

399
        $automaticStyle0 = $this->readDataOnly ? null : $dom->getElementsByTagNameNS($officeNs, 'automatic-styles')->item(0);
104✔
400
        $this->processSomeNumberFormats($automaticStyle0, $numberNs, $styleNs);
104✔
401

402
        $pageSettings = new PageSettings($dom);
104✔
403

404
        // Main Content
405

406
        $dom = new DOMDocument('1.01', 'UTF-8');
104✔
407
        $dom->loadXML(
104✔
408
            $this->getSecurityScannerOrThrow()
104✔
409
                ->scan($zip->getFromName(self::INITIAL_FILE))
104✔
410
        );
104✔
411

412
        $pageSettings->readStyleCrossReferences($dom);
104✔
413

414
        $autoFilterReader = new AutoFilter($spreadsheet, $tableNs);
104✔
415
        $definedNameReader = new DefinedNames($spreadsheet, $tableNs);
104✔
416
        $columnWidths = [];
104✔
417
        $automaticStyle0 = $this->readDataOnly ? null : $dom->getElementsByTagNameNS($officeNs, 'automatic-styles')->item(0);
104✔
418
        $this->processSomeNumberFormats($automaticStyle0, $numberNs, $styleNs);
104✔
419
        $automaticStyles = ($automaticStyle0 === null) ? [] : $automaticStyle0->getElementsByTagNameNS($styleNs, 'style');
104✔
420
        foreach ($automaticStyles as $automaticStyle) {
104✔
421
            $styleName = $automaticStyle->getAttributeNS($styleNs, 'name');
101✔
422
            $styleFamily = $automaticStyle->getAttributeNS($styleNs, 'family');
101✔
423
            if ($styleFamily === 'table-column') {
101✔
424
                $tcprops = $automaticStyle->getElementsByTagNameNS($styleNs, 'table-column-properties');
55✔
425
                $tcprop = $tcprops->item(0);
55✔
426
                if ($tcprop !== null) {
55✔
427
                    $columnWidth = $tcprop->getAttributeNs($styleNs, 'column-width');
55✔
428
                    $columnWidths[$styleName] = $columnWidth;
55✔
429
                }
430
            }
431
            if ($styleFamily === 'table-cell') {
101✔
432
                $fonts = $fills = $alignment1 = $alignment2 = $protection = $borders = [];
86✔
433
                $numberFormatName = $automaticStyle->getAttributeNS($styleNs, 'data-style-name');
86✔
434
                $numberFormat = $this->numberFormats[$numberFormatName] ?? '';
86✔
435
                foreach ($automaticStyle->getElementsByTagNameNS($styleNs, 'text-properties') as $textProperty) {
86✔
436
                    $fonts = $this->getFontStyles($textProperty, $styleNs, $fontNs);
63✔
437
                }
438
                foreach ($automaticStyle->getElementsByTagNameNS($styleNs, 'table-cell-properties') as $tableCellProperty) {
86✔
439
                    $fills = $this->getFillStyles($tableCellProperty, $fontNs);
76✔
440
                    $borders = $this->getBorderStyles($tableCellProperty, $fontNs, $styleNs);
76✔
441
                    $protection = $this->getProtectionStyles($tableCellProperty, $styleNs);
76✔
442
                }
443
                foreach ($automaticStyle->getElementsByTagNameNS($styleNs, 'table-cell-properties') as $tableCellProperty) {
86✔
444
                    $alignment1 = $this->getAlignment1Styles($tableCellProperty, $styleNs, $fontNs);
76✔
445
                }
446
                foreach ($automaticStyle->getElementsByTagNameNS($styleNs, 'paragraph-properties') as $paragraphProperty) {
86✔
447
                    $alignment2 = $this->getAlignment2Styles($paragraphProperty, $styleNs, $fontNs);
23✔
448
                }
449
                if ($styleName !== '') {
86✔
450
                    if (!empty($fonts)) {
86✔
451
                        $this->allStyles[$styleName]['font'] = $fonts;
62✔
452
                    }
453
                    if (!empty($fills)) {
86✔
454
                        $this->allStyles[$styleName]['fill'] = $fills;
59✔
455
                    }
456
                    $alignment = array_merge($alignment1, $alignment2);
86✔
457
                    if (!empty($alignment)) {
86✔
458
                        $this->allStyles[$styleName]['alignment'] = $alignment;
72✔
459
                    }
460
                    if (!empty($protection)) {
86✔
461
                        $this->allStyles[$styleName]['protection'] = $protection;
10✔
462
                    }
463
                    if (!empty($borders)) {
86✔
464
                        $this->allStyles[$styleName]['borders'] = $borders;
12✔
465
                    }
466
                    if ($numberFormat !== '') {
86✔
467
                        $this->allStyles[$styleName]['numberFormat']['formatCode'] = $numberFormat;
3✔
468
                    }
469
                }
470
            }
471
        }
472

473
        // Content
474
        $item0 = $dom->getElementsByTagNameNS($officeNs, 'body')->item(0);
104✔
475
        $spreadsheets = ($item0 === null) ? [] : $item0->getElementsByTagNameNS($officeNs, 'spreadsheet');
104✔
476

477
        foreach ($spreadsheets as $workbookData) {
104✔
478
            /** @var DOMElement $workbookData */
479
            $tables = $workbookData->getElementsByTagNameNS($tableNs, 'table');
104✔
480

481
            $worksheetID = 0;
104✔
482
            $sheetCreated = false;
104✔
483
            foreach ($tables as $worksheetDataSet) {
104✔
484
                /** @var DOMElement $worksheetDataSet */
485
                $worksheetName = $worksheetDataSet->getAttributeNS($tableNs, 'name');
104✔
486

487
                // Check loadSheetsOnly
488
                if (
489
                    $this->loadSheetsOnly !== null
104✔
490
                    && $worksheetName
491
                    && !in_array($worksheetName, $this->loadSheetsOnly)
104✔
492
                ) {
493
                    continue;
5✔
494
                }
495

496
                $worksheetStyleName = $worksheetDataSet->getAttributeNS($tableNs, 'style-name');
101✔
497

498
                // Create sheet
499
                $spreadsheet->createSheet();
101✔
500
                $sheetCreated = true;
101✔
501
                $spreadsheet->setActiveSheetIndex($worksheetID);
101✔
502

503
                if ($worksheetName || is_numeric($worksheetName)) {
101✔
504
                    // Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in
505
                    // formula cells... during the load, all formulae should be correct, and we're simply
506
                    // bringing the worksheet name in line with the formula, not the reverse
507
                    $spreadsheet->getActiveSheet()
101✔
508
                        ->setTitle((string) $worksheetName, false, false);
101✔
509
                }
510

511
                // Go through every child of table element
512
                $rowID = 1;
101✔
513
                $tableColumnIndex = 1;
101✔
514
                $this->highestDataIndex = AddressRange::MAX_COLUMN_INT;
101✔
515
                foreach ($worksheetDataSet->childNodes as $childNode) {
101✔
516
                    /** @var DOMElement $childNode */
517

518
                    // Filter elements which are not under the "table" ns
519
                    if ($childNode->namespaceURI != $tableNs) {
101✔
520
                        continue;
63✔
521
                    }
522

523
                    $key = self::extractNodeName($childNode->nodeName);
101✔
524

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

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

552
                            break;
1✔
553
                        case 'table-row':
101✔
554
                            $this->processTableRow(
100✔
555
                                $childNode,
100✔
556
                                $tableNs,
100✔
557
                                $rowID,
100✔
558
                                $worksheetName,
100✔
559
                                $officeNs,
100✔
560
                                $textNs,
100✔
561
                                $xlinkNs,
100✔
562
                                $spreadsheet
100✔
563
                            );
100✔
564

565
                            break;
100✔
566
                        case 'table-header-columns':
55✔
567
                        case 'table-columns':
55✔
568
                            $this->processTableColumnHeader(
1✔
569
                                $childNode,
1✔
570
                                $tableNs,
1✔
571
                                $columnWidths,
1✔
572
                                $tableColumnIndex,
1✔
573
                                $spreadsheet,
1✔
574
                                $this->readEmptyCells,
1✔
575
                                true
1✔
576
                            );
1✔
577

578
                            break;
1✔
579
                        case 'table-column-group':
55✔
580
                            $this->processTableColumnGroup(
1✔
581
                                $childNode,
1✔
582
                                $tableNs,
1✔
583
                                $columnWidths,
1✔
584
                                $tableColumnIndex,
1✔
585
                                $spreadsheet,
1✔
586
                                $this->readEmptyCells,
1✔
587
                                true
1✔
588
                            );
1✔
589

590
                            break;
1✔
591
                        case 'table-column':
54✔
592
                            $this->processTableColumn(
54✔
593
                                $childNode,
54✔
594
                                $tableNs,
54✔
595
                                $columnWidths,
54✔
596
                                $tableColumnIndex,
54✔
597
                                $spreadsheet,
54✔
598
                                $this->readEmptyCells,
54✔
599
                                true
54✔
600
                            );
54✔
601

602
                            break;
54✔
603
                    }
604
                }
605
                $pageSettings->setVisibilityForWorksheet(
101✔
606
                    $spreadsheet->getActiveSheet(),
101✔
607
                    $worksheetStyleName
101✔
608
                );
101✔
609
                $pageSettings->setPrintSettingsForWorksheet(
101✔
610
                    $spreadsheet->getActiveSheet(),
101✔
611
                    $worksheetStyleName
101✔
612
                );
101✔
613
                ++$worksheetID;
101✔
614
            }
615
            if ($this->createBlankSheetIfNoneRead && !$sheetCreated) {
104✔
616
                $spreadsheet->createSheet();
1✔
617
            }
618
        }
619

620
        foreach ($spreadsheets as $workbookData) {
104✔
621
            /** @var DOMElement $workbookData */
622
            $tables = $workbookData->getElementsByTagNameNS($tableNs, 'table');
104✔
623

624
            $worksheetID = 0;
104✔
625
            foreach ($tables as $worksheetDataSet) {
104✔
626
                /** @var DOMElement $worksheetDataSet */
627
                $worksheetName = $worksheetDataSet->getAttributeNS($tableNs, 'name');
104✔
628

629
                // Check loadSheetsOnly
630
                if (
631
                    $this->loadSheetsOnly !== null
104✔
632
                    && $worksheetName
633
                    && !in_array($worksheetName, $this->loadSheetsOnly)
104✔
634
                ) {
635
                    continue;
5✔
636
                }
637

638
                // Create sheet
639
                $spreadsheet->setActiveSheetIndex($worksheetID);
101✔
640
                $highestDataColumn = $spreadsheet->getActiveSheet()->getHighestDataColumn();
101✔
641
                $this->highestDataIndex = Coordinate::columnIndexFromString($highestDataColumn);
101✔
642

643
                // Go through every child of table element processing column widths
644
                $rowID = 1;
101✔
645
                $tableColumnIndex = 1;
101✔
646
                foreach ($worksheetDataSet->childNodes as $childNode) {
101✔
647
                    /** @var DOMElement $childNode */
648
                    if (empty($columnWidths) || $this->readEmptyCells) {
101✔
649
                        break;
96✔
650
                    }
651

652
                    // Filter elements which are not under the "table" ns
653
                    if ($childNode->namespaceURI != $tableNs) {
5✔
654
                        continue;
1✔
655
                    }
656

657
                    $key = self::extractNodeName($childNode->nodeName);
5✔
658

659
                    switch ($key) {
660
                        case 'table-header-columns':
5✔
661
                        case 'table-columns':
5✔
662
                            $this->processTableColumnHeader(
1✔
663
                                $childNode,
1✔
664
                                $tableNs,
1✔
665
                                $columnWidths,
1✔
666
                                $tableColumnIndex,
1✔
667
                                $spreadsheet,
1✔
668
                                true,
1✔
669
                                false
1✔
670
                            );
1✔
671

672
                            break;
1✔
673
                        case 'table-column-group':
5✔
674
                            $this->processTableColumnGroup(
1✔
675
                                $childNode,
1✔
676
                                $tableNs,
1✔
677
                                $columnWidths,
1✔
678
                                $tableColumnIndex,
1✔
679
                                $spreadsheet,
1✔
680
                                true,
1✔
681
                                false
1✔
682
                            );
1✔
683

684
                            break;
1✔
685
                        case 'table-column':
5✔
686
                            $this->processTableColumn(
4✔
687
                                $childNode,
4✔
688
                                $tableNs,
4✔
689
                                $columnWidths,
4✔
690
                                $tableColumnIndex,
4✔
691
                                $spreadsheet,
4✔
692
                                true,
4✔
693
                                false
4✔
694
                            );
4✔
695

696
                            break;
4✔
697
                    }
698
                }
699
                ++$worksheetID;
101✔
700
            }
701

702
            $autoFilterReader->read($workbookData);
104✔
703
            $definedNameReader->read($workbookData);
104✔
704
        }
705

706
        $spreadsheet->setActiveSheetIndex(0);
104✔
707

708
        if ($zip->locateName('settings.xml') !== false) {
102✔
709
            $this->processSettings($zip, $spreadsheet);
94✔
710
        }
711

712
        // Return
713
        return $spreadsheet;
102✔
714
    }
715

716
    private function processTableHeaderRows(
1✔
717
        DOMElement $childNode,
718
        string $tableNs,
719
        int &$rowID,
720
        string $worksheetName,
721
        string $officeNs,
722
        string $textNs,
723
        string $xlinkNs,
724
        Spreadsheet $spreadsheet
725
    ): void {
726
        foreach ($childNode->childNodes as $grandchildNode) {
1✔
727
            /** @var DOMElement $grandchildNode */
728
            $grandkey = self::extractNodeName($grandchildNode->nodeName);
1✔
729
            switch ($grandkey) {
730
                case 'table-row':
1✔
731
                    $this->processTableRow(
1✔
732
                        $grandchildNode,
1✔
733
                        $tableNs,
1✔
734
                        $rowID,
1✔
735
                        $worksheetName,
1✔
736
                        $officeNs,
1✔
737
                        $textNs,
1✔
738
                        $xlinkNs,
1✔
739
                        $spreadsheet
1✔
740
                    );
1✔
741

742
                    break;
1✔
743
            }
744
        }
745
    }
746

747
    private function processTableRowGroup(
1✔
748
        DOMElement $childNode,
749
        string $tableNs,
750
        int &$rowID,
751
        string $worksheetName,
752
        string $officeNs,
753
        string $textNs,
754
        string $xlinkNs,
755
        Spreadsheet $spreadsheet
756
    ): void {
757
        foreach ($childNode->childNodes as $grandchildNode) {
1✔
758
            /** @var DOMElement $grandchildNode */
759
            $grandkey = self::extractNodeName($grandchildNode->nodeName);
1✔
760
            switch ($grandkey) {
761
                case 'table-row':
1✔
762
                    $this->processTableRow(
1✔
763
                        $grandchildNode,
1✔
764
                        $tableNs,
1✔
765
                        $rowID,
1✔
766
                        $worksheetName,
1✔
767
                        $officeNs,
1✔
768
                        $textNs,
1✔
769
                        $xlinkNs,
1✔
770
                        $spreadsheet
1✔
771
                    );
1✔
772

773
                    break;
1✔
774
                case 'table-header-rows':
×
775
                case 'table-rows':
×
776
                    $this->processTableHeaderRows(
×
777
                        $grandchildNode,
×
778
                        $tableNs,
×
779
                        $rowID,
×
780
                        $worksheetName,
×
781
                        $officeNs,
×
782
                        $textNs,
×
783
                        $xlinkNs,
×
784
                        $spreadsheet
×
UNCOV
785
                    );
×
786

UNCOV
787
                    break;
×
UNCOV
788
                case 'table-row-group':
×
UNCOV
789
                    $this->processTableRowGroup(
×
UNCOV
790
                        $grandchildNode,
×
UNCOV
791
                        $tableNs,
×
UNCOV
792
                        $rowID,
×
UNCOV
793
                        $worksheetName,
×
UNCOV
794
                        $officeNs,
×
UNCOV
795
                        $textNs,
×
UNCOV
796
                        $xlinkNs,
×
UNCOV
797
                        $spreadsheet
×
UNCOV
798
                    );
×
799

UNCOV
800
                    break;
×
801
            }
802
        }
803
    }
804

805
    private function processTableRow(
100✔
806
        DOMElement $childNode,
807
        string $tableNs,
808
        int &$rowID,
809
        string $worksheetName,
810
        string $officeNs,
811
        string $textNs,
812
        string $xlinkNs,
813
        Spreadsheet $spreadsheet
814
    ): void {
815
        if ($childNode->hasAttributeNS($tableNs, 'number-rows-repeated')) {
100✔
816
            $rowRepeats = (int) $childNode->getAttributeNS($tableNs, 'number-rows-repeated');
33✔
817
        } else {
818
            $rowRepeats = 1;
100✔
819
        }
820
        $worksheet = $spreadsheet->getSheetByName($worksheetName);
100✔
821

822
        $columnID = 'A';
100✔
823
        /** @var DOMElement|DOMText $cellData */
824
        foreach ($childNode->childNodes as $cellData) {
100✔
825
            if ($cellData instanceof DOMText) {
100✔
826
                continue; // should just be whitespace
3✔
827
            }
828
            if ($cellData->hasAttributeNS($tableNs, 'number-columns-repeated')) {
100✔
829
                $colRepeats = (int) $cellData->getAttributeNS($tableNs, 'number-columns-repeated');
44✔
830
            } else {
831
                $colRepeats = 1;
100✔
832
            }
833
            $styleName = $cellData->getAttributeNS($tableNs, 'style-name');
100✔
834
            if ($styleName === '') {
100✔
835
                if ($worksheet === null || !$worksheet->columnDimensionExists($columnID)) {
60✔
836
                    $assignedNumberFormat = '';
13✔
837
                } else {
838
                    $colStyle = $worksheet->getColumnDimension($columnID)->getXfIndex() ?? 0;
47✔
839
                    $assignedNumberFormat = $spreadsheet
47✔
840
                        ->getCellXfByIndex($colStyle)
47✔
841
                        ->getNumberFormat()->getFormatCode();
47✔
842
                    if ($assignedNumberFormat === NumberFormat::FORMAT_GENERAL) {
47✔
843
                        $assignedNumberFormat = '';
45✔
844
                    }
845
                }
846
            } else {
847
                $assignedNumberFormat = $this->allStyles[$styleName]['numberFormat']['formatCode'] ?? '';
84✔
848
            }
849

850
            // When a cell has number-columns-repeated, check if ANY column in the
851
            // repeated range passes the read filter. If not, skip the entire group.
852
            // If some columns pass, we need to fall through to the processing block
853
            // which will handle per-column filtering.
854
            if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) {
100✔
855
                if ($colRepeats <= 1) {
3✔
856
                    StringHelper::stringIncrement($columnID);
3✔
857

858
                    continue;
3✔
859
                }
860

861
                // Check if any column within this repeated group passes the filter
862
                $anyColumnPasses = false;
2✔
863
                $tempCol = $columnID;
2✔
864
                for ($i = 0; $i < $colRepeats; ++$i) {
2✔
865
                    if ($i > 0) {
2✔
866
                        StringHelper::stringIncrement($tempCol);
2✔
867
                    }
868
                    if ($this->getReadFilter()->readCell($tempCol, $rowID, $worksheetName)) {
2✔
869
                        $anyColumnPasses = true;
2✔
870

871
                        break;
2✔
872
                    }
873
                }
874

875
                if (!$anyColumnPasses) {
2✔
876
                    for ($i = 0; $i < $colRepeats; ++$i) {
2✔
877
                        StringHelper::stringIncrement($columnID);
2✔
878
                    }
879

880
                    continue;
2✔
881
                }
882
                // Fall through to process the cell, with per-column filter checks
883
            }
884
            if ($worksheet !== null && ($cellData->hasChildNodes() || ($cellData->nextSibling !== null)) && isset($this->allStyles[$styleName])) {
100✔
885
                $spannedRange = "$columnID$rowID";
70✔
886
                // the following is sufficient for ods,
887
                // and does no harm for xlsx/xls.
888
                $worksheet->getStyle($spannedRange)
70✔
889
                    ->applyFromArray($this->allStyles[$styleName]);
70✔
890
                // the rest of this block is needed for xlsx/xls,
891
                // and does no harm for ods.
892
                if (isset($this->allStyles[$styleName]['borders'])) {
70✔
893
                    $spannedRows = $cellData->getAttributeNS($tableNs, 'number-columns-spanned');
10✔
894
                    $spannedColumns = $cellData->getAttributeNS($tableNs, 'number-rows-spanned');
10✔
895
                    $spannedRows = max((int) $spannedRows, 1);
10✔
896
                    $spannedColumns = max((int) $spannedColumns, 1);
10✔
897
                    if ($spannedRows > 1 || $spannedColumns > 1) {
10✔
898
                        $endRow = $rowID + $spannedRows - 1;
7✔
899
                        $endCol = $columnID;
7✔
900
                        while ($spannedColumns > 1) {
7✔
901
                            StringHelper::stringIncrement($endCol);
7✔
902
                            --$spannedColumns;
7✔
903
                        }
904
                        $spannedRange .= ":$endCol$endRow";
7✔
905
                        $worksheet->getStyle($spannedRange)
7✔
906
                            ->getBorders()
7✔
907
                            ->applyFromArray(
7✔
908
                                $this->allStyles[$styleName]['borders']
7✔
909
                            );
7✔
910
                    }
911
                }
912
            }
913

914
            // Initialize variables
915
            $formatting = $hyperlink = null;
100✔
916
            $hasCalculatedValue = false;
100✔
917
            $cellDataFormula = '';
100✔
918
            $cellDataType = '';
100✔
919
            $cellDataRef = '';
100✔
920

921
            if ($cellData->hasAttributeNS($tableNs, 'formula')) {
100✔
922
                $cellDataFormula = $cellData->getAttributeNS($tableNs, 'formula');
39✔
923
                $hasCalculatedValue = true;
39✔
924
            }
925
            if ($cellData->hasAttributeNS($tableNs, 'number-matrix-columns-spanned')) {
100✔
926
                if ($cellData->hasAttributeNS($tableNs, 'number-matrix-rows-spanned')) {
12✔
927
                    $cellDataType = 'array';
12✔
928
                    $arrayRow = (int) $cellData->getAttributeNS($tableNs, 'number-matrix-rows-spanned');
12✔
929
                    $arrayCol = (int) $cellData->getAttributeNS($tableNs, 'number-matrix-columns-spanned');
12✔
930
                    $lastRow = $rowID + $arrayRow - 1;
12✔
931
                    $lastCol = $columnID;
12✔
932
                    while ($arrayCol > 1) {
12✔
933
                        StringHelper::stringIncrement($lastCol);
7✔
934
                        --$arrayCol;
7✔
935
                    }
936
                    $cellDataRef = "$columnID$rowID:$lastCol$lastRow";
12✔
937
                }
938
            }
939

940
            // Annotations
941
            $annotation = $cellData->getElementsByTagNameNS($officeNs, 'annotation');
100✔
942

943
            if ($annotation->length > 0 && $annotation->item(0) !== null) {
100✔
944
                $textNode = $annotation->item(0)->getElementsByTagNameNS($textNs, 'p');
12✔
945
                $textNodeLength = $textNode->length;
12✔
946
                $newLineOwed = false;
12✔
947
                for ($textNodeIndex = 0; $textNodeIndex < $textNodeLength; ++$textNodeIndex) {
12✔
948
                    $textNodeItem = $textNode->item($textNodeIndex);
12✔
949
                    if ($textNodeItem !== null) {
12✔
950
                        $text = $this->scanElementForText($textNodeItem);
12✔
951
                        if ($newLineOwed) {
12✔
952
                            $spreadsheet->getActiveSheet()
1✔
953
                                ->getComment($columnID . $rowID)
1✔
954
                                ->getText()
1✔
955
                                ->createText("\n");
1✔
956
                        }
957
                        $newLineOwed = true;
12✔
958

959
                        $spreadsheet->getActiveSheet()
12✔
960
                            ->getComment($columnID . $rowID)
12✔
961
                            ->getText()
12✔
962
                            ->createText(
12✔
963
                                $this->parseRichText($text)
12✔
964
                            );
12✔
965
                    }
966
                }
967
            }
968

969
            // Content
970

971
            /** @var DOMElement[] $paragraphs */
972
            $paragraphs = [];
100✔
973

974
            foreach ($cellData->childNodes as $item) {
100✔
975
                /** @var DOMElement $item */
976

977
                // Filter text:p elements
978
                if ($item->nodeName == 'text:p') {
100✔
979
                    $paragraphs[] = $item;
100✔
980
                }
981
            }
982

983
            if (count($paragraphs) > 0) {
100✔
984
                $dataValue = null;
100✔
985
                // Consolidate if there are multiple p records (maybe with spans as well)
986
                $dataArray = [];
100✔
987

988
                // Text can have multiple text:p and within those, multiple text:span.
989
                // text:p newlines, but text:span does not.
990
                // Also, here we assume there is no text data is span fields are specified, since
991
                // we have no way of knowing proper positioning anyway.
992

993
                foreach ($paragraphs as $pData) {
100✔
994
                    $dataArray[] = $this->scanElementForText($pData);
100✔
995
                }
996
                $allCellDataText = implode("\n", $dataArray);
100✔
997

998
                $type = $cellData->getAttributeNS($officeNs, 'value-type');
100✔
999
                $symbol = '';
100✔
1000
                $leftHandCurrency = Preg::isMatch('/\$|£|¥/', $allCellDataText, $matches);
100✔
1001
                if ($leftHandCurrency) {
100✔
1002
                    $type = str_replace('float', 'currency', $type);
8✔
1003
                    $symbol = (string) $matches[0];
8✔
1004
                }
1005
                $customFormatting = '';
100✔
1006
                if ($this->formatCallback !== null) {
100✔
1007
                    $temp = ($this->formatCallback)($type, $allCellDataText);
1✔
1008
                    if ($temp !== '') {
1✔
1009
                        $customFormatting = $temp;
1✔
1010
                    }
1011
                }
1012

1013
                switch ($type) {
1014
                    case 'string':
100✔
1015
                        $type = DataType::TYPE_STRING;
59✔
1016
                        $dataValue = $allCellDataText;
59✔
1017

1018
                        foreach ($paragraphs as $paragraph) {
59✔
1019
                            $link = $paragraph->getElementsByTagNameNS($textNs, 'a');
59✔
1020
                            if ($link->length > 0 && $link->item(0) !== null) {
59✔
1021
                                $hyperlink = $link->item(0)->getAttributeNS($xlinkNs, 'href');
8✔
1022
                            }
1023
                        }
1024

1025
                        break;
59✔
1026
                    case 'boolean':
70✔
1027
                        $type = DataType::TYPE_BOOL;
11✔
1028
                        $dataValue = ($cellData->getAttributeNS($officeNs, 'boolean-value') === 'true') ? true : false;
11✔
1029

1030
                        break;
11✔
1031
                    case 'percentage':
67✔
1032
                        if (!str_contains($allCellDataText, '.')) {
6✔
1033
                            $formatting = NumberFormat::FORMAT_PERCENTAGE;
4✔
1034
                        } elseif (substr($allCellDataText, -3, 1) === '.') {
6✔
1035
                            $formatting = NumberFormat::FORMAT_PERCENTAGE_0;
1✔
1036
                        } else {
1037
                            $formatting = NumberFormat::FORMAT_PERCENTAGE_00;
5✔
1038
                        }
1039
                        $type = DataType::TYPE_NUMERIC;
6✔
1040
                        $dataValue = (float) $cellData->getAttributeNS($officeNs, 'value');
6✔
1041

1042
                        break;
6✔
1043
                    case 'currency':
66✔
1044
                        $type = DataType::TYPE_NUMERIC;
8✔
1045
                        $dataValue = (float) $cellData->getAttributeNS($officeNs, 'value');
8✔
1046

1047
                        $currency = $cellData->getAttributeNS($officeNs, 'currency');
8✔
1048
                        if ($leftHandCurrency) {
8✔
1049
                            $typeValue = 'currency';
8✔
1050
                            $formatting = str_contains($allCellDataText, '.') ? NumberFormat::FORMAT_CURRENCY_USD : NumberFormat::FORMAT_CURRENCY_USD_INTEGER;
8✔
1051
                            if ($symbol !== '$') {
8✔
1052
                                $formatting = str_replace('$', $symbol, $formatting);
1✔
1053
                            }
1054
                        } elseif (str_contains($allCellDataText, '€')) {
5✔
1055
                            $typeValue = 'currency';
5✔
1056
                            $formatting = str_contains($allCellDataText, '.') ? NumberFormat::FORMAT_CURRENCY_EUR : NumberFormat::FORMAT_CURRENCY_EUR_INTEGER;
5✔
1057
                        }
1058

1059
                        break;
8✔
1060
                    case 'float':
60✔
1061
                        $type = DataType::TYPE_NUMERIC;
58✔
1062
                        $dataValue = (float) $cellData->getAttributeNS($officeNs, 'value');
58✔
1063

1064
                        if ($assignedNumberFormat !== '') {
58✔
1065
                            $formatting = $assignedNumberFormat;
3✔
1066
                        } elseif ($dataValue !== floor($dataValue)) {
56✔
1067
                            // do nothing
1068
                        } elseif (substr($allCellDataText, -2, 1) === '.') {
49✔
1069
                            $formatting = NumberFormat::FORMAT_NUMBER_0;
1✔
1070
                        } elseif (substr($allCellDataText, -3, 1) === '.') {
48✔
1071
                            $formatting = NumberFormat::FORMAT_NUMBER_00;
9✔
1072
                        }
1073
                        if (floor($dataValue) == $dataValue) {
58✔
1074
                            if ($dataValue == (int) $dataValue) {
51✔
1075
                                $dataValue = (int) $dataValue;
51✔
1076
                            }
1077
                        }
1078

1079
                        break;
58✔
1080
                    case 'date':
15✔
1081
                        $type = DataType::TYPE_NUMERIC;
13✔
1082
                        $value = $cellData->getAttributeNS($officeNs, 'date-value');
13✔
1083
                        $dataValue = Date::convertIsoDate($value);
13✔
1084

1085
                        if (Preg::isMatch('/^\d\d\d\d-\d\d-\d\d$/', $allCellDataText)) {
13✔
1086
                            $formatting = 'yyyy-mm-dd';
2✔
1087
                        } elseif (Preg::isMatch('/^\d\d?-[a-zA-Z]+-\d\d\d\d$/', $allCellDataText)) {
11✔
1088
                            $formatting = 'd-mmm-yyyy';
7✔
1089
                        } elseif ($dataValue != floor($dataValue)) {
11✔
1090
                            $formatting = NumberFormat::FORMAT_DATE_XLSX15
7✔
1091
                                . ' '
7✔
1092
                                . NumberFormat::FORMAT_DATE_TIME4;
7✔
1093
                        } else {
1094
                            $formatting = NumberFormat::FORMAT_DATE_XLSX15;
11✔
1095
                        }
1096

1097
                        break;
13✔
1098
                    case 'time':
10✔
1099
                        $type = DataType::TYPE_NUMERIC;
9✔
1100

1101
                        $timeValue = $cellData->getAttributeNS($officeNs, 'time-value');
9✔
1102
                        $minus = '';
9✔
1103
                        if (str_starts_with($timeValue, '-')) {
9✔
1104
                            $minus = '-';
1✔
1105
                            $timeValue = substr($timeValue, 1);
1✔
1106
                        }
1107
                        $timeArray = sscanf($timeValue, 'PT%dH%dM%dS');
9✔
1108
                        if (is_array($timeArray)) {
9✔
1109
                            /** @var array{int, int, int} $timeArray */
1110
                            $days = intdiv($timeArray[0], 24);
9✔
1111
                            $hours = $timeArray[0] % 24;
9✔
1112
                            $dt = new DateTime("1899-12-30 $hours:{$timeArray[1]}:{$timeArray[2]}", new DateTimeZone('UTC'));
9✔
1113
                            $dt->modify("+$days days");
9✔
1114
                            $dataValue = Date::PHPToExcel($dt);
9✔
1115
                            if ($minus === '-') {
9✔
1116
                                $dataValue *= -1;
1✔
1117
                                $formatting = '[hh]:mm:ss';
1✔
1118
                            } else {
1119
                                $formatting = NumberFormat::FORMAT_DATE_TIME4;
9✔
1120
                            }
1121
                        }
1122

1123
                        break;
9✔
1124
                    default:
1125
                        $dataValue = null;
1✔
1126
                }
1127
                if ($customFormatting !== '') {
100✔
1128
                    $formatting = $customFormatting;
1✔
1129
                }
1130
            } else {
1131
                $type = DataType::TYPE_NULL;
51✔
1132
                $dataValue = null;
51✔
1133
            }
1134

1135
            if ($hasCalculatedValue) {
100✔
1136
                $type = DataType::TYPE_FORMULA;
39✔
1137
                $cellDataFormula = substr($cellDataFormula, strpos($cellDataFormula, ':=') + 1);
39✔
1138
                $cellDataFormula = FormulaTranslator::convertToExcelFormulaValue($cellDataFormula);
39✔
1139
            }
1140

1141
            for ($i = 0; $i < $colRepeats; ++$i) {
100✔
1142
                if ($i > 0) {
100✔
1143
                    StringHelper::stringIncrement($columnID);
44✔
1144
                }
1145

1146
                if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) {
100✔
1147
                    continue;
2✔
1148
                }
1149

1150
                if ($type !== DataType::TYPE_NULL) {
100✔
1151
                    for ($rowAdjust = 0; $rowAdjust < $rowRepeats; ++$rowAdjust) {
100✔
1152
                        $rID = $rowID + $rowAdjust;
100✔
1153

1154
                        $cell = $spreadsheet->getActiveSheet()
100✔
1155
                            ->getCell($columnID . $rID);
100✔
1156

1157
                        // Set value
1158
                        if ($hasCalculatedValue) {
100✔
1159
                            $cell->setValueExplicit($cellDataFormula, $type);
39✔
1160
                            if ($cellDataType === 'array') {
39✔
1161
                                $cell->setFormulaAttributes(['t' => 'array', 'ref' => $cellDataRef]);
12✔
1162
                            }
1163
                        } elseif ($type !== '' || $dataValue !== null) {
88✔
1164
                            $cell->setValueExplicit($dataValue, $type);
88✔
1165
                        }
1166

1167
                        if ($hasCalculatedValue) {
100✔
1168
                            $cell->setCalculatedValue($dataValue, $type === DataType::TYPE_NUMERIC);
39✔
1169
                        }
1170

1171
                        // Set other properties
1172
                        if ($formatting !== null) {
100✔
1173
                            $spreadsheet->getActiveSheet()
27✔
1174
                                ->getStyle($columnID . $rID)
27✔
1175
                                ->getNumberFormat()
27✔
1176
                                ->setFormatCode($formatting);
27✔
1177
                        } else {
1178
                            $spreadsheet->getActiveSheet()
93✔
1179
                                ->getStyle($columnID . $rID)
93✔
1180
                                ->getNumberFormat()
93✔
1181
                                ->setFormatCode(NumberFormat::FORMAT_GENERAL);
93✔
1182
                        }
1183

1184
                        if ($hyperlink !== null) {
100✔
1185
                            if ($hyperlink[0] === '#') {
8✔
1186
                                $hyperlink = 'sheet://' . substr($hyperlink, 1);
1✔
1187
                            }
1188
                            $cell->getHyperlink()
8✔
1189
                                ->setUrl($hyperlink);
8✔
1190
                        }
1191
                    }
1192
                }
1193
            }
1194

1195
            // Merged cells
1196
            $this->processMergedCells($cellData, $tableNs, $type, $columnID, $rowID, $spreadsheet);
100✔
1197

1198
            StringHelper::stringIncrement($columnID);
100✔
1199
        }
1200
        $rowID += $rowRepeats;
100✔
1201
    }
1202

1203
    private static function extractNodeName(string $key): string
101✔
1204
    {
1205
        // Remove ns from node name
1206
        if (str_contains($key, ':')) {
101✔
1207
            $keyChunks = explode(':', $key);
101✔
1208
            $key = array_pop($keyChunks);
101✔
1209
        }
1210

1211
        return $key;
101✔
1212
    }
1213

1214
    /**
1215
     * @param string[] $columnWidths
1216
     */
1217
    private function processTableColumnHeader(
1✔
1218
        DOMElement $childNode,
1219
        string $tableNs,
1220
        array $columnWidths,
1221
        int &$tableColumnIndex,
1222
        Spreadsheet $spreadsheet,
1223
        bool $processWidths = true,
1224
        bool $processStyles = true
1225
    ): void {
1226
        foreach ($childNode->childNodes as $grandchildNode) {
1✔
1227
            /** @var DOMElement $grandchildNode */
1228
            $grandkey = self::extractNodeName($grandchildNode->nodeName);
1✔
1229
            switch ($grandkey) {
1230
                case 'table-column':
1✔
1231
                    $this->processTableColumn(
1✔
1232
                        $grandchildNode,
1✔
1233
                        $tableNs,
1✔
1234
                        $columnWidths,
1✔
1235
                        $tableColumnIndex,
1✔
1236
                        $spreadsheet,
1✔
1237
                        $processWidths,
1✔
1238
                        $processStyles
1✔
1239
                    );
1✔
1240

1241
                    break;
1✔
1242
            }
1243
        }
1244
    }
1245

1246
    /**
1247
     * @param string[] $columnWidths
1248
     */
1249
    private function processTableColumnGroup(
1✔
1250
        DOMElement $childNode,
1251
        string $tableNs,
1252
        array $columnWidths,
1253
        int &$tableColumnIndex,
1254
        Spreadsheet $spreadsheet,
1255
        bool $processWidths = true,
1256
        bool $processStyles = true
1257
    ): void {
1258
        foreach ($childNode->childNodes as $grandchildNode) {
1✔
1259
            /** @var DOMElement $grandchildNode */
1260
            $grandkey = self::extractNodeName($grandchildNode->nodeName);
1✔
1261
            switch ($grandkey) {
1262
                case 'table-column':
1✔
1263
                    $this->processTableColumn(
1✔
1264
                        $grandchildNode,
1✔
1265
                        $tableNs,
1✔
1266
                        $columnWidths,
1✔
1267
                        $tableColumnIndex,
1✔
1268
                        $spreadsheet,
1✔
1269
                        $processWidths,
1✔
1270
                        $processStyles
1✔
1271
                    );
1✔
1272

1273
                    break;
1✔
UNCOV
1274
                case 'table-header-columns':
×
UNCOV
1275
                case 'table-columns':
×
UNCOV
1276
                    $this->processTableColumnHeader(
×
UNCOV
1277
                        $grandchildNode,
×
UNCOV
1278
                        $tableNs,
×
UNCOV
1279
                        $columnWidths,
×
UNCOV
1280
                        $tableColumnIndex,
×
UNCOV
1281
                        $spreadsheet,
×
UNCOV
1282
                        $processWidths,
×
UNCOV
1283
                        $processStyles
×
UNCOV
1284
                    );
×
1285

UNCOV
1286
                    break;
×
UNCOV
1287
                case 'table-column-group':
×
UNCOV
1288
                    $this->processTableColumnGroup(
×
UNCOV
1289
                        $grandchildNode,
×
UNCOV
1290
                        $tableNs,
×
UNCOV
1291
                        $columnWidths,
×
UNCOV
1292
                        $tableColumnIndex,
×
UNCOV
1293
                        $spreadsheet,
×
UNCOV
1294
                        $processWidths,
×
UNCOV
1295
                        $processStyles
×
UNCOV
1296
                    );
×
1297

UNCOV
1298
                    break;
×
1299
            }
1300
        }
1301
    }
1302

1303
    /**
1304
     * @param string[] $columnWidths
1305
     */
1306
    private function processTableColumn(
55✔
1307
        DOMElement $childNode,
1308
        string $tableNs,
1309
        array $columnWidths,
1310
        int &$tableColumnIndex,
1311
        Spreadsheet $spreadsheet,
1312
        bool $processWidths = true,
1313
        bool $processStyles = true
1314
    ): void {
1315
        if ($childNode->hasAttributeNS($tableNs, 'number-columns-repeated')) {
55✔
1316
            $rowRepeats = (int) $childNode->getAttributeNS($tableNs, 'number-columns-repeated');
47✔
1317
        } else {
1318
            $rowRepeats = 1;
28✔
1319
        }
1320
        $tableStyleName = $childNode->getAttributeNS($tableNs, 'style-name');
55✔
1321
        if ($processWidths) {
55✔
1322
            if (isset($columnWidths[$tableStyleName])) {
53✔
1323
                $columnWidth = new HelperDimension($columnWidths[$tableStyleName]);
52✔
1324
                $tableColumnIndex2 = $tableColumnIndex;
52✔
1325
                $tableColumnString = Coordinate::stringFromColumnIndex($tableColumnIndex2);
52✔
1326
                for ($rowRepeats2 = $rowRepeats; $rowRepeats2 > 0 && $tableColumnIndex2 <= AddressRange::MAX_COLUMN_INT; --$rowRepeats2) {
52✔
1327
                    if (!$this->readEmptyCells && $tableColumnIndex2 > $this->highestDataIndex) {
52✔
1328
                        break;
3✔
1329
                    }
1330
                    $spreadsheet->getActiveSheet()
52✔
1331
                        ->getColumnDimension($tableColumnString)
52✔
1332
                        ->setWidth($columnWidth->toUnit('cm'), 'cm');
52✔
1333
                    StringHelper::stringIncrement(
52✔
1334
                        $tableColumnString
52✔
1335
                    );
52✔
1336
                    ++$tableColumnIndex2;
52✔
1337
                }
1338
            }
1339
        }
1340
        if ($processStyles) {
55✔
1341
            $defaultStyleName = $childNode->getAttributeNS($tableNs, 'default-cell-style-name');
55✔
1342
            if ($defaultStyleName !== 'Default' && isset($this->allStyles[$defaultStyleName])) {
55✔
1343
                $tableColumnIndex2 = $tableColumnIndex;
11✔
1344
                $tableColumnString = Coordinate::stringFromColumnIndex($tableColumnIndex2);
11✔
1345
                for ($rowRepeats2 = $rowRepeats; $rowRepeats2 > 0 && $tableColumnIndex2 <= AddressRange::MAX_COLUMN_INT; --$rowRepeats2) {
11✔
1346
                    $spreadsheet->getActiveSheet()
11✔
1347
                        ->getStyle($tableColumnString)
11✔
1348
                        ->applyFromArray(
11✔
1349
                            $this->allStyles[$defaultStyleName]
11✔
1350
                        );
11✔
1351
                    StringHelper::stringIncrement(
11✔
1352
                        $tableColumnString
11✔
1353
                    );
11✔
1354
                    ++$tableColumnIndex2;
11✔
1355
                }
1356
            }
1357
        }
1358
        $tableColumnIndex += $rowRepeats;
55✔
1359
    }
1360

1361
    private function processSettings(ZipArchive $zip, Spreadsheet $spreadsheet): void
94✔
1362
    {
1363
        $dom = new DOMDocument('1.01', 'UTF-8');
94✔
1364
        $dom->loadXML(
94✔
1365
            $this->getSecurityScannerOrThrow()
94✔
1366
                ->scan($zip->getFromName('settings.xml'))
94✔
1367
        );
94✔
1368
        $configNs = (string) $dom->lookupNamespaceUri('config');
94✔
1369
        $officeNs = (string) $dom->lookupNamespaceUri('office');
94✔
1370
        $settings = $dom->getElementsByTagNameNS($officeNs, 'settings')
94✔
1371
            ->item(0);
94✔
1372
        if ($settings !== null) {
94✔
1373
            $this->lookForActiveSheet($settings, $spreadsheet, $configNs);
94✔
1374
            $this->lookForSelectedCells($settings, $spreadsheet, $configNs);
94✔
1375
        }
1376
    }
1377

1378
    private function lookForActiveSheet(DOMElement $settings, Spreadsheet $spreadsheet, string $configNs): void
94✔
1379
    {
1380
        /** @var DOMElement $t */
1381
        foreach ($settings->getElementsByTagNameNS($configNs, 'config-item') as $t) {
94✔
1382
            if ($t->getAttributeNs($configNs, 'name') === 'ActiveTable') {
93✔
1383
                try {
1384
                    $spreadsheet->setActiveSheetIndexByName($t->nodeValue ?? '');
93✔
1385
                } catch (Throwable) {
2✔
1386
                    // do nothing
1387
                }
1388

1389
                break;
93✔
1390
            }
1391
        }
1392
    }
1393

1394
    private function lookForSelectedCells(DOMElement $settings, Spreadsheet $spreadsheet, string $configNs): void
94✔
1395
    {
1396
        /** @var DOMElement $t */
1397
        foreach ($settings->getElementsByTagNameNS($configNs, 'config-item-map-named') as $t) {
94✔
1398
            if ($t->getAttributeNs($configNs, 'name') === 'Tables') {
93✔
1399
                foreach ($t->getElementsByTagNameNS($configNs, 'config-item-map-entry') as $ws) {
93✔
1400
                    $setRow = $setCol = '';
93✔
1401
                    $wsname = $ws->getAttributeNs($configNs, 'name');
93✔
1402
                    foreach ($ws->getElementsByTagNameNS($configNs, 'config-item') as $configItem) {
93✔
1403
                        $attrName = $configItem->getAttributeNs($configNs, 'name');
93✔
1404
                        if ($attrName === 'CursorPositionX') {
93✔
1405
                            $setCol = $configItem->nodeValue;
93✔
1406
                        }
1407
                        if ($attrName === 'CursorPositionY') {
93✔
1408
                            $setRow = $configItem->nodeValue;
93✔
1409
                        }
1410
                    }
1411
                    $this->setSelected($spreadsheet, $wsname, "$setCol", "$setRow");
93✔
1412
                }
1413

1414
                break;
93✔
1415
            }
1416
        }
1417
    }
1418

1419
    private function setSelected(Spreadsheet $spreadsheet, string $wsname, string $setCol, string $setRow): void
93✔
1420
    {
1421
        if (is_numeric($setCol) && is_numeric($setRow)) {
93✔
1422
            $sheet = $spreadsheet->getSheetByName($wsname);
93✔
1423
            if ($sheet !== null) {
93✔
1424
                $sheet->setSelectedCells([(int) $setCol + 1, (int) $setRow + 1]);
92✔
1425
            }
1426
        }
1427
    }
1428

1429
    /**
1430
     * Recursively scan element.
1431
     */
1432
    protected function scanElementForText(DOMNode $element): string
100✔
1433
    {
1434
        $str = '';
100✔
1435
        foreach ($element->childNodes as $child) {
100✔
1436
            /** @var DOMNode $child */
1437
            if ($child->nodeType == XML_TEXT_NODE) {
100✔
1438
                $str .= $child->nodeValue;
100✔
1439
            } elseif ($child->nodeType == XML_ELEMENT_NODE && $child->nodeName == 'text:line-break') {
19✔
1440
                $str .= "\n";
1✔
1441
            } elseif ($child->nodeType == XML_ELEMENT_NODE && $child->nodeName == 'text:s') {
19✔
1442
                // It's a space
1443

1444
                // Multiple spaces?
1445
                $attributes = $child->attributes;
6✔
1446
                /** @var ?DOMAttr $cAttr */
1447
                $cAttr = ($attributes === null) ? null : $attributes->getNamedItem('c');
6✔
1448
                $multiplier = self::getMultiplier($cAttr);
6✔
1449
                $str .= str_repeat(' ', $multiplier);
6✔
1450
            }
1451

1452
            if ($child->hasChildNodes()) {
100✔
1453
                $str .= $this->scanElementForText($child);
17✔
1454
            }
1455
        }
1456

1457
        return $str;
100✔
1458
    }
1459

1460
    private static function getMultiplier(?DOMAttr $cAttr): int
6✔
1461
    {
1462
        if ($cAttr) {
6✔
1463
            $multiplier = (int) $cAttr->nodeValue;
6✔
1464
        } else {
1465
            $multiplier = 1;
6✔
1466
        }
1467

1468
        return $multiplier;
6✔
1469
    }
1470

1471
    private function parseRichText(string $is): RichText
12✔
1472
    {
1473
        $value = new RichText();
12✔
1474
        $value->createText($is);
12✔
1475

1476
        return $value;
12✔
1477
    }
1478

1479
    private function processMergedCells(
100✔
1480
        DOMElement $cellData,
1481
        string $tableNs,
1482
        string $type,
1483
        string $columnID,
1484
        int $rowID,
1485
        Spreadsheet $spreadsheet
1486
    ): void {
1487
        if (
1488
            $cellData->hasAttributeNS($tableNs, 'number-columns-spanned')
100✔
1489
            || $cellData->hasAttributeNS($tableNs, 'number-rows-spanned')
100✔
1490
        ) {
1491
            if (($type !== DataType::TYPE_NULL) || ($this->readDataOnly === false)) {
19✔
1492
                $columnTo = $columnID;
19✔
1493

1494
                if ($cellData->hasAttributeNS($tableNs, 'number-columns-spanned')) {
19✔
1495
                    $columnIndex = Coordinate::columnIndexFromString($columnID);
18✔
1496
                    $columnIndex += (int) $cellData->getAttributeNS($tableNs, 'number-columns-spanned');
18✔
1497
                    $columnIndex -= 2;
18✔
1498

1499
                    $columnTo = Coordinate::stringFromColumnIndex($columnIndex + 1);
18✔
1500
                }
1501

1502
                $rowTo = $rowID;
19✔
1503

1504
                if ($cellData->hasAttributeNS($tableNs, 'number-rows-spanned')) {
19✔
1505
                    $rowTo = $rowTo + (int) $cellData->getAttributeNS($tableNs, 'number-rows-spanned') - 1;
19✔
1506
                }
1507

1508
                $cellRange = $columnID . $rowID . ':' . $columnTo . $rowTo;
19✔
1509
                $spreadsheet->getActiveSheet()->mergeCells($cellRange, Worksheet::MERGE_CELL_CONTENT_HIDE);
19✔
1510
            }
1511
        }
1512
    }
1513

1514
    /** @var null|Closure(string, string):string */
1515
    private ?Closure $formatCallback = null;
1516

1517
    /** @param Closure(string, string):string $formatCallback */
1518
    public function setFormatCallback(Closure $formatCallback): void
1✔
1519
    {
1520
        $this->formatCallback = $formatCallback;
1✔
1521
    }
1522

1523
    /** @return array{
1524
     *   autoColor?: true,
1525
     *   bold?: true,
1526
     *   color?: array{rgb: string},
1527
     *   italic?: true,
1528
     *   name?: non-empty-string,
1529
     *   size?: float|int,
1530
     *   strikethrough?: true,
1531
     *   underline?: 'double'|'single',
1532
     * }
1533
     */
1534
    protected function getFontStyles(DOMElement $textProperty, string $styleNs, string $fontNs): array
101✔
1535
    {
1536
        $fonts = [];
101✔
1537
        $temp = $textProperty->getAttributeNs($styleNs, 'font-name') ?: $textProperty->getAttributeNs($fontNs, 'font-family');
101✔
1538
        if ($temp !== '') {
101✔
1539
            $fonts['name'] = $temp;
101✔
1540
        }
1541
        $temp = $textProperty->getAttributeNs($fontNs, 'font-size');
101✔
1542
        if ($temp !== '' && str_ends_with($temp, 'pt')) {
101✔
1543
            $fonts['size'] = (float) substr($temp, 0, -2);
101✔
1544
        }
1545
        $temp = $textProperty->getAttributeNs($fontNs, 'font-style');
101✔
1546
        if ($temp === 'italic') {
101✔
1547
            $fonts['italic'] = true;
48✔
1548
        }
1549
        $temp = $textProperty->getAttributeNs($fontNs, 'font-weight');
101✔
1550
        if ($temp === 'bold') {
101✔
1551
            $fonts['bold'] = true;
50✔
1552
        }
1553
        $temp = $textProperty->getAttributeNs($fontNs, 'color');
101✔
1554
        if (Preg::isMatch('/^#[a-f0-9]{6}$/i', $temp)) {
101✔
1555
            $fonts['color'] = ['rgb' => substr($temp, 1)];
92✔
1556
        }
1557
        $temp = $textProperty->getAttributeNs($styleNs, 'use-window-font-color');
101✔
1558
        if ($temp === 'true') {
101✔
1559
            $fonts['autoColor'] = true;
3✔
1560
        }
1561
        $temp = $textProperty->getAttributeNs($styleNs, 'text-underline-type');
101✔
1562
        if ($temp === '') {
101✔
1563
            $temp = $textProperty->getAttributeNs($styleNs, 'text-underline-style');
101✔
1564
            if ($temp !== '' && $temp !== 'none') {
101✔
1565
                $temp = 'single';
45✔
1566
            }
1567
        }
1568
        if ($temp === 'single' || $temp === 'double') {
101✔
1569
            $fonts['underline'] = $temp;
46✔
1570
        }
1571
        $temp = $textProperty->getAttributeNs($styleNs, 'text-line-through-type');
101✔
1572
        if ($temp !== '' && $temp !== 'none') {
101✔
1573
            $fonts['strikethrough'] = true;
10✔
1574
        }
1575

1576
        return $fonts;
101✔
1577
    }
1578

1579
    /** @return array{
1580
     *   fillType?: string,
1581
     *   startColor?: array{rgb: string},
1582
     * }
1583
     */
1584
    protected function getFillStyles(DOMElement $tableCellProperties, string $fontNs): array
101✔
1585
    {
1586
        $fills = [];
101✔
1587
        $temp = $tableCellProperties->getAttributeNs($fontNs, 'background-color');
101✔
1588
        if (Preg::isMatch('/^#[a-f0-9]{6}$/i', $temp)) {
101✔
1589
            $fills['fillType'] = Fill::FILL_SOLID;
41✔
1590
            $fills['startColor'] = ['rgb' => substr($temp, 1)];
41✔
1591
        } elseif ($temp === 'transparent') {
92✔
1592
            $fills['fillType'] = Fill::FILL_NONE;
65✔
1593
        }
1594

1595
        return $fills;
101✔
1596
    }
1597

1598
    private const MAP_VERTICAL = [
1599
        'top' => Alignment::VERTICAL_TOP,
1600
        'middle' => Alignment::VERTICAL_CENTER,
1601
        'automatic' => Alignment::VERTICAL_JUSTIFY,
1602
        'bottom' => Alignment::VERTICAL_BOTTOM,
1603
    ];
1604
    private const MAP_HORIZONTAL = [
1605
        'center' => Alignment::HORIZONTAL_CENTER,
1606
        'end' => Alignment::HORIZONTAL_RIGHT,
1607
        'justify' => Alignment::HORIZONTAL_FILL,
1608
        'start' => Alignment::HORIZONTAL_LEFT,
1609
    ];
1610

1611
    /** @return array{
1612
     *   shrinkToFit?: bool,
1613
     *   textRotation?: int,
1614
     *   vertical?: string,
1615
     *   wrapText?: bool,
1616
     * }
1617
     */
1618
    protected function getAlignment1Styles(DOMElement $tableCellProperties, string $styleNs, string $fontNs): array
76✔
1619
    {
1620
        $alignment1 = [];
76✔
1621
        $temp = $tableCellProperties->getAttributeNs($styleNs, 'rotation-angle');
76✔
1622
        if (is_numeric($temp)) {
76✔
1623
            $temp2 = (int) $temp;
58✔
1624
            if ($temp2 > 90) {
58✔
1625
                $temp2 -= 360;
9✔
1626
            }
1627
            if ($temp2 >= -90 && $temp2 <= 90) {
58✔
1628
                $alignment1['textRotation'] = (int) $temp2;
58✔
1629
            }
1630
        }
1631
        $temp = $tableCellProperties->getAttributeNs($styleNs, 'vertical-align');
76✔
1632
        $temp2 = self::MAP_VERTICAL[$temp] ?? '';
76✔
1633
        if ($temp2 !== '') {
76✔
1634
            $alignment1['vertical'] = $temp2;
70✔
1635
        }
1636
        $temp = $tableCellProperties->getAttributeNs($fontNs, 'wrap-option');
76✔
1637
        if ($temp === 'wrap') {
76✔
1638
            $alignment1['wrapText'] = true;
12✔
1639
        } elseif ($temp === 'no-wrap') {
75✔
1640
            $alignment1['wrapText'] = false;
9✔
1641
        }
1642
        $temp = $tableCellProperties->getAttributeNs($styleNs, 'shrink-to-fit');
76✔
1643
        if ($temp === 'true' || $temp === 'false') {
76✔
1644
            $alignment1['shrinkToFit'] = $temp === 'true';
10✔
1645
        }
1646

1647
        return $alignment1;
76✔
1648
    }
1649

1650
    /** @return array{
1651
     *   horizontal?: string,
1652
     *   readOrder?: int,
1653
     * }
1654
     */
1655
    protected function getAlignment2Styles(DOMElement $paragraphProperties, string $styleNs, string $fontNs): array
23✔
1656
    {
1657
        $alignment2 = [];
23✔
1658
        $temp = $paragraphProperties->getAttributeNs($fontNs, 'text-align');
23✔
1659
        $temp2 = self::MAP_HORIZONTAL[$temp] ?? '';
23✔
1660
        if ($temp2 !== '') {
23✔
1661
            $alignment2['horizontal'] = $temp2;
23✔
1662
        }
1663
        $temp = $paragraphProperties->getAttributeNs($fontNs, 'margin-left') ?: $paragraphProperties->getAttributeNs($fontNs, 'margin-right');
23✔
1664
        if (Preg::isMatch('/^\d+([.]\d+)?(cm|in|mm|pt)$/', $temp)) {
23✔
1665
            $dimension = new HelperDimension($temp);
14✔
1666
            $alignment2['indent'] = (int) round($dimension->toUnit('px') / Alignment::INDENT_UNITS_TO_PIXELS);
14✔
1667
        }
1668

1669
        $temp = $paragraphProperties->getAttributeNs($styleNs, 'writing-mode');
23✔
1670
        if ($temp === 'rl-tb') {
23✔
1671
            $alignment2['readOrder'] = Alignment::READORDER_RTL;
1✔
1672
        } elseif ($temp === 'lr-tb') {
23✔
1673
            $alignment2['readOrder'] = Alignment::READORDER_LTR;
1✔
1674
        }
1675

1676
        return $alignment2;
23✔
1677
    }
1678

1679
    /** @return array{
1680
     *   locked?: string,
1681
     *   hidden?: string,
1682
     * }
1683
     */
1684
    protected function getProtectionStyles(DOMElement $tableCellProperties, string $styleNs): array
76✔
1685
    {
1686
        $protection = [];
76✔
1687
        $temp = $tableCellProperties->getAttributeNs($styleNs, 'cell-protect');
76✔
1688
        switch ($temp) {
1689
            case 'protected formula-hidden':
76✔
1690
                $protection['locked'] = Protection::PROTECTION_PROTECTED;
1✔
1691
                $protection['hidden'] = Protection::PROTECTION_PROTECTED;
1✔
1692

1693
                break;
1✔
1694
            case 'formula-hidden':
76✔
1695
                $protection['locked'] = Protection::PROTECTION_UNPROTECTED;
1✔
1696
                $protection['hidden'] = Protection::PROTECTION_PROTECTED;
1✔
1697

1698
                break;
1✔
1699
            case 'protected':
76✔
1700
                $protection['locked'] = Protection::PROTECTION_PROTECTED;
10✔
1701
                $protection['hidden'] = Protection::PROTECTION_UNPROTECTED;
10✔
1702

1703
                break;
10✔
1704
            case 'none':
76✔
1705
                $protection['locked'] = Protection::PROTECTION_UNPROTECTED;
1✔
1706
                $protection['hidden'] = Protection::PROTECTION_UNPROTECTED;
1✔
1707

1708
                break;
1✔
1709
        }
1710

1711
        return $protection;
76✔
1712
    }
1713

1714
    private const MAP_BORDER_STYLE = [ // default BORDER_THIN
1715
        'none' => Border::BORDER_NONE,
1716
        'hidden' => Border::BORDER_NONE,
1717
        'dotted' => Border::BORDER_DOTTED,
1718
        'dash-dot' => Border::BORDER_DASHDOT,
1719
        'dash-dot-dot' => Border::BORDER_DASHDOTDOT,
1720
        'dashed' => Border::BORDER_DASHED,
1721
        'double' => Border::BORDER_DOUBLE,
1722
    ];
1723

1724
    private const MAP_BORDER_MEDIUM = [
1725
        Border::BORDER_THIN => Border::BORDER_MEDIUM,
1726
        Border::BORDER_DASHDOT => Border::BORDER_MEDIUMDASHDOT,
1727
        Border::BORDER_DASHDOTDOT => Border::BORDER_MEDIUMDASHDOTDOT,
1728
        Border::BORDER_DASHED => Border::BORDER_MEDIUMDASHED,
1729
    ];
1730

1731
    private const MAP_BORDER_THICK = [
1732
        Border::BORDER_THIN => Border::BORDER_THICK,
1733
        Border::BORDER_DASHDOT => Border::BORDER_MEDIUMDASHDOT,
1734
        Border::BORDER_DASHDOTDOT => Border::BORDER_MEDIUMDASHDOTDOT,
1735
        Border::BORDER_DASHED => Border::BORDER_MEDIUMDASHED,
1736
    ];
1737

1738
    /** @return array{
1739
     *   bottom?: array{borderStyle:string, color:array{rgb: string}},
1740
     *   top?: array{borderStyle:string, color:array{rgb: string}},
1741
     *   left?: array{borderStyle:string, color:array{rgb: string}},
1742
     *   right?: array{borderStyle:string, color:array{rgb: string}},
1743
     *   diagonal?: array{borderStyle:string, color:array{rgb: string}},
1744
     *   diagonalDirection?: int,
1745
     * }
1746
     */
1747
    protected function getBorderStyles(DOMElement $tableCellProperties, string $fontNs, string $styleNs): array
76✔
1748
    {
1749
        $borders = [];
76✔
1750
        $temp = $tableCellProperties->getAttributeNs($fontNs, 'border');
76✔
1751
        $diagonalIndex = Borders::DIAGONAL_NONE;
76✔
1752
        foreach (['bottom', 'left', 'right', 'top', 'diagonal-tl-br', 'diagonal-bl-tr'] as $direction) {
76✔
1753
            if (str_starts_with($direction, 'diagonal')) {
76✔
1754
                $directionIndex = 'diagonal';
76✔
1755
                $temp = $tableCellProperties->getAttributeNs($styleNs, $direction);
76✔
1756
            } else {
1757
                $directionIndex = $direction;
76✔
1758
                $temp = $tableCellProperties->getAttributeNs($fontNs, "border-$direction");
76✔
1759
            }
1760
            if (Preg::isMatch('/^(\d+(?:[.]\d+)?)pt\s+([-\w]+)\s+#([0-9a-fA-F]{6})$/', $temp, $matches)) {
76✔
1761
                $style = self::MAP_BORDER_STYLE[$matches[2]] ?? Border::BORDER_THIN;
12✔
1762
                $width = (float) $matches[1];
12✔
1763
                if ($width >= 2.5) {
12✔
1764
                    $style = self::MAP_BORDER_THICK[$style] ?? $style;
12✔
1765
                } elseif ($width >= 1.75) {
10✔
1766
                    $style = self::MAP_BORDER_MEDIUM[$style] ?? $style;
10✔
1767
                }
1768
                $color = $matches[3];
12✔
1769
                $borders[$directionIndex] = ['borderStyle' => $style, 'color' => ['rgb' => $matches[3]]];
12✔
1770
                if ($direction === 'diagonal-tl-br') {
12✔
1771
                    $diagonalIndex = Borders::DIAGONAL_DOWN;
10✔
1772
                } elseif ($direction === 'diagonal-bl-tr') {
12✔
1773
                    $diagonalIndex = ($diagonalIndex === Borders::DIAGONAL_NONE) ? Borders::DIAGONAL_UP : Borders::DIAGONAL_BOTH;
10✔
1774
                }
1775
            }
1776
        }
1777
        if ($diagonalIndex !== Borders::DIAGONAL_NONE) {
76✔
1778
            $borders['diagonalDirection'] = $diagonalIndex;
10✔
1779
        }
1780

1781
        return $borders; // @phpstan-ignore-line
76✔
1782
    }
1783

1784
    protected function processSomeNumberFormats(?DOMElement $automaticStyle0, string $numberNs, string $styleNs): void
104✔
1785
    {
1786
        $automaticStyles = ($automaticStyle0 === null) ? [] : $automaticStyle0->getElementsByTagNameNS($numberNs, 'number-style');
104✔
1787
        foreach ($automaticStyles as $automaticStyle) {
104✔
1788
            $this->processNumberNumber($automaticStyle, $numberNs, $styleNs);
55✔
1789
        }
1790
    }
1791

1792
    protected function processNumberNumber(DOMElement $automaticStyle, string $numberNs, string $styleNs): void
55✔
1793
    {
1794
        $styleName = $automaticStyle->getAttributeNS($styleNs, 'name');
55✔
1795
        foreach ($automaticStyle->getElementsByTagNameNS($numberNs, 'number') as $numberNumber) {
55✔
1796
            $decimalPlaces = $numberNumber->getAttributeNs($numberNs, 'decimal-places');
55✔
1797
            $minIntegerDigits = (int) $numberNumber->getAttributeNs($numberNs, 'min-integer-digits');
55✔
1798
            if ($decimalPlaces === '0' && $minIntegerDigits > 1) {
55✔
1799
                $this->numberFormats[$styleName] = str_repeat('0', $minIntegerDigits);
6✔
1800
            }
1801
        }
1802
    }
1803
}
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