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

PHPOffice / PhpSpreadsheet / 22501992877

27 Feb 2026 08:04PM UTC coverage: 96.496% (+0.08%) from 96.416%
22501992877

push

github

web-flow
Merge pull request #4815 from oleibman/odsstyles4b

Ods Reader Style Support (Last) - Column Styles (Partial)

117 of 124 new or added lines in 1 file covered. (94.35%)

2 existing lines in 1 file now uncovered.

47610 of 49339 relevant lines covered (96.5%)

381.63 hits per line

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

95.1
/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()
136✔
48
    {
49
        parent::__construct();
136✔
50
        $this->securityScanner = XmlScanner::getInstance($this);
136✔
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);
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
100✔
253
    {
254
        $spreadsheet = $this->newSpreadsheet();
100✔
255
        $spreadsheet->setValueBinder($this->valueBinder);
100✔
256
        $spreadsheet->removeSheetByIndex(0);
100✔
257

258
        // Load into this instance
259
        return $this->loadIntoExisting($filename, $spreadsheet);
100✔
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
     *  }>
299
     */
300
    private array $allStyles;
301

302
    private int $highestDataIndex;
303

304
    /**
305
     * Loads PhpSpreadsheet from file into PhpSpreadsheet instance.
306
     */
307
    public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Spreadsheet
105✔
308
    {
309
        File::assertFile($filename, self::INITIAL_FILE);
105✔
310

311
        $zip = new ZipArchive();
101✔
312
        $zip->open($filename);
101✔
313

314
        // Meta
315

316
        $xml = @simplexml_load_string(
101✔
317
            $this->getSecurityScannerOrThrow()
101✔
318
                ->scan($zip->getFromName('meta.xml'))
101✔
319
        );
101✔
320
        if ($xml === false) {
101✔
321
            throw new Exception('Unable to read data from {$pFilename}');
1✔
322
        }
323

324
        /** @var array{meta?: string, office?: string, dc?: string} */
325
        $namespacesMeta = $xml->getNamespaces(true);
100✔
326

327
        (new DocumentProperties($spreadsheet))->load($xml, $namespacesMeta);
100✔
328

329
        // Styles
330

331
        $this->allStyles = [];
100✔
332
        $dom = new DOMDocument('1.01', 'UTF-8');
100✔
333
        $dom->loadXML(
100✔
334
            $this->getSecurityScannerOrThrow()
100✔
335
                ->scan($zip->getFromName('styles.xml'))
100✔
336
        );
100✔
337
        $officeNs = (string) $dom->lookupNamespaceUri('office');
100✔
338
        $styleNs = (string) $dom->lookupNamespaceUri('style');
100✔
339
        $fontNs = (string) $dom->lookupNamespaceUri('fo');
100✔
340

341
        $automaticStyle0 = $this->readDataOnly ? null : $dom->getElementsByTagNameNS($officeNs, 'styles')->item(0);
100✔
342
        $automaticStyles = ($automaticStyle0 === null) ? [] : $automaticStyle0->getElementsByTagNameNS($styleNs, 'default-style');
100✔
343
        foreach ($automaticStyles as $automaticStyle) {
100✔
344
            $styleFamily = $automaticStyle->getAttributeNS($styleNs, 'family');
92✔
345
            if ($styleFamily === 'table-cell') {
92✔
346
                $fonts = [];
87✔
347
                foreach ($automaticStyle->getElementsByTagNameNS($styleNs, 'text-properties') as $textProperty) {
87✔
348
                    $fonts = $this->getFontStyles($textProperty, $styleNs, $fontNs);
87✔
349
                }
350
                if (!empty($fonts)) {
87✔
351
                    $spreadsheet->getDefaultStyle()
87✔
352
                        ->getFont()
87✔
353
                        ->applyFromArray($fonts);
87✔
354
                }
355
            }
356
        }
357
        $automaticStyles = ($automaticStyle0 === null) ? [] : $automaticStyle0->getElementsByTagNameNS($styleNs, 'style');
100✔
358
        foreach ($automaticStyles as $automaticStyle) {
100✔
359
            $styleName = $automaticStyle->getAttributeNS($styleNs, 'name');
97✔
360
            $styleFamily = $automaticStyle->getAttributeNS($styleNs, 'family');
97✔
361
            if ($styleFamily === 'table-cell') {
97✔
362
                $fills = $fonts = [];
97✔
363
                foreach ($automaticStyle->getElementsByTagNameNS($styleNs, 'text-properties') as $textProperty) {
97✔
364
                    $fonts = $this->getFontStyles($textProperty, $styleNs, $fontNs);
50✔
365
                }
366
                foreach ($automaticStyle->getElementsByTagNameNS($styleNs, 'table-cell-properties') as $tableCellProperty) {
97✔
367
                    $fills = $this->getFillStyles($tableCellProperty, $fontNs);
97✔
368
                }
369
                if ($styleName !== '') {
97✔
370
                    if (!empty($fonts)) {
97✔
371
                        $this->allStyles[$styleName]['font'] = $fonts;
50✔
372
                        if ($styleName === 'Default') {
50✔
373
                            $spreadsheet->getDefaultStyle()
14✔
374
                                ->getFont()
14✔
375
                                ->applyFromArray($fonts);
14✔
376
                        }
377
                    }
378
                    if (!empty($fills)) {
97✔
379
                        $this->allStyles[$styleName]['fill'] = $fills;
89✔
380
                        if ($styleName === 'Default') {
89✔
381
                            $spreadsheet->getDefaultStyle()
56✔
382
                                ->getFill()
56✔
383
                                ->applyFromArray($fills);
56✔
384
                        }
385
                    }
386
                }
387
            }
388
        }
389

390
        $pageSettings = new PageSettings($dom);
100✔
391

392
        // Main Content
393

394
        $dom = new DOMDocument('1.01', 'UTF-8');
100✔
395
        $dom->loadXML(
100✔
396
            $this->getSecurityScannerOrThrow()
100✔
397
                ->scan($zip->getFromName(self::INITIAL_FILE))
100✔
398
        );
100✔
399

400
        $tableNs = (string) $dom->lookupNamespaceUri('table');
100✔
401
        $textNs = (string) $dom->lookupNamespaceUri('text');
100✔
402
        $xlinkNs = (string) $dom->lookupNamespaceUri('xlink');
100✔
403

404
        $pageSettings->readStyleCrossReferences($dom);
100✔
405

406
        $autoFilterReader = new AutoFilter($spreadsheet, $tableNs);
100✔
407
        $definedNameReader = new DefinedNames($spreadsheet, $tableNs);
100✔
408
        $columnWidths = [];
100✔
409
        $automaticStyle0 = $this->readDataOnly ? null : $dom->getElementsByTagNameNS($officeNs, 'automatic-styles')->item(0);
100✔
410
        $automaticStyles = ($automaticStyle0 === null) ? [] : $automaticStyle0->getElementsByTagNameNS($styleNs, 'style');
100✔
411
        foreach ($automaticStyles as $automaticStyle) {
100✔
412
            $styleName = $automaticStyle->getAttributeNS($styleNs, 'name');
97✔
413
            $styleFamily = $automaticStyle->getAttributeNS($styleNs, 'family');
97✔
414
            if ($styleFamily === 'table-column') {
97✔
415
                $tcprops = $automaticStyle->getElementsByTagNameNS($styleNs, 'table-column-properties');
52✔
416
                $tcprop = $tcprops->item(0);
52✔
417
                if ($tcprop !== null) {
52✔
418
                    $columnWidth = $tcprop->getAttributeNs($styleNs, 'column-width');
52✔
419
                    $columnWidths[$styleName] = $columnWidth;
52✔
420
                }
421
            }
422
            if ($styleFamily === 'table-cell') {
97✔
423
                $fonts = $fills = $alignment1 = $alignment2 = $protection = $borders = [];
82✔
424
                foreach ($automaticStyle->getElementsByTagNameNS($styleNs, 'text-properties') as $textProperty) {
82✔
425
                    $fonts = $this->getFontStyles($textProperty, $styleNs, $fontNs);
62✔
426
                }
427
                foreach ($automaticStyle->getElementsByTagNameNS($styleNs, 'table-cell-properties') as $tableCellProperty) {
82✔
428
                    $fills = $this->getFillStyles($tableCellProperty, $fontNs);
74✔
429
                    $borders = $this->getBorderStyles($tableCellProperty, $fontNs, $styleNs);
74✔
430
                    $protection = $this->getProtectionStyles($tableCellProperty, $styleNs);
74✔
431
                }
432
                foreach ($automaticStyle->getElementsByTagNameNS($styleNs, 'table-cell-properties') as $tableCellProperty) {
82✔
433
                    $alignment1 = $this->getAlignment1Styles($tableCellProperty, $styleNs, $fontNs);
74✔
434
                }
435
                foreach ($automaticStyle->getElementsByTagNameNS($styleNs, 'paragraph-properties') as $paragraphProperty) {
82✔
436
                    $alignment2 = $this->getAlignment2Styles($paragraphProperty, $styleNs, $fontNs);
23✔
437
                }
438
                if ($styleName !== '') {
82✔
439
                    if (!empty($fonts)) {
82✔
440
                        $this->allStyles[$styleName]['font'] = $fonts;
61✔
441
                    }
442
                    if (!empty($fills)) {
82✔
443
                        $this->allStyles[$styleName]['fill'] = $fills;
58✔
444
                    }
445
                    $alignment = array_merge($alignment1, $alignment2);
82✔
446
                    if (!empty($alignment)) {
82✔
447
                        $this->allStyles[$styleName]['alignment'] = $alignment;
71✔
448
                    }
449
                    if (!empty($protection)) {
82✔
450
                        $this->allStyles[$styleName]['protection'] = $protection;
10✔
451
                    }
452
                    if (!empty($borders)) {
82✔
453
                        $this->allStyles[$styleName]['borders'] = $borders;
12✔
454
                    }
455
                }
456
            }
457
        }
458

459
        // Content
460
        $item0 = $dom->getElementsByTagNameNS($officeNs, 'body')->item(0);
100✔
461
        $spreadsheets = ($item0 === null) ? [] : $item0->getElementsByTagNameNS($officeNs, 'spreadsheet');
100✔
462

463
        foreach ($spreadsheets as $workbookData) {
100✔
464
            /** @var DOMElement $workbookData */
465
            $tables = $workbookData->getElementsByTagNameNS($tableNs, 'table');
100✔
466

467
            $worksheetID = 0;
100✔
468
            $sheetCreated = false;
100✔
469
            foreach ($tables as $worksheetDataSet) {
100✔
470
                /** @var DOMElement $worksheetDataSet */
471
                $worksheetName = $worksheetDataSet->getAttributeNS($tableNs, 'name');
100✔
472

473
                // Check loadSheetsOnly
474
                if (
475
                    $this->loadSheetsOnly !== null
100✔
476
                    && $worksheetName
477
                    && !in_array($worksheetName, $this->loadSheetsOnly)
100✔
478
                ) {
479
                    continue;
5✔
480
                }
481

482
                $worksheetStyleName = $worksheetDataSet->getAttributeNS($tableNs, 'style-name');
97✔
483

484
                // Create sheet
485
                $spreadsheet->createSheet();
97✔
486
                $sheetCreated = true;
97✔
487
                $spreadsheet->setActiveSheetIndex($worksheetID);
97✔
488

489
                if ($worksheetName || is_numeric($worksheetName)) {
97✔
490
                    // Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in
491
                    // formula cells... during the load, all formulae should be correct, and we're simply
492
                    // bringing the worksheet name in line with the formula, not the reverse
493
                    $spreadsheet->getActiveSheet()
97✔
494
                        ->setTitle((string) $worksheetName, false, false);
97✔
495
                }
496

497
                // Go through every child of table element
498
                $rowID = 1;
97✔
499
                $tableColumnIndex = 1;
97✔
500
                $this->highestDataIndex = AddressRange::MAX_COLUMN_INT;
97✔
501
                foreach ($worksheetDataSet->childNodes as $childNode) {
97✔
502
                    /** @var DOMElement $childNode */
503

504
                    // Filter elements which are not under the "table" ns
505
                    if ($childNode->namespaceURI != $tableNs) {
97✔
506
                        continue;
61✔
507
                    }
508

509
                    $key = self::extractNodeName($childNode->nodeName);
97✔
510

511
                    switch ($key) {
512
                        case 'table-header-rows':
97✔
513
                        case 'table-rows':
97✔
514
                            $this->processTableHeaderRows(
1✔
515
                                $childNode,
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-row-group':
97✔
527
                            $this->processTableRowGroup(
1✔
528
                                $childNode,
1✔
529
                                $tableNs,
1✔
530
                                $rowID,
1✔
531
                                $worksheetName,
1✔
532
                                $officeNs,
1✔
533
                                $textNs,
1✔
534
                                $xlinkNs,
1✔
535
                                $spreadsheet
1✔
536
                            );
1✔
537

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

551
                            break;
96✔
552
                        case 'table-header-columns':
52✔
553
                        case 'table-columns':
52✔
554
                            $this->processTableColumnHeader(
1✔
555
                                $childNode,
1✔
556
                                $tableNs,
1✔
557
                                $columnWidths,
1✔
558
                                $tableColumnIndex,
1✔
559
                                $spreadsheet,
1✔
560
                                $this->readEmptyCells,
1✔
561
                                true
1✔
562
                            );
1✔
563

564
                            break;
1✔
565
                        case 'table-column-group':
52✔
566
                            $this->processTableColumnGroup(
1✔
567
                                $childNode,
1✔
568
                                $tableNs,
1✔
569
                                $columnWidths,
1✔
570
                                $tableColumnIndex,
1✔
571
                                $spreadsheet,
1✔
572
                                $this->readEmptyCells,
1✔
573
                                true
1✔
574
                            );
1✔
575

576
                            break;
1✔
577
                        case 'table-column':
51✔
578
                            $this->processTableColumn(
51✔
579
                                $childNode,
51✔
580
                                $tableNs,
51✔
581
                                $columnWidths,
51✔
582
                                $tableColumnIndex,
51✔
583
                                $spreadsheet,
51✔
584
                                $this->readEmptyCells,
51✔
585
                                true
51✔
586
                            );
51✔
587

588
                            break;
51✔
589
                    }
590
                }
591
                $pageSettings->setVisibilityForWorksheet(
97✔
592
                    $spreadsheet->getActiveSheet(),
97✔
593
                    $worksheetStyleName
97✔
594
                );
97✔
595
                $pageSettings->setPrintSettingsForWorksheet(
97✔
596
                    $spreadsheet->getActiveSheet(),
97✔
597
                    $worksheetStyleName
97✔
598
                );
97✔
599
                ++$worksheetID;
97✔
600
            }
601
            if ($this->createBlankSheetIfNoneRead && !$sheetCreated) {
100✔
602
                $spreadsheet->createSheet();
1✔
603
            }
604
        }
605

606
        foreach ($spreadsheets as $workbookData) {
100✔
607
            /** @var DOMElement $workbookData */
608
            $tables = $workbookData->getElementsByTagNameNS($tableNs, 'table');
100✔
609

610
            $worksheetID = 0;
100✔
611
            foreach ($tables as $worksheetDataSet) {
100✔
612
                /** @var DOMElement $worksheetDataSet */
613
                $worksheetName = $worksheetDataSet->getAttributeNS($tableNs, 'name');
100✔
614

615
                // Check loadSheetsOnly
616
                if (
617
                    $this->loadSheetsOnly !== null
100✔
618
                    && $worksheetName
619
                    && !in_array($worksheetName, $this->loadSheetsOnly)
100✔
620
                ) {
621
                    continue;
5✔
622
                }
623

624
                // Create sheet
625
                $spreadsheet->setActiveSheetIndex($worksheetID);
97✔
626
                $highestDataColumn = $spreadsheet->getActiveSheet()->getHighestDataColumn();
97✔
627
                $this->highestDataIndex = Coordinate::columnIndexFromString($highestDataColumn);
97✔
628

629
                // Go through every child of table element processing column widths
630
                $rowID = 1;
97✔
631
                $tableColumnIndex = 1;
97✔
632
                foreach ($worksheetDataSet->childNodes as $childNode) {
97✔
633
                    /** @var DOMElement $childNode */
634
                    if (empty($columnWidths) || $this->readEmptyCells) {
97✔
635
                        break;
94✔
636
                    }
637

638
                    // Filter elements which are not under the "table" ns
639
                    if ($childNode->namespaceURI != $tableNs) {
3✔
640
                        continue;
1✔
641
                    }
642

643
                    $key = self::extractNodeName($childNode->nodeName);
3✔
644

645
                    switch ($key) {
646
                        case 'table-header-columns':
3✔
647
                        case 'table-columns':
3✔
648
                            $this->processTableColumnHeader(
1✔
649
                                $childNode,
1✔
650
                                $tableNs,
1✔
651
                                $columnWidths,
1✔
652
                                $tableColumnIndex,
1✔
653
                                $spreadsheet,
1✔
654
                                true,
1✔
655
                                false
1✔
656
                            );
1✔
657

658
                            break;
1✔
659
                        case 'table-column-group':
3✔
660
                            $this->processTableColumnGroup(
1✔
661
                                $childNode,
1✔
662
                                $tableNs,
1✔
663
                                $columnWidths,
1✔
664
                                $tableColumnIndex,
1✔
665
                                $spreadsheet,
1✔
666
                                true,
1✔
667
                                false
1✔
668
                            );
1✔
669

670
                            break;
1✔
671
                        case 'table-column':
3✔
672
                            $this->processTableColumn(
2✔
673
                                $childNode,
2✔
674
                                $tableNs,
2✔
675
                                $columnWidths,
2✔
676
                                $tableColumnIndex,
2✔
677
                                $spreadsheet,
2✔
678
                                true,
2✔
679
                                false
2✔
680
                            );
2✔
681

682
                            break;
2✔
683
                    }
684
                }
685
                ++$worksheetID;
97✔
686
            }
687

688
            $autoFilterReader->read($workbookData);
100✔
689
            $definedNameReader->read($workbookData);
100✔
690
        }
691

692
        $spreadsheet->setActiveSheetIndex(0);
100✔
693

694
        if ($zip->locateName('settings.xml') !== false) {
98✔
695
            $this->processSettings($zip, $spreadsheet);
90✔
696
        }
697

698
        // Return
699
        return $spreadsheet;
98✔
700
    }
701

702
    private function processTableHeaderRows(
1✔
703
        DOMElement $childNode,
704
        string $tableNs,
705
        int &$rowID,
706
        string $worksheetName,
707
        string $officeNs,
708
        string $textNs,
709
        string $xlinkNs,
710
        Spreadsheet $spreadsheet
711
    ): void {
712
        foreach ($childNode->childNodes as $grandchildNode) {
1✔
713
            /** @var DOMElement $grandchildNode */
714
            $grandkey = self::extractNodeName($grandchildNode->nodeName);
1✔
715
            switch ($grandkey) {
716
                case 'table-row':
1✔
717
                    $this->processTableRow(
1✔
718
                        $grandchildNode,
1✔
719
                        $tableNs,
1✔
720
                        $rowID,
1✔
721
                        $worksheetName,
1✔
722
                        $officeNs,
1✔
723
                        $textNs,
1✔
724
                        $xlinkNs,
1✔
725
                        $spreadsheet
1✔
726
                    );
1✔
727

728
                    break;
1✔
729
            }
730
        }
731
    }
732

733
    private function processTableRowGroup(
1✔
734
        DOMElement $childNode,
735
        string $tableNs,
736
        int &$rowID,
737
        string $worksheetName,
738
        string $officeNs,
739
        string $textNs,
740
        string $xlinkNs,
741
        Spreadsheet $spreadsheet
742
    ): void {
743
        foreach ($childNode->childNodes as $grandchildNode) {
1✔
744
            /** @var DOMElement $grandchildNode */
745
            $grandkey = self::extractNodeName($grandchildNode->nodeName);
1✔
746
            switch ($grandkey) {
747
                case 'table-row':
1✔
748
                    $this->processTableRow(
1✔
749
                        $grandchildNode,
1✔
750
                        $tableNs,
1✔
751
                        $rowID,
1✔
752
                        $worksheetName,
1✔
753
                        $officeNs,
1✔
754
                        $textNs,
1✔
755
                        $xlinkNs,
1✔
756
                        $spreadsheet
1✔
757
                    );
1✔
758

759
                    break;
1✔
760
                case 'table-header-rows':
×
761
                case 'table-rows':
×
762
                    $this->processTableHeaderRows(
×
763
                        $grandchildNode,
×
764
                        $tableNs,
×
765
                        $rowID,
×
766
                        $worksheetName,
×
767
                        $officeNs,
×
768
                        $textNs,
×
769
                        $xlinkNs,
×
770
                        $spreadsheet
×
771
                    );
×
772

773
                    break;
×
774
                case 'table-row-group':
×
775
                    $this->processTableRowGroup(
×
776
                        $grandchildNode,
×
777
                        $tableNs,
×
778
                        $rowID,
×
779
                        $worksheetName,
×
780
                        $officeNs,
×
781
                        $textNs,
×
782
                        $xlinkNs,
×
783
                        $spreadsheet
×
784
                    );
×
785

786
                    break;
×
787
            }
788
        }
789
    }
790

791
    private function processTableRow(
96✔
792
        DOMElement $childNode,
793
        string $tableNs,
794
        int &$rowID,
795
        string $worksheetName,
796
        string $officeNs,
797
        string $textNs,
798
        string $xlinkNs,
799
        Spreadsheet $spreadsheet
800
    ): void {
801
        if ($childNode->hasAttributeNS($tableNs, 'number-rows-repeated')) {
96✔
802
            $rowRepeats = (int) $childNode->getAttributeNS($tableNs, 'number-rows-repeated');
30✔
803
        } else {
804
            $rowRepeats = 1;
96✔
805
        }
806
        $worksheet = $spreadsheet->getSheetByName($worksheetName);
96✔
807

808
        $columnID = 'A';
96✔
809
        /** @var DOMElement|DOMText $cellData */
810
        foreach ($childNode->childNodes as $cellData) {
96✔
811
            if ($cellData instanceof DOMText) {
96✔
812
                continue; // should just be whitespace
3✔
813
            }
814
            if ($cellData->hasAttributeNS($tableNs, 'number-columns-repeated')) {
96✔
815
                $colRepeats = (int) $cellData->getAttributeNS($tableNs, 'number-columns-repeated');
41✔
816
            } else {
817
                $colRepeats = 1;
96✔
818
            }
819
            $styleName = $cellData->getAttributeNS($tableNs, 'style-name');
96✔
820

821
            // When a cell has number-columns-repeated, check if ANY column in the
822
            // repeated range passes the read filter. If not, skip the entire group.
823
            // If some columns pass, we need to fall through to the processing block
824
            // which will handle per-column filtering.
825
            if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) {
96✔
826
                if ($colRepeats <= 1) {
3✔
827
                    StringHelper::stringIncrement($columnID);
3✔
828

829
                    continue;
3✔
830
                }
831

832
                // Check if any column within this repeated group passes the filter
833
                $anyColumnPasses = false;
2✔
834
                $tempCol = $columnID;
2✔
835
                for ($i = 0; $i < $colRepeats; ++$i) {
2✔
836
                    if ($i > 0) {
2✔
837
                        StringHelper::stringIncrement($tempCol);
2✔
838
                    }
839
                    if ($this->getReadFilter()->readCell($tempCol, $rowID, $worksheetName)) {
2✔
840
                        $anyColumnPasses = true;
2✔
841

842
                        break;
2✔
843
                    }
844
                }
845

846
                if (!$anyColumnPasses) {
2✔
847
                    for ($i = 0; $i < $colRepeats; ++$i) {
2✔
848
                        StringHelper::stringIncrement($columnID);
2✔
849
                    }
850

851
                    continue;
2✔
852
                }
853
                // Fall through to process the cell, with per-column filter checks
854
            }
855
            if ($worksheet !== null && ($cellData->hasChildNodes() || ($cellData->nextSibling !== null)) && isset($this->allStyles[$styleName])) {
96✔
856
                $spannedRange = "$columnID$rowID";
69✔
857
                // the following is sufficient for ods,
858
                // and does no harm for xlsx/xls.
859
                $worksheet->getStyle($spannedRange)
69✔
860
                    ->applyFromArray($this->allStyles[$styleName]);
69✔
861
                // the rest of this block is needed for xlsx/xls,
862
                // and does no harm for ods.
863
                if (isset($this->allStyles[$styleName]['borders'])) {
69✔
864
                    $spannedRows = $cellData->getAttributeNS($tableNs, 'number-columns-spanned');
10✔
865
                    $spannedColumns = $cellData->getAttributeNS($tableNs, 'number-rows-spanned');
10✔
866
                    $spannedRows = max((int) $spannedRows, 1);
10✔
867
                    $spannedColumns = max((int) $spannedColumns, 1);
10✔
868
                    if ($spannedRows > 1 || $spannedColumns > 1) {
10✔
869
                        $endRow = $rowID + $spannedRows - 1;
7✔
870
                        $endCol = $columnID;
7✔
871
                        while ($spannedColumns > 1) {
7✔
872
                            StringHelper::stringIncrement($endCol);
7✔
873
                            --$spannedColumns;
7✔
874
                        }
875
                        $spannedRange .= ":$endCol$endRow";
7✔
876
                        $worksheet->getStyle($spannedRange)
7✔
877
                            ->getBorders()
7✔
878
                            ->applyFromArray(
7✔
879
                                $this->allStyles[$styleName]['borders']
7✔
880
                            );
7✔
881
                    }
882
                }
883
            }
884

885
            // Initialize variables
886
            $formatting = $hyperlink = null;
96✔
887
            $hasCalculatedValue = false;
96✔
888
            $cellDataFormula = '';
96✔
889
            $cellDataType = '';
96✔
890
            $cellDataRef = '';
96✔
891

892
            if ($cellData->hasAttributeNS($tableNs, 'formula')) {
96✔
893
                $cellDataFormula = $cellData->getAttributeNS($tableNs, 'formula');
39✔
894
                $hasCalculatedValue = true;
39✔
895
            }
896
            if ($cellData->hasAttributeNS($tableNs, 'number-matrix-columns-spanned')) {
96✔
897
                if ($cellData->hasAttributeNS($tableNs, 'number-matrix-rows-spanned')) {
12✔
898
                    $cellDataType = 'array';
12✔
899
                    $arrayRow = (int) $cellData->getAttributeNS($tableNs, 'number-matrix-rows-spanned');
12✔
900
                    $arrayCol = (int) $cellData->getAttributeNS($tableNs, 'number-matrix-columns-spanned');
12✔
901
                    $lastRow = $rowID + $arrayRow - 1;
12✔
902
                    $lastCol = $columnID;
12✔
903
                    while ($arrayCol > 1) {
12✔
904
                        StringHelper::stringIncrement($lastCol);
7✔
905
                        --$arrayCol;
7✔
906
                    }
907
                    $cellDataRef = "$columnID$rowID:$lastCol$lastRow";
12✔
908
                }
909
            }
910

911
            // Annotations
912
            $annotation = $cellData->getElementsByTagNameNS($officeNs, 'annotation');
96✔
913

914
            if ($annotation->length > 0 && $annotation->item(0) !== null) {
96✔
915
                $textNode = $annotation->item(0)->getElementsByTagNameNS($textNs, 'p');
12✔
916
                $textNodeLength = $textNode->length;
12✔
917
                $newLineOwed = false;
12✔
918
                for ($textNodeIndex = 0; $textNodeIndex < $textNodeLength; ++$textNodeIndex) {
12✔
919
                    $textNodeItem = $textNode->item($textNodeIndex);
12✔
920
                    if ($textNodeItem !== null) {
12✔
921
                        $text = $this->scanElementForText($textNodeItem);
12✔
922
                        if ($newLineOwed) {
12✔
923
                            $spreadsheet->getActiveSheet()
1✔
924
                                ->getComment($columnID . $rowID)
1✔
925
                                ->getText()
1✔
926
                                ->createText("\n");
1✔
927
                        }
928
                        $newLineOwed = true;
12✔
929

930
                        $spreadsheet->getActiveSheet()
12✔
931
                            ->getComment($columnID . $rowID)
12✔
932
                            ->getText()
12✔
933
                            ->createText(
12✔
934
                                $this->parseRichText($text)
12✔
935
                            );
12✔
936
                    }
937
                }
938
            }
939

940
            // Content
941

942
            /** @var DOMElement[] $paragraphs */
943
            $paragraphs = [];
96✔
944

945
            foreach ($cellData->childNodes as $item) {
96✔
946
                /** @var DOMElement $item */
947

948
                // Filter text:p elements
949
                if ($item->nodeName == 'text:p') {
96✔
950
                    $paragraphs[] = $item;
96✔
951
                }
952
            }
953

954
            if (count($paragraphs) > 0) {
96✔
955
                $dataValue = null;
96✔
956
                // Consolidate if there are multiple p records (maybe with spans as well)
957
                $dataArray = [];
96✔
958

959
                // Text can have multiple text:p and within those, multiple text:span.
960
                // text:p newlines, but text:span does not.
961
                // Also, here we assume there is no text data is span fields are specified, since
962
                // we have no way of knowing proper positioning anyway.
963

964
                foreach ($paragraphs as $pData) {
96✔
965
                    $dataArray[] = $this->scanElementForText($pData);
96✔
966
                }
967
                $allCellDataText = implode("\n", $dataArray);
96✔
968

969
                $type = $cellData->getAttributeNS($officeNs, 'value-type');
96✔
970
                $symbol = '';
96✔
971
                $leftHandCurrency = Preg::isMatch('/\$|£|¥/', $allCellDataText, $matches);
96✔
972
                if ($leftHandCurrency) {
96✔
973
                    $type = str_replace('float', 'currency', $type);
8✔
974
                    $symbol = (string) $matches[0];
8✔
975
                }
976
                $customFormatting = '';
96✔
977
                if ($this->formatCallback !== null) {
96✔
978
                    $temp = ($this->formatCallback)($type, $allCellDataText);
1✔
979
                    if ($temp !== '') {
1✔
980
                        $customFormatting = $temp;
1✔
981
                    }
982
                }
983

984
                switch ($type) {
985
                    case 'string':
96✔
986
                        $type = DataType::TYPE_STRING;
58✔
987
                        $dataValue = $allCellDataText;
58✔
988

989
                        foreach ($paragraphs as $paragraph) {
58✔
990
                            $link = $paragraph->getElementsByTagNameNS($textNs, 'a');
58✔
991
                            if ($link->length > 0 && $link->item(0) !== null) {
58✔
992
                                $hyperlink = $link->item(0)->getAttributeNS($xlinkNs, 'href');
8✔
993
                            }
994
                        }
995

996
                        break;
58✔
997
                    case 'boolean':
66✔
998
                        $type = DataType::TYPE_BOOL;
11✔
999
                        $dataValue = ($cellData->getAttributeNS($officeNs, 'boolean-value') === 'true') ? true : false;
11✔
1000

1001
                        break;
11✔
1002
                    case 'percentage':
63✔
1003
                        if (!str_contains($allCellDataText, '.')) {
6✔
1004
                            $formatting = NumberFormat::FORMAT_PERCENTAGE;
4✔
1005
                        } elseif (substr($allCellDataText, -3, 1) === '.') {
6✔
1006
                            $formatting = NumberFormat::FORMAT_PERCENTAGE_0;
1✔
1007
                        } else {
1008
                            $formatting = NumberFormat::FORMAT_PERCENTAGE_00;
5✔
1009
                        }
1010
                        $type = DataType::TYPE_NUMERIC;
6✔
1011
                        $dataValue = (float) $cellData->getAttributeNS($officeNs, 'value');
6✔
1012

1013
                        break;
6✔
1014
                    case 'currency':
62✔
1015
                        $type = DataType::TYPE_NUMERIC;
8✔
1016
                        $dataValue = (float) $cellData->getAttributeNS($officeNs, 'value');
8✔
1017

1018
                        $currency = $cellData->getAttributeNS($officeNs, 'currency');
8✔
1019
                        if ($leftHandCurrency) {
8✔
1020
                            $typeValue = 'currency';
8✔
1021
                            $formatting = str_contains($allCellDataText, '.') ? NumberFormat::FORMAT_CURRENCY_USD : NumberFormat::FORMAT_CURRENCY_USD_INTEGER;
8✔
1022
                            if ($symbol !== '$') {
8✔
1023
                                $formatting = str_replace('$', $symbol, $formatting);
1✔
1024
                            }
1025
                        } elseif (str_contains($allCellDataText, '€')) {
5✔
1026
                            $typeValue = 'currency';
5✔
1027
                            $formatting = str_contains($allCellDataText, '.') ? NumberFormat::FORMAT_CURRENCY_EUR : NumberFormat::FORMAT_CURRENCY_EUR_INTEGER;
5✔
1028
                        }
1029

1030
                        break;
8✔
1031
                    case 'float':
56✔
1032
                        $type = DataType::TYPE_NUMERIC;
54✔
1033
                        $dataValue = (float) $cellData->getAttributeNS($officeNs, 'value');
54✔
1034

1035
                        if ($dataValue !== floor($dataValue)) {
54✔
1036
                            // do nothing
1037
                        } elseif (substr($allCellDataText, -2, 1) === '.') {
48✔
1038
                            $formatting = NumberFormat::FORMAT_NUMBER_0;
1✔
1039
                        } elseif (substr($allCellDataText, -3, 1) === '.') {
47✔
1040
                            $formatting = NumberFormat::FORMAT_NUMBER_00;
9✔
1041
                        }
1042
                        if (floor($dataValue) == $dataValue) {
54✔
1043
                            if ($dataValue == (int) $dataValue) {
48✔
1044
                                $dataValue = (int) $dataValue;
48✔
1045
                            }
1046
                        }
1047

1048
                        break;
54✔
1049
                    case 'date':
14✔
1050
                        $type = DataType::TYPE_NUMERIC;
12✔
1051
                        $value = $cellData->getAttributeNS($officeNs, 'date-value');
12✔
1052
                        $dataValue = Date::convertIsoDate($value);
12✔
1053

1054
                        if (Preg::isMatch('/^\d\d\d\d-\d\d-\d\d$/', $allCellDataText)) {
12✔
1055
                            $formatting = 'yyyy-mm-dd';
2✔
1056
                        } elseif (Preg::isMatch('/^\d\d?-[a-zA-Z]+-\d\d\d\d$/', $allCellDataText)) {
10✔
1057
                            $formatting = 'd-mmm-yyyy';
7✔
1058
                        } elseif ($dataValue != floor($dataValue)) {
10✔
1059
                            $formatting = NumberFormat::FORMAT_DATE_XLSX15
7✔
1060
                                . ' '
7✔
1061
                                . NumberFormat::FORMAT_DATE_TIME4;
7✔
1062
                        } else {
1063
                            $formatting = NumberFormat::FORMAT_DATE_XLSX15;
10✔
1064
                        }
1065

1066
                        break;
12✔
1067
                    case 'time':
10✔
1068
                        $type = DataType::TYPE_NUMERIC;
9✔
1069

1070
                        $timeValue = $cellData->getAttributeNS($officeNs, 'time-value');
9✔
1071
                        $minus = '';
9✔
1072
                        if (str_starts_with($timeValue, '-')) {
9✔
1073
                            $minus = '-';
1✔
1074
                            $timeValue = substr($timeValue, 1);
1✔
1075
                        }
1076
                        $timeArray = sscanf($timeValue, 'PT%dH%dM%dS');
9✔
1077
                        if (is_array($timeArray)) {
9✔
1078
                            /** @var array{int, int, int} $timeArray */
1079
                            $days = intdiv($timeArray[0], 24);
9✔
1080
                            $hours = $timeArray[0] % 24;
9✔
1081
                            $dt = new DateTime("1899-12-30 $hours:{$timeArray[1]}:{$timeArray[2]}", new DateTimeZone('UTC'));
9✔
1082
                            $dt->modify("+$days days");
9✔
1083
                            $dataValue = Date::PHPToExcel($dt);
9✔
1084
                            if ($minus === '-') {
9✔
1085
                                $dataValue *= -1;
1✔
1086
                                $formatting = '[hh]:mm:ss';
1✔
1087
                            } else {
1088
                                $formatting = NumberFormat::FORMAT_DATE_TIME4;
9✔
1089
                            }
1090
                        }
1091

1092
                        break;
9✔
1093
                    default:
1094
                        $dataValue = null;
1✔
1095
                }
1096
                if ($customFormatting !== '') {
96✔
1097
                    $formatting = $customFormatting;
1✔
1098
                }
1099
            } else {
1100
                $type = DataType::TYPE_NULL;
48✔
1101
                $dataValue = null;
48✔
1102
            }
1103

1104
            if ($hasCalculatedValue) {
96✔
1105
                $type = DataType::TYPE_FORMULA;
39✔
1106
                $cellDataFormula = substr($cellDataFormula, strpos($cellDataFormula, ':=') + 1);
39✔
1107
                $cellDataFormula = FormulaTranslator::convertToExcelFormulaValue($cellDataFormula);
39✔
1108
            }
1109

1110
            for ($i = 0; $i < $colRepeats; ++$i) {
96✔
1111
                if ($i > 0) {
96✔
1112
                    StringHelper::stringIncrement($columnID);
41✔
1113
                }
1114

1115
                if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) {
96✔
1116
                    continue;
2✔
1117
                }
1118

1119
                if ($type !== DataType::TYPE_NULL) {
96✔
1120
                    for ($rowAdjust = 0; $rowAdjust < $rowRepeats; ++$rowAdjust) {
96✔
1121
                        $rID = $rowID + $rowAdjust;
96✔
1122

1123
                        $cell = $spreadsheet->getActiveSheet()
96✔
1124
                            ->getCell($columnID . $rID);
96✔
1125

1126
                        // Set value
1127
                        if ($hasCalculatedValue) {
96✔
1128
                            $cell->setValueExplicit($cellDataFormula, $type);
39✔
1129
                            if ($cellDataType === 'array') {
39✔
1130
                                $cell->setFormulaAttributes(['t' => 'array', 'ref' => $cellDataRef]);
12✔
1131
                            }
1132
                        } elseif ($type !== '' || $dataValue !== null) {
84✔
1133
                            $cell->setValueExplicit($dataValue, $type);
84✔
1134
                        }
1135

1136
                        if ($hasCalculatedValue) {
96✔
1137
                            $cell->setCalculatedValue($dataValue, $type === DataType::TYPE_NUMERIC);
39✔
1138
                        }
1139

1140
                        // Set other properties
1141
                        if ($formatting !== null) {
96✔
1142
                            $spreadsheet->getActiveSheet()
23✔
1143
                                ->getStyle($columnID . $rID)
23✔
1144
                                ->getNumberFormat()
23✔
1145
                                ->setFormatCode($formatting);
23✔
1146
                        } else {
1147
                            $spreadsheet->getActiveSheet()
91✔
1148
                                ->getStyle($columnID . $rID)
91✔
1149
                                ->getNumberFormat()
91✔
1150
                                ->setFormatCode(NumberFormat::FORMAT_GENERAL);
91✔
1151
                        }
1152

1153
                        if ($hyperlink !== null) {
96✔
1154
                            if ($hyperlink[0] === '#') {
8✔
1155
                                $hyperlink = 'sheet://' . substr($hyperlink, 1);
1✔
1156
                            }
1157
                            $cell->getHyperlink()
8✔
1158
                                ->setUrl($hyperlink);
8✔
1159
                        }
1160
                    }
1161
                }
1162
            }
1163

1164
            // Merged cells
1165
            $this->processMergedCells($cellData, $tableNs, $type, $columnID, $rowID, $spreadsheet);
96✔
1166

1167
            StringHelper::stringIncrement($columnID);
96✔
1168
        }
1169
        $rowID += $rowRepeats;
96✔
1170
    }
1171

1172
    private static function extractNodeName(string $key): string
97✔
1173
    {
1174
        // Remove ns from node name
1175
        if (str_contains($key, ':')) {
97✔
1176
            $keyChunks = explode(':', $key);
97✔
1177
            $key = array_pop($keyChunks);
97✔
1178
        }
1179

1180
        return $key;
97✔
1181
    }
1182

1183
    /**
1184
     * @param string[] $columnWidths
1185
     */
1186
    private function processTableColumnHeader(
1✔
1187
        DOMElement $childNode,
1188
        string $tableNs,
1189
        array $columnWidths,
1190
        int &$tableColumnIndex,
1191
        Spreadsheet $spreadsheet,
1192
        bool $processWidths = true,
1193
        bool $processStyles = true
1194
    ): void {
1195
        foreach ($childNode->childNodes as $grandchildNode) {
1✔
1196
            /** @var DOMElement $grandchildNode */
1197
            $grandkey = self::extractNodeName($grandchildNode->nodeName);
1✔
1198
            switch ($grandkey) {
1199
                case 'table-column':
1✔
1200
                    $this->processTableColumn(
1✔
1201
                        $grandchildNode,
1✔
1202
                        $tableNs,
1✔
1203
                        $columnWidths,
1✔
1204
                        $tableColumnIndex,
1✔
1205
                        $spreadsheet,
1✔
1206
                        $processWidths,
1✔
1207
                        $processStyles
1✔
1208
                    );
1✔
1209

1210
                    break;
1✔
1211
            }
1212
        }
1213
    }
1214

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

1242
                    break;
1✔
1243
                case 'table-header-columns':
×
1244
                case 'table-columns':
×
NEW
1245
                    $this->processTableColumnHeader(
×
1246
                        $grandchildNode,
×
1247
                        $tableNs,
×
1248
                        $columnWidths,
×
1249
                        $tableColumnIndex,
×
NEW
1250
                        $spreadsheet,
×
NEW
1251
                        $processWidths,
×
NEW
1252
                        $processStyles
×
UNCOV
1253
                    );
×
1254

1255
                    break;
×
1256
                case 'table-column-group':
×
1257
                    $this->processTableColumnGroup(
×
1258
                        $grandchildNode,
×
1259
                        $tableNs,
×
1260
                        $columnWidths,
×
1261
                        $tableColumnIndex,
×
NEW
1262
                        $spreadsheet,
×
NEW
1263
                        $processWidths,
×
NEW
1264
                        $processStyles
×
UNCOV
1265
                    );
×
1266

1267
                    break;
×
1268
            }
1269
        }
1270
    }
1271

1272
    /**
1273
     * @param string[] $columnWidths
1274
     */
1275
    private function processTableColumn(
52✔
1276
        DOMElement $childNode,
1277
        string $tableNs,
1278
        array $columnWidths,
1279
        int &$tableColumnIndex,
1280
        Spreadsheet $spreadsheet,
1281
        bool $processWidths = true,
1282
        bool $processStyles = true
1283
    ): void {
1284
        if ($childNode->hasAttributeNS($tableNs, 'number-columns-repeated')) {
52✔
1285
            $rowRepeats = (int) $childNode->getAttributeNS($tableNs, 'number-columns-repeated');
44✔
1286
        } else {
1287
            $rowRepeats = 1;
25✔
1288
        }
1289
        $tableStyleName = $childNode->getAttributeNS($tableNs, 'style-name');
52✔
1290
        if ($processWidths) {
52✔
1291
            if (isset($columnWidths[$tableStyleName])) {
50✔
1292
                $columnWidth = new HelperDimension($columnWidths[$tableStyleName]);
49✔
1293
                $tableColumnIndex2 = $tableColumnIndex;
49✔
1294
                $tableColumnString = Coordinate::stringFromColumnIndex($tableColumnIndex2);
49✔
1295
                for ($rowRepeats2 = $rowRepeats; $rowRepeats2 > 0 && $tableColumnIndex2 <= AddressRange::MAX_COLUMN_INT; --$rowRepeats2) {
49✔
1296
                    if (!$this->readEmptyCells && $tableColumnIndex2 > $this->highestDataIndex) {
49✔
1297
                        break;
1✔
1298
                    }
1299
                    $spreadsheet->getActiveSheet()
49✔
1300
                        ->getColumnDimension($tableColumnString)
49✔
1301
                        ->setWidth($columnWidth->toUnit('cm'), 'cm');
49✔
1302
                    StringHelper::stringIncrement(
49✔
1303
                        $tableColumnString
49✔
1304
                    );
49✔
1305
                    ++$tableColumnIndex2;
49✔
1306
                }
1307
            }
1308
        }
1309
        if ($processStyles) {
52✔
1310
            $defaultStyleName = $childNode->getAttributeNS($tableNs, 'default-cell-style-name');
52✔
1311
            if ($defaultStyleName !== 'Default' && isset($this->allStyles[$defaultStyleName])) {
52✔
1312
                $tableColumnIndex2 = $tableColumnIndex;
9✔
1313
                $tableColumnString = Coordinate::stringFromColumnIndex($tableColumnIndex2);
9✔
1314
                for ($rowRepeats2 = $rowRepeats; $rowRepeats2 > 0 && $tableColumnIndex2 <= AddressRange::MAX_COLUMN_INT; --$rowRepeats2) {
9✔
1315
                    $spreadsheet->getActiveSheet()
9✔
1316
                        ->getStyle($tableColumnString)
9✔
1317
                        ->applyFromArray(
9✔
1318
                            $this->allStyles[$defaultStyleName]
9✔
1319
                        );
9✔
1320
                    StringHelper::stringIncrement(
9✔
1321
                        $tableColumnString
9✔
1322
                    );
9✔
1323
                    ++$tableColumnIndex2;
9✔
1324
                }
1325
            }
1326
        }
1327
        $tableColumnIndex += $rowRepeats;
52✔
1328
    }
1329

1330
    private function processSettings(ZipArchive $zip, Spreadsheet $spreadsheet): void
90✔
1331
    {
1332
        $dom = new DOMDocument('1.01', 'UTF-8');
90✔
1333
        $dom->loadXML(
90✔
1334
            $this->getSecurityScannerOrThrow()
90✔
1335
                ->scan($zip->getFromName('settings.xml'))
90✔
1336
        );
90✔
1337
        $configNs = (string) $dom->lookupNamespaceUri('config');
90✔
1338
        $officeNs = (string) $dom->lookupNamespaceUri('office');
90✔
1339
        $settings = $dom->getElementsByTagNameNS($officeNs, 'settings')
90✔
1340
            ->item(0);
90✔
1341
        if ($settings !== null) {
90✔
1342
            $this->lookForActiveSheet($settings, $spreadsheet, $configNs);
90✔
1343
            $this->lookForSelectedCells($settings, $spreadsheet, $configNs);
90✔
1344
        }
1345
    }
1346

1347
    private function lookForActiveSheet(DOMElement $settings, Spreadsheet $spreadsheet, string $configNs): void
90✔
1348
    {
1349
        /** @var DOMElement $t */
1350
        foreach ($settings->getElementsByTagNameNS($configNs, 'config-item') as $t) {
90✔
1351
            if ($t->getAttributeNs($configNs, 'name') === 'ActiveTable') {
89✔
1352
                try {
1353
                    $spreadsheet->setActiveSheetIndexByName($t->nodeValue ?? '');
89✔
1354
                } catch (Throwable) {
2✔
1355
                    // do nothing
1356
                }
1357

1358
                break;
89✔
1359
            }
1360
        }
1361
    }
1362

1363
    private function lookForSelectedCells(DOMElement $settings, Spreadsheet $spreadsheet, string $configNs): void
90✔
1364
    {
1365
        /** @var DOMElement $t */
1366
        foreach ($settings->getElementsByTagNameNS($configNs, 'config-item-map-named') as $t) {
90✔
1367
            if ($t->getAttributeNs($configNs, 'name') === 'Tables') {
89✔
1368
                foreach ($t->getElementsByTagNameNS($configNs, 'config-item-map-entry') as $ws) {
89✔
1369
                    $setRow = $setCol = '';
89✔
1370
                    $wsname = $ws->getAttributeNs($configNs, 'name');
89✔
1371
                    foreach ($ws->getElementsByTagNameNS($configNs, 'config-item') as $configItem) {
89✔
1372
                        $attrName = $configItem->getAttributeNs($configNs, 'name');
89✔
1373
                        if ($attrName === 'CursorPositionX') {
89✔
1374
                            $setCol = $configItem->nodeValue;
89✔
1375
                        }
1376
                        if ($attrName === 'CursorPositionY') {
89✔
1377
                            $setRow = $configItem->nodeValue;
89✔
1378
                        }
1379
                    }
1380
                    $this->setSelected($spreadsheet, $wsname, "$setCol", "$setRow");
89✔
1381
                }
1382

1383
                break;
89✔
1384
            }
1385
        }
1386
    }
1387

1388
    private function setSelected(Spreadsheet $spreadsheet, string $wsname, string $setCol, string $setRow): void
89✔
1389
    {
1390
        if (is_numeric($setCol) && is_numeric($setRow)) {
89✔
1391
            $sheet = $spreadsheet->getSheetByName($wsname);
89✔
1392
            if ($sheet !== null) {
89✔
1393
                $sheet->setSelectedCells([(int) $setCol + 1, (int) $setRow + 1]);
88✔
1394
            }
1395
        }
1396
    }
1397

1398
    /**
1399
     * Recursively scan element.
1400
     */
1401
    protected function scanElementForText(DOMNode $element): string
96✔
1402
    {
1403
        $str = '';
96✔
1404
        foreach ($element->childNodes as $child) {
96✔
1405
            /** @var DOMNode $child */
1406
            if ($child->nodeType == XML_TEXT_NODE) {
96✔
1407
                $str .= $child->nodeValue;
96✔
1408
            } elseif ($child->nodeType == XML_ELEMENT_NODE && $child->nodeName == 'text:line-break') {
19✔
1409
                $str .= "\n";
1✔
1410
            } elseif ($child->nodeType == XML_ELEMENT_NODE && $child->nodeName == 'text:s') {
19✔
1411
                // It's a space
1412

1413
                // Multiple spaces?
1414
                $attributes = $child->attributes;
6✔
1415
                /** @var ?DOMAttr $cAttr */
1416
                $cAttr = ($attributes === null) ? null : $attributes->getNamedItem('c');
6✔
1417
                $multiplier = self::getMultiplier($cAttr);
6✔
1418
                $str .= str_repeat(' ', $multiplier);
6✔
1419
            }
1420

1421
            if ($child->hasChildNodes()) {
96✔
1422
                $str .= $this->scanElementForText($child);
17✔
1423
            }
1424
        }
1425

1426
        return $str;
96✔
1427
    }
1428

1429
    private static function getMultiplier(?DOMAttr $cAttr): int
6✔
1430
    {
1431
        if ($cAttr) {
6✔
1432
            $multiplier = (int) $cAttr->nodeValue;
6✔
1433
        } else {
1434
            $multiplier = 1;
6✔
1435
        }
1436

1437
        return $multiplier;
6✔
1438
    }
1439

1440
    private function parseRichText(string $is): RichText
12✔
1441
    {
1442
        $value = new RichText();
12✔
1443
        $value->createText($is);
12✔
1444

1445
        return $value;
12✔
1446
    }
1447

1448
    private function processMergedCells(
96✔
1449
        DOMElement $cellData,
1450
        string $tableNs,
1451
        string $type,
1452
        string $columnID,
1453
        int $rowID,
1454
        Spreadsheet $spreadsheet
1455
    ): void {
1456
        if (
1457
            $cellData->hasAttributeNS($tableNs, 'number-columns-spanned')
96✔
1458
            || $cellData->hasAttributeNS($tableNs, 'number-rows-spanned')
96✔
1459
        ) {
1460
            if (($type !== DataType::TYPE_NULL) || ($this->readDataOnly === false)) {
19✔
1461
                $columnTo = $columnID;
19✔
1462

1463
                if ($cellData->hasAttributeNS($tableNs, 'number-columns-spanned')) {
19✔
1464
                    $columnIndex = Coordinate::columnIndexFromString($columnID);
18✔
1465
                    $columnIndex += (int) $cellData->getAttributeNS($tableNs, 'number-columns-spanned');
18✔
1466
                    $columnIndex -= 2;
18✔
1467

1468
                    $columnTo = Coordinate::stringFromColumnIndex($columnIndex + 1);
18✔
1469
                }
1470

1471
                $rowTo = $rowID;
19✔
1472

1473
                if ($cellData->hasAttributeNS($tableNs, 'number-rows-spanned')) {
19✔
1474
                    $rowTo = $rowTo + (int) $cellData->getAttributeNS($tableNs, 'number-rows-spanned') - 1;
19✔
1475
                }
1476

1477
                $cellRange = $columnID . $rowID . ':' . $columnTo . $rowTo;
19✔
1478
                $spreadsheet->getActiveSheet()->mergeCells($cellRange, Worksheet::MERGE_CELL_CONTENT_HIDE);
19✔
1479
            }
1480
        }
1481
    }
1482

1483
    /** @var null|Closure(string, string):string */
1484
    private ?Closure $formatCallback = null;
1485

1486
    /** @param Closure(string, string):string $formatCallback */
1487
    public function setFormatCallback(Closure $formatCallback): void
1✔
1488
    {
1489
        $this->formatCallback = $formatCallback;
1✔
1490
    }
1491

1492
    /** @return array{
1493
     *   autoColor?: true,
1494
     *   bold?: true,
1495
     *   color?: array{rgb: string},
1496
     *   italic?: true,
1497
     *   name?: non-empty-string,
1498
     *   size?: float|int,
1499
     *   strikethrough?: true,
1500
     *   underline?: 'double'|'single',
1501
     * }
1502
     */
1503
    protected function getFontStyles(DOMElement $textProperty, string $styleNs, string $fontNs): array
97✔
1504
    {
1505
        $fonts = [];
97✔
1506
        $temp = $textProperty->getAttributeNs($styleNs, 'font-name') ?: $textProperty->getAttributeNs($fontNs, 'font-family');
97✔
1507
        if ($temp !== '') {
97✔
1508
            $fonts['name'] = $temp;
97✔
1509
        }
1510
        $temp = $textProperty->getAttributeNs($fontNs, 'font-size');
97✔
1511
        if ($temp !== '' && str_ends_with($temp, 'pt')) {
97✔
1512
            $fonts['size'] = (float) substr($temp, 0, -2);
97✔
1513
        }
1514
        $temp = $textProperty->getAttributeNs($fontNs, 'font-style');
97✔
1515
        if ($temp === 'italic') {
97✔
1516
            $fonts['italic'] = true;
45✔
1517
        }
1518
        $temp = $textProperty->getAttributeNs($fontNs, 'font-weight');
97✔
1519
        if ($temp === 'bold') {
97✔
1520
            $fonts['bold'] = true;
47✔
1521
        }
1522
        $temp = $textProperty->getAttributeNs($fontNs, 'color');
97✔
1523
        if (Preg::isMatch('/^#[a-f0-9]{6}$/i', $temp)) {
97✔
1524
            $fonts['color'] = ['rgb' => substr($temp, 1)];
88✔
1525
        }
1526
        $temp = $textProperty->getAttributeNs($styleNs, 'use-window-font-color');
97✔
1527
        if ($temp === 'true') {
97✔
1528
            $fonts['autoColor'] = true;
3✔
1529
        }
1530
        $temp = $textProperty->getAttributeNs($styleNs, 'text-underline-type');
97✔
1531
        if ($temp === '') {
97✔
1532
            $temp = $textProperty->getAttributeNs($styleNs, 'text-underline-style');
97✔
1533
            if ($temp !== '' && $temp !== 'none') {
97✔
1534
                $temp = 'single';
42✔
1535
            }
1536
        }
1537
        if ($temp === 'single' || $temp === 'double') {
97✔
1538
            $fonts['underline'] = $temp;
43✔
1539
        }
1540
        $temp = $textProperty->getAttributeNs($styleNs, 'text-line-through-type');
97✔
1541
        if ($temp !== '' && $temp !== 'none') {
97✔
1542
            $fonts['strikethrough'] = true;
10✔
1543
        }
1544

1545
        return $fonts;
97✔
1546
    }
1547

1548
    /** @return array{
1549
     *   fillType?: string,
1550
     *   startColor?: array{rgb: string},
1551
     * }
1552
     */
1553
    protected function getFillStyles(DOMElement $tableCellProperties, string $fontNs): array
97✔
1554
    {
1555
        $fills = [];
97✔
1556
        $temp = $tableCellProperties->getAttributeNs($fontNs, 'background-color');
97✔
1557
        if (Preg::isMatch('/^#[a-f0-9]{6}$/i', $temp)) {
97✔
1558
            $fills['fillType'] = Fill::FILL_SOLID;
38✔
1559
            $fills['startColor'] = ['rgb' => substr($temp, 1)];
38✔
1560
        } elseif ($temp === 'transparent') {
88✔
1561
            $fills['fillType'] = Fill::FILL_NONE;
64✔
1562
        }
1563

1564
        return $fills;
97✔
1565
    }
1566

1567
    private const MAP_VERTICAL = [
1568
        'top' => Alignment::VERTICAL_TOP,
1569
        'middle' => Alignment::VERTICAL_CENTER,
1570
        'automatic' => Alignment::VERTICAL_JUSTIFY,
1571
        'bottom' => Alignment::VERTICAL_BOTTOM,
1572
    ];
1573
    private const MAP_HORIZONTAL = [
1574
        'center' => Alignment::HORIZONTAL_CENTER,
1575
        'end' => Alignment::HORIZONTAL_RIGHT,
1576
        'justify' => Alignment::HORIZONTAL_FILL,
1577
        'start' => Alignment::HORIZONTAL_LEFT,
1578
    ];
1579

1580
    /** @return array{
1581
     *   shrinkToFit?: bool,
1582
     *   textRotation?: int,
1583
     *   vertical?: string,
1584
     *   wrapText?: bool,
1585
     * }
1586
     */
1587
    protected function getAlignment1Styles(DOMElement $tableCellProperties, string $styleNs, string $fontNs): array
74✔
1588
    {
1589
        $alignment1 = [];
74✔
1590
        $temp = $tableCellProperties->getAttributeNs($styleNs, 'rotation-angle');
74✔
1591
        if (is_numeric($temp)) {
74✔
1592
            $temp2 = (int) $temp;
57✔
1593
            if ($temp2 > 90) {
57✔
1594
                $temp2 -= 360;
9✔
1595
            }
1596
            if ($temp2 >= -90 && $temp2 <= 90) {
57✔
1597
                $alignment1['textRotation'] = (int) $temp2;
57✔
1598
            }
1599
        }
1600
        $temp = $tableCellProperties->getAttributeNs($styleNs, 'vertical-align');
74✔
1601
        $temp2 = self::MAP_VERTICAL[$temp] ?? '';
74✔
1602
        if ($temp2 !== '') {
74✔
1603
            $alignment1['vertical'] = $temp2;
69✔
1604
        }
1605
        $temp = $tableCellProperties->getAttributeNs($fontNs, 'wrap-option');
74✔
1606
        if ($temp === 'wrap') {
74✔
1607
            $alignment1['wrapText'] = true;
12✔
1608
        } elseif ($temp === 'no-wrap') {
73✔
1609
            $alignment1['wrapText'] = false;
9✔
1610
        }
1611
        $temp = $tableCellProperties->getAttributeNs($styleNs, 'shrink-to-fit');
74✔
1612
        if ($temp === 'true' || $temp === 'false') {
74✔
1613
            $alignment1['shrinkToFit'] = $temp === 'true';
10✔
1614
        }
1615

1616
        return $alignment1;
74✔
1617
    }
1618

1619
    /** @return array{
1620
     *   horizontal?: string,
1621
     *   readOrder?: int,
1622
     * }
1623
     */
1624
    protected function getAlignment2Styles(DOMElement $paragraphProperties, string $styleNs, string $fontNs): array
23✔
1625
    {
1626
        $alignment2 = [];
23✔
1627
        $temp = $paragraphProperties->getAttributeNs($fontNs, 'text-align');
23✔
1628
        $temp2 = self::MAP_HORIZONTAL[$temp] ?? '';
23✔
1629
        if ($temp2 !== '') {
23✔
1630
            $alignment2['horizontal'] = $temp2;
23✔
1631
        }
1632
        $temp = $paragraphProperties->getAttributeNs($fontNs, 'margin-left') ?: $paragraphProperties->getAttributeNs($fontNs, 'margin-right');
23✔
1633
        if (Preg::isMatch('/^\d+([.]\d+)?(cm|in|mm|pt)$/', $temp)) {
23✔
1634
            $dimension = new HelperDimension($temp);
14✔
1635
            $alignment2['indent'] = (int) round($dimension->toUnit('px') / Alignment::INDENT_UNITS_TO_PIXELS);
14✔
1636
        }
1637

1638
        $temp = $paragraphProperties->getAttributeNs($styleNs, 'writing-mode');
23✔
1639
        if ($temp === 'rl-tb') {
23✔
1640
            $alignment2['readOrder'] = Alignment::READORDER_RTL;
1✔
1641
        } elseif ($temp === 'lr-tb') {
23✔
1642
            $alignment2['readOrder'] = Alignment::READORDER_LTR;
1✔
1643
        }
1644

1645
        return $alignment2;
23✔
1646
    }
1647

1648
    /** @return array{
1649
     *   locked?: string,
1650
     *   hidden?: string,
1651
     * }
1652
     */
1653
    protected function getProtectionStyles(DOMElement $tableCellProperties, string $styleNs): array
74✔
1654
    {
1655
        $protection = [];
74✔
1656
        $temp = $tableCellProperties->getAttributeNs($styleNs, 'cell-protect');
74✔
1657
        switch ($temp) {
1658
            case 'protected formula-hidden':
74✔
1659
                $protection['locked'] = Protection::PROTECTION_PROTECTED;
1✔
1660
                $protection['hidden'] = Protection::PROTECTION_PROTECTED;
1✔
1661

1662
                break;
1✔
1663
            case 'formula-hidden':
74✔
1664
                $protection['locked'] = Protection::PROTECTION_UNPROTECTED;
1✔
1665
                $protection['hidden'] = Protection::PROTECTION_PROTECTED;
1✔
1666

1667
                break;
1✔
1668
            case 'protected':
74✔
1669
                $protection['locked'] = Protection::PROTECTION_PROTECTED;
10✔
1670
                $protection['hidden'] = Protection::PROTECTION_UNPROTECTED;
10✔
1671

1672
                break;
10✔
1673
            case 'none':
74✔
1674
                $protection['locked'] = Protection::PROTECTION_UNPROTECTED;
1✔
1675
                $protection['hidden'] = Protection::PROTECTION_UNPROTECTED;
1✔
1676

1677
                break;
1✔
1678
        }
1679

1680
        return $protection;
74✔
1681
    }
1682

1683
    private const MAP_BORDER_STYLE = [ // default BORDER_THIN
1684
        'none' => Border::BORDER_NONE,
1685
        'hidden' => Border::BORDER_NONE,
1686
        'dotted' => Border::BORDER_DOTTED,
1687
        'dash-dot' => Border::BORDER_DASHDOT,
1688
        'dash-dot-dot' => Border::BORDER_DASHDOTDOT,
1689
        'dashed' => Border::BORDER_DASHED,
1690
        'double' => Border::BORDER_DOUBLE,
1691
    ];
1692

1693
    private const MAP_BORDER_MEDIUM = [
1694
        Border::BORDER_THIN => Border::BORDER_MEDIUM,
1695
        Border::BORDER_DASHDOT => Border::BORDER_MEDIUMDASHDOT,
1696
        Border::BORDER_DASHDOTDOT => Border::BORDER_MEDIUMDASHDOTDOT,
1697
        Border::BORDER_DASHED => Border::BORDER_MEDIUMDASHED,
1698
    ];
1699

1700
    private const MAP_BORDER_THICK = [
1701
        Border::BORDER_THIN => Border::BORDER_THICK,
1702
        Border::BORDER_DASHDOT => Border::BORDER_MEDIUMDASHDOT,
1703
        Border::BORDER_DASHDOTDOT => Border::BORDER_MEDIUMDASHDOTDOT,
1704
        Border::BORDER_DASHED => Border::BORDER_MEDIUMDASHED,
1705
    ];
1706

1707
    /** @return array{
1708
     *   bottom?: array{borderStyle:string, color:array{rgb: string}},
1709
     *   top?: array{borderStyle:string, color:array{rgb: string}},
1710
     *   left?: array{borderStyle:string, color:array{rgb: string}},
1711
     *   right?: array{borderStyle:string, color:array{rgb: string}},
1712
     *   diagonal?: array{borderStyle:string, color:array{rgb: string}},
1713
     *   diagonalDirection?: int,
1714
     * }
1715
     */
1716
    protected function getBorderStyles(DOMElement $tableCellProperties, string $fontNs, string $styleNs): array
74✔
1717
    {
1718
        $borders = [];
74✔
1719
        $temp = $tableCellProperties->getAttributeNs($fontNs, 'border');
74✔
1720
        $diagonalIndex = Borders::DIAGONAL_NONE;
74✔
1721
        foreach (['bottom', 'left', 'right', 'top', 'diagonal-tl-br', 'diagonal-bl-tr'] as $direction) {
74✔
1722
            if (str_starts_with($direction, 'diagonal')) {
74✔
1723
                $directionIndex = 'diagonal';
74✔
1724
                $temp = $tableCellProperties->getAttributeNs($styleNs, $direction);
74✔
1725
            } else {
1726
                $directionIndex = $direction;
74✔
1727
                $temp = $tableCellProperties->getAttributeNs($fontNs, "border-$direction");
74✔
1728
            }
1729
            if (Preg::isMatch('/^(\d+(?:[.]\d+)?)pt\s+([-\w]+)\s+#([0-9a-fA-F]{6})$/', $temp, $matches)) {
74✔
1730
                $style = self::MAP_BORDER_STYLE[$matches[2]] ?? Border::BORDER_THIN;
12✔
1731
                $width = (float) $matches[1];
12✔
1732
                if ($width >= 2.5) {
12✔
1733
                    $style = self::MAP_BORDER_THICK[$style] ?? $style;
12✔
1734
                } elseif ($width >= 1.75) {
10✔
1735
                    $style = self::MAP_BORDER_MEDIUM[$style] ?? $style;
10✔
1736
                }
1737
                $color = $matches[3];
12✔
1738
                $borders[$directionIndex] = ['borderStyle' => $style, 'color' => ['rgb' => $matches[3]]];
12✔
1739
                if ($direction === 'diagonal-tl-br') {
12✔
1740
                    $diagonalIndex = Borders::DIAGONAL_DOWN;
10✔
1741
                } elseif ($direction === 'diagonal-bl-tr') {
12✔
1742
                    $diagonalIndex = ($diagonalIndex === Borders::DIAGONAL_NONE) ? Borders::DIAGONAL_UP : Borders::DIAGONAL_BOTH;
10✔
1743
                }
1744
            }
1745
        }
1746
        if ($diagonalIndex !== Borders::DIAGONAL_NONE) {
74✔
1747
            $borders['diagonalDirection'] = $diagonalIndex;
10✔
1748
        }
1749

1750
        return $borders; // @phpstan-ignore-line
74✔
1751
    }
1752
}
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