Xlsx::readTablesInTablesFile()   A
last analyzed

Complexity

Conditions 6
Paths 6

Size

Total Lines 28
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 15
dl 0
loc 28
ccs 16
cts 16
cp 1
rs 9.2222
c 0
b 0
f 0
cc 6
nc 6
nop 8
crap 6

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Reader;
4
5
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
6
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
7
use PhpOffice\PhpSpreadsheet\Cell\DataType;
8
use PhpOffice\PhpSpreadsheet\Cell\Hyperlink;
9
use PhpOffice\PhpSpreadsheet\Comment;
10
use PhpOffice\PhpSpreadsheet\DefinedName;
11
use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner;
12
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\AutoFilter;
13
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Chart;
14
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\ColumnAndRowAttributes;
15
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\ConditionalStyles;
16
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\DataValidations;
17
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Hyperlinks;
18
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Namespaces;
19
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\PageSetup;
20
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Properties as PropertyReader;
21
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\SharedFormula;
22
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\SheetViewOptions;
23
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\SheetViews;
24
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Styles;
25
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\TableReader;
26
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Theme;
27
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\WorkbookView;
28
use PhpOffice\PhpSpreadsheet\ReferenceHelper;
29
use PhpOffice\PhpSpreadsheet\RichText\RichText;
30
use PhpOffice\PhpSpreadsheet\Shared\Date;
31
use PhpOffice\PhpSpreadsheet\Shared\Drawing;
32
use PhpOffice\PhpSpreadsheet\Shared\File;
33
use PhpOffice\PhpSpreadsheet\Shared\Font;
34
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
35
use PhpOffice\PhpSpreadsheet\Spreadsheet;
36
use PhpOffice\PhpSpreadsheet\Style\Color;
37
use PhpOffice\PhpSpreadsheet\Style\Font as StyleFont;
38
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
39
use PhpOffice\PhpSpreadsheet\Style\Style;
40
use PhpOffice\PhpSpreadsheet\Worksheet\HeaderFooterDrawing;
41
use PhpOffice\PhpSpreadsheet\Worksheet\Table\TableDxfsStyle;
42
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
43
use SimpleXMLElement;
44
use Throwable;
45
use XMLReader;
46
use ZipArchive;
47
48
class Xlsx extends BaseReader
49
{
50
    const INITIAL_FILE = '_rels/.rels';
51
52
    /**
53
     * ReferenceHelper instance.
54
     */
55
    private ReferenceHelper $referenceHelper;
56
57
    private ZipArchive $zip;
58
59
    private Styles $styleReader;
60
61
    /** @var SharedFormula[] */
62
    private array $sharedFormulae = [];
63
64
    private bool $parseHuge = false;
65
66
    /**
67
     * Allow use of LIBXML_PARSEHUGE.
68
     * This option can lead to memory leaks and failures,
69
     * and is not recommended. But some very large spreadsheets
70
     * seem to require it.
71
     */
72
    public function setParseHuge(bool $parseHuge): void
73
    {
74
        $this->parseHuge = $parseHuge;
75
    }
76
77
    /**
78
     * Create a new Xlsx Reader instance.
79
     */
80 761
    public function __construct()
81
    {
82 761
        parent::__construct();
83 761
        $this->referenceHelper = ReferenceHelper::getInstance();
84 761
        $this->securityScanner = XmlScanner::getInstance($this);
85
    }
86
87
    /**
88
     * Can the current IReader read the file?
89
     */
90 36
    public function canRead(string $filename): bool
91
    {
92 36
        if (!File::testFileNoThrow($filename, self::INITIAL_FILE)) {
93 16
            return false;
94
        }
95
96 20
        $result = false;
97 20
        $this->zip = $zip = new ZipArchive();
98
99 20
        if ($zip->open($filename) === true) {
100 20
            [$workbookBasename] = $this->getWorkbookBaseName();
101 20
            $result = !empty($workbookBasename);
102
103 20
            $zip->close();
104
        }
105
106 20
        return $result;
107
    }
108
109 734
    public static function testSimpleXml(mixed $value): SimpleXMLElement
110
    {
111 734
        return ($value instanceof SimpleXMLElement) ? $value : new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><root></root>');
112
    }
113
114 730
    public static function getAttributes(?SimpleXMLElement $value, string $ns = ''): SimpleXMLElement
115
    {
116 730
        return self::testSimpleXml($value === null ? $value : $value->attributes($ns));
117
    }
118
119
    // Phpstan thinks, correctly, that xpath can return false.
120
    /** @return mixed[] */
121 700
    private static function xpathNoFalse(SimpleXMLElement $sxml, string $path): array
122
    {
123 700
        return self::falseToArray($sxml->xpath($path));
124
    }
125
126
    /** @return mixed[] */
127 700
    public static function falseToArray(mixed $value): array
128
    {
129 700
        return is_array($value) ? $value : [];
130
    }
131
132 730
    private function loadZip(string $filename, string $ns = '', bool $replaceUnclosedBr = false): SimpleXMLElement
133
    {
134 730
        $contents = $this->getFromZipArchive($this->zip, $filename);
135 730
        if ($replaceUnclosedBr) {
136 38
            $contents = str_replace('<br>', '<br/>', $contents);
137
        }
138 730
        $rels = @simplexml_load_string(
139 730
            $this->getSecurityScannerOrThrow()->scan($contents),
140 730
            SimpleXMLElement::class,
141 730
            $this->parseHuge ? LIBXML_PARSEHUGE : 0,
142 730
            $ns
143 730
        );
144
145 730
        return self::testSimpleXml($rels);
146
    }
147
148
    // This function is just to identify cases where I'm not sure
149
    // why empty namespace is required.
150 698
    private function loadZipNonamespace(string $filename, string $ns): SimpleXMLElement
151
    {
152 698
        $contents = $this->getFromZipArchive($this->zip, $filename);
153 698
        $rels = simplexml_load_string(
154 698
            $this->getSecurityScannerOrThrow()->scan($contents),
155 698
            SimpleXMLElement::class,
156 698
            $this->parseHuge ? LIBXML_PARSEHUGE : 0,
157 698
            ($ns === '' ? $ns : '')
158 698
        );
159
160 693
        return self::testSimpleXml($rels);
161
    }
162
163
    private const REL_TO_MAIN = [
164
        Namespaces::PURL_OFFICE_DOCUMENT => Namespaces::PURL_MAIN,
165
        Namespaces::THUMBNAIL => '',
166
    ];
167
168
    private const REL_TO_DRAWING = [
169
        Namespaces::PURL_RELATIONSHIPS => Namespaces::PURL_DRAWING,
170
    ];
171
172
    private const REL_TO_CHART = [
173
        Namespaces::PURL_RELATIONSHIPS => Namespaces::PURL_CHART,
174
    ];
175
176
    /**
177
     * Reads names of the worksheets from a file, without parsing the whole file to a Spreadsheet object.
178
     *
179
     * @return string[]
180
     */
181 18
    public function listWorksheetNames(string $filename): array
182
    {
183 18
        File::assertFile($filename, self::INITIAL_FILE);
184
185 15
        $worksheetNames = [];
186
187 15
        $this->zip = $zip = new ZipArchive();
188 15
        $zip->open($filename);
189
190
        //    The files we're looking at here are small enough that simpleXML is more efficient than XMLReader
191 15
        $rels = $this->loadZip(self::INITIAL_FILE, Namespaces::RELATIONSHIPS);
192 15
        foreach ($rels->Relationship as $relx) {
193 15
            $rel = self::getAttributes($relx);
194 15
            $relType = (string) $rel['Type'];
195 15
            $mainNS = self::REL_TO_MAIN[$relType] ?? Namespaces::MAIN;
196 15
            if ($mainNS !== '') {
197 15
                $xmlWorkbook = $this->loadZip((string) $rel['Target'], $mainNS);
198
199 15
                if ($xmlWorkbook->sheets) {
200 15
                    foreach ($xmlWorkbook->sheets->sheet as $eleSheet) {
201
                        // Check if sheet should be skipped
202 15
                        $worksheetNames[] = (string) self::getAttributes($eleSheet)['name'];
203
                    }
204
                }
205
            }
206
        }
207
208 15
        $zip->close();
209
210 15
        return $worksheetNames;
211
    }
212
213
    /**
214
     * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
215
     *
216
     * @return array<int, array{worksheetName: string, lastColumnLetter: string, lastColumnIndex: int, totalRows: int, totalColumns: int, sheetState: string}>
217
     */
218 19
    public function listWorksheetInfo(string $filename): array
219
    {
220 19
        File::assertFile($filename, self::INITIAL_FILE);
221
222 16
        $worksheetInfo = [];
223
224 16
        $this->zip = $zip = new ZipArchive();
225 16
        $zip->open($filename);
226
227 16
        $rels = $this->loadZip(self::INITIAL_FILE, Namespaces::RELATIONSHIPS);
228 16
        foreach ($rels->Relationship as $relx) {
229 16
            $rel = self::getAttributes($relx);
230 16
            $relType = (string) $rel['Type'];
231 16
            $mainNS = self::REL_TO_MAIN[$relType] ?? Namespaces::MAIN;
232 16
            if ($mainNS !== '') {
233 16
                $relTarget = (string) $rel['Target'];
234 16
                $dir = dirname($relTarget);
235 16
                $namespace = dirname($relType);
236 16
                $relsWorkbook = $this->loadZip("$dir/_rels/" . basename($relTarget) . '.rels', Namespaces::RELATIONSHIPS);
237
238 16
                $worksheets = [];
239 16
                foreach ($relsWorkbook->Relationship as $elex) {
240 16
                    $ele = self::getAttributes($elex);
241
                    if (
242 16
                        ((string) $ele['Type'] === "$namespace/worksheet")
243 16
                        || ((string) $ele['Type'] === "$namespace/chartsheet")
244
                    ) {
245 16
                        $worksheets[(string) $ele['Id']] = $ele['Target'];
246
                    }
247
                }
248
249 16
                $xmlWorkbook = $this->loadZip($relTarget, $mainNS);
250 16
                if ($xmlWorkbook->sheets) {
251 16
                    $dir = dirname($relTarget);
252
253 16
                    foreach ($xmlWorkbook->sheets->sheet as $eleSheet) {
254 16
                        $tmpInfo = [
255 16
                            'worksheetName' => (string) self::getAttributes($eleSheet)['name'],
256 16
                            'lastColumnLetter' => 'A',
257 16
                            'lastColumnIndex' => 0,
258 16
                            'totalRows' => 0,
259 16
                            'totalColumns' => 0,
260 16
                        ];
261 16
                        $sheetState = (string) (self::getAttributes($eleSheet)['state'] ?? Worksheet::SHEETSTATE_VISIBLE);
262 16
                        $tmpInfo['sheetState'] = $sheetState;
263
264 16
                        $fileWorksheet = (string) $worksheets[self::getArrayItemString(self::getAttributes($eleSheet, $namespace), 'id')];
265 16
                        $fileWorksheetPath = str_starts_with($fileWorksheet, '/') ? substr($fileWorksheet, 1) : "$dir/$fileWorksheet";
266
267 16
                        $xml = new XMLReader();
268 16
                        $xml->xml(
269 16
                            $this->getSecurityScannerOrThrow()
270 16
                                ->scan(
271 16
                                    $this->getFromZipArchive(
272 16
                                        $this->zip,
273 16
                                        $fileWorksheetPath
274 16
                                    )
275 16
                                ),
276 16
                            null,
277 16
                            $this->parseHuge ? LIBXML_PARSEHUGE : 0
278 16
                        );
279 16
                        $xml->setParserProperty(2, true);
280
281 16
                        $currCells = 0;
282 16
                        while ($xml->read()) {
283 16
                            if ($xml->localName == 'row' && $xml->nodeType == XMLReader::ELEMENT && $xml->namespaceURI === $mainNS) {
284 16
                                $row = (int) $xml->getAttribute('r');
285 16
                                $tmpInfo['totalRows'] = $row;
286 16
                                $tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells);
287 16
                                $currCells = 0;
288 16
                            } elseif ($xml->localName == 'c' && $xml->nodeType == XMLReader::ELEMENT && $xml->namespaceURI === $mainNS) {
289 16
                                $cell = $xml->getAttribute('r');
290 16
                                $currCells = $cell ? max($currCells, Coordinate::indexesFromString($cell)[0]) : ($currCells + 1);
291
                            }
292
                        }
293 16
                        $tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells);
294 16
                        $xml->close();
295
296 16
                        $tmpInfo['lastColumnIndex'] = $tmpInfo['totalColumns'] - 1;
297 16
                        $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1);
298
299 16
                        $worksheetInfo[] = $tmpInfo;
300
                    }
301
                }
302
            }
303
        }
304
305 16
        $zip->close();
306
307 16
        return $worksheetInfo;
308
    }
309
310 22
    private static function castToBoolean(SimpleXMLElement $c): bool
311
    {
312 22
        $value = isset($c->v) ? (string) $c->v : null;
313 22
        if ($value == '0') {
314 15
            return false;
315 18
        } elseif ($value == '1') {
316 18
            return true;
317
        }
318
319
        return (bool) $c->v;
320
    }
321
322 189
    private static function castToError(?SimpleXMLElement $c): ?string
323
    {
324 189
        return isset($c, $c->v) ? (string) $c->v : null;
325
    }
326
327 547
    private static function castToString(?SimpleXMLElement $c): ?string
328
    {
329 547
        return isset($c, $c->v) ? (string) $c->v : null;
330
    }
331
332 379
    public static function replacePrefixes(string $formula): string
333
    {
334 379
        return str_replace(['_xlfn.', '_xlws.'], '', $formula);
335
    }
336
337 369
    private function castToFormula(?SimpleXMLElement $c, string $r, string &$cellDataType, mixed &$value, mixed &$calculatedValue, string $castBaseType, bool $updateSharedCells = true): void
338
    {
339 369
        if ($c === null) {
340
            return;
341
        }
342 369
        $attr = $c->f->attributes();
343 369
        $cellDataType = DataType::TYPE_FORMULA;
344 369
        $formula = self::replacePrefixes((string) $c->f);
345 369
        $value = "=$formula";
346 369
        $calculatedValue = self::$castBaseType($c);
347
348
        // Shared formula?
349 369
        if (isset($attr['t']) && strtolower((string) $attr['t']) == 'shared') {
350 219
            $instance = (string) $attr['si'];
351
352 219
            if (!isset($this->sharedFormulae[(string) $attr['si']])) {
353 219
                $this->sharedFormulae[$instance] = new SharedFormula($r, $value);
354 218
            } elseif ($updateSharedCells === true) {
355
                // It's only worth the overhead of adjusting the shared formula for this cell if we're actually loading
356
                //     the cell, which may not be the case if we're using a read filter.
357 218
                $master = Coordinate::indexesFromString($this->sharedFormulae[$instance]->master());
358 218
                $current = Coordinate::indexesFromString($r);
359
360 218
                $difference = [0, 0];
361 218
                $difference[0] = $current[0] - $master[0];
362 218
                $difference[1] = $current[1] - $master[1];
363
364 218
                $value = $this->referenceHelper->updateFormulaReferences($this->sharedFormulae[$instance]->formula(), 'A1', $difference[0], $difference[1]);
365
            }
366
        }
367
    }
368
369 685
    private function fileExistsInArchive(ZipArchive $archive, string $fileName = ''): bool
370
    {
371
        // Root-relative paths
372 685
        if (str_contains($fileName, '//')) {
373 1
            $fileName = substr($fileName, strpos($fileName, '//') + 1);
374
        }
375 685
        $fileName = File::realpath($fileName);
376
377
        // Sadly, some 3rd party xlsx generators don't use consistent case for filenaming
378
        //    so we need to load case-insensitively from the zip file
379
380
        // Apache POI fixes
381 685
        $contents = $archive->locateName($fileName, ZipArchive::FL_NOCASE);
382 685
        if ($contents === false) {
383 4
            $contents = $archive->locateName(substr($fileName, 1), ZipArchive::FL_NOCASE);
384
        }
385
386 685
        return $contents !== false;
387
    }
388
389 730
    private function getFromZipArchive(ZipArchive $archive, string $fileName = ''): string
390
    {
391
        // Root-relative paths
392 730
        if (str_contains($fileName, '//')) {
393 2
            $fileName = substr($fileName, strpos($fileName, '//') + 1);
394
        }
395
        // Relative paths generated by dirname($filename) when $filename
396
        // has no path (i.e.files in root of the zip archive)
397 730
        $fileName = (string) preg_replace('/^\.\//', '', $fileName);
398 730
        $fileName = File::realpath($fileName);
399
400
        // Sadly, some 3rd party xlsx generators don't use consistent case for filenaming
401
        //    so we need to load case-insensitively from the zip file
402
403 730
        $contents = $archive->getFromName($fileName, 0, ZipArchive::FL_NOCASE);
404
405
        // Apache POI fixes
406 730
        if ($contents === false) {
407 53
            $contents = $archive->getFromName(substr($fileName, 1), 0, ZipArchive::FL_NOCASE);
408
        }
409
410
        // Has the file been saved with Windoze directory separators rather than unix?
411 730
        if ($contents === false) {
412 50
            $contents = $archive->getFromName(str_replace('/', '\\', $fileName), 0, ZipArchive::FL_NOCASE);
413
        }
414
415 730
        return ($contents === false) ? '' : $contents;
416
    }
417
418
    /**
419
     * Loads Spreadsheet from file.
420
     */
421 703
    protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
422
    {
423 703
        File::assertFile($filename, self::INITIAL_FILE);
424
425
        // Initialisations
426 700
        $excel = $this->newSpreadsheet();
427 700
        $excel->setValueBinder($this->valueBinder);
428 700
        $excel->removeSheetByIndex(0);
429 700
        $addingFirstCellStyleXf = true;
430 700
        $addingFirstCellXf = true;
431
432
        /** @var mixed[][][][] */
433 700
        $unparsedLoadedData = [];
434
435 700
        $this->zip = $zip = new ZipArchive();
436 700
        $zip->open($filename);
437
438
        //    Read the theme first, because we need the colour scheme when reading the styles
439 700
        [$workbookBasename, $xmlNamespaceBase] = $this->getWorkbookBaseName();
440 700
        $drawingNS = self::REL_TO_DRAWING[$xmlNamespaceBase] ?? Namespaces::DRAWINGML;
441 700
        $chartNS = self::REL_TO_CHART[$xmlNamespaceBase] ?? Namespaces::CHART;
442 700
        $wbRels = $this->loadZip("xl/_rels/{$workbookBasename}.rels", Namespaces::RELATIONSHIPS);
443 700
        $theme = null;
444 700
        $this->styleReader = new Styles();
445 700
        foreach ($wbRels->Relationship as $relx) {
446 699
            $rel = self::getAttributes($relx);
447 699
            $relTarget = (string) $rel['Target'];
448 699
            if (str_starts_with($relTarget, '/xl/')) {
449 12
                $relTarget = substr($relTarget, 4);
450
            }
451 699
            switch ($rel['Type']) {
452 699
                case "$xmlNamespaceBase/sheetMetadata":
453 32
                    if ($this->fileExistsInArchive($zip, "xl/{$relTarget}")) {
454 32
                        $excel->returnArrayAsArray();
455
                    }
456
457 32
                    break;
458 699
                case "$xmlNamespaceBase/theme":
459 683
                    if (!$this->fileExistsInArchive($zip, "xl/{$relTarget}")) {
460 3
                        break; // issue3770
461
                    }
462 680
                    $themeOrderArray = ['lt1', 'dk1', 'lt2', 'dk2'];
463 680
                    $themeOrderAdditional = count($themeOrderArray);
464
465 680
                    $xmlTheme = $this->loadZip("xl/{$relTarget}", $drawingNS);
466 680
                    $xmlThemeName = self::getAttributes($xmlTheme);
467 680
                    $xmlTheme = $xmlTheme->children($drawingNS);
468 680
                    $themeName = (string) $xmlThemeName['name'];
469
470 680
                    $colourScheme = self::getAttributes($xmlTheme->themeElements->clrScheme);
471 680
                    $colourSchemeName = (string) $colourScheme['name'];
472 680
                    $excel->getTheme()->setThemeColorName($colourSchemeName);
473 680
                    $colourScheme = $xmlTheme->themeElements->clrScheme->children($drawingNS);
474
475 680
                    $themeColours = [];
476 680
                    foreach ($colourScheme as $k => $xmlColour) {
477 680
                        $themePos = array_search($k, $themeOrderArray);
478 680
                        if ($themePos === false) {
479 680
                            $themePos = $themeOrderAdditional++;
480
                        }
481 680
                        if (isset($xmlColour->sysClr)) {
482 660
                            $xmlColourData = self::getAttributes($xmlColour->sysClr);
483 660
                            $themeColours[$themePos] = (string) $xmlColourData['lastClr'];
484 660
                            $excel->getTheme()->setThemeColor($k, (string) $xmlColourData['lastClr']);
485 680
                        } elseif (isset($xmlColour->srgbClr)) {
486 680
                            $xmlColourData = self::getAttributes($xmlColour->srgbClr);
487 680
                            $themeColours[$themePos] = (string) $xmlColourData['val'];
488 680
                            $excel->getTheme()->setThemeColor($k, (string) $xmlColourData['val']);
489
                        }
490
                    }
491 680
                    $theme = new Theme($themeName, $colourSchemeName, $themeColours);
492 680
                    $this->styleReader->setTheme($theme);
493
494 680
                    $fontScheme = self::getAttributes($xmlTheme->themeElements->fontScheme);
495 680
                    $fontSchemeName = (string) $fontScheme['name'];
496 680
                    $excel->getTheme()->setThemeFontName($fontSchemeName);
497 680
                    $majorFonts = [];
498 680
                    $minorFonts = [];
499 680
                    $fontScheme = $xmlTheme->themeElements->fontScheme->children($drawingNS);
500 680
                    $majorLatin = self::getAttributes($fontScheme->majorFont->latin)['typeface'] ?? '';
501 680
                    $majorEastAsian = self::getAttributes($fontScheme->majorFont->ea)['typeface'] ?? '';
502 680
                    $majorComplexScript = self::getAttributes($fontScheme->majorFont->cs)['typeface'] ?? '';
503 680
                    $minorLatin = self::getAttributes($fontScheme->minorFont->latin)['typeface'] ?? '';
504 680
                    $minorEastAsian = self::getAttributes($fontScheme->minorFont->ea)['typeface'] ?? '';
505 680
                    $minorComplexScript = self::getAttributes($fontScheme->minorFont->cs)['typeface'] ?? '';
506
507 680
                    foreach ($fontScheme->majorFont->font as $xmlFont) {
508 658
                        $fontAttributes = self::getAttributes($xmlFont);
509 658
                        $script = (string) ($fontAttributes['script'] ?? '');
510 658
                        if (!empty($script)) {
511 658
                            $majorFonts[$script] = (string) ($fontAttributes['typeface'] ?? '');
512
                        }
513
                    }
514 680
                    foreach ($fontScheme->minorFont->font as $xmlFont) {
515 658
                        $fontAttributes = self::getAttributes($xmlFont);
516 658
                        $script = (string) ($fontAttributes['script'] ?? '');
517 658
                        if (!empty($script)) {
518 658
                            $minorFonts[$script] = (string) ($fontAttributes['typeface'] ?? '');
519
                        }
520
                    }
521 680
                    $excel->getTheme()->setMajorFontValues($majorLatin, $majorEastAsian, $majorComplexScript, $majorFonts);
522 680
                    $excel->getTheme()->setMinorFontValues($minorLatin, $minorEastAsian, $minorComplexScript, $minorFonts);
523
524 680
                    break;
525
            }
526
        }
527
528 700
        $rels = $this->loadZip(self::INITIAL_FILE, Namespaces::RELATIONSHIPS);
529
530 700
        $propertyReader = new PropertyReader($this->getSecurityScannerOrThrow(), $excel->getProperties());
531 700
        $charts = $chartDetails = [];
532 700
        foreach ($rels->Relationship as $relx) {
533 700
            $rel = self::getAttributes($relx);
534 700
            $relTarget = (string) $rel['Target'];
535
            // issue 3553
536 700
            if ($relTarget[0] === '/') {
537 7
                $relTarget = substr($relTarget, 1);
538
            }
539 700
            $relType = (string) $rel['Type'];
540 700
            $mainNS = self::REL_TO_MAIN[$relType] ?? Namespaces::MAIN;
541
            switch ($relType) {
542 693
                case Namespaces::CORE_PROPERTIES:
543 680
                    $propertyReader->readCoreProperties($this->getFromZipArchive($zip, $relTarget));
544
545 680
                    break;
546 700
                case "$xmlNamespaceBase/extended-properties":
547 671
                    $propertyReader->readExtendedProperties($this->getFromZipArchive($zip, $relTarget));
548
549 671
                    break;
550 700
                case "$xmlNamespaceBase/custom-properties":
551 59
                    $propertyReader->readCustomProperties($this->getFromZipArchive($zip, $relTarget));
552
553 59
                    break;
554
                    //Ribbon
555 700
                case Namespaces::EXTENSIBILITY:
556 2
                    $customUI = $relTarget;
557 2
                    if ($customUI) {
558 2
                        $this->readRibbon($excel, $customUI, $zip);
559
                    }
560
561 2
                    break;
562 700
                case "$xmlNamespaceBase/officeDocument":
563 700
                    $dir = dirname($relTarget);
564
565
                    // Do not specify namespace in next stmt - do it in Xpath
566 700
                    $relsWorkbook = $this->loadZip("$dir/_rels/" . basename($relTarget) . '.rels', Namespaces::RELATIONSHIPS);
567 700
                    $relsWorkbook->registerXPathNamespace('rel', Namespaces::RELATIONSHIPS);
568
569 700
                    $worksheets = [];
570 700
                    $macros = $customUI = null;
571 700
                    foreach ($relsWorkbook->Relationship as $elex) {
572 700
                        $ele = self::getAttributes($elex);
573 700
                        switch ($ele['Type']) {
574 693
                            case Namespaces::WORKSHEET:
575 693
                            case Namespaces::PURL_WORKSHEET:
576 700
                                $worksheets[(string) $ele['Id']] = $ele['Target'];
577
578 700
                                break;
579 693
                            case Namespaces::CHARTSHEET:
580 2
                                if ($this->includeCharts === true) {
581 1
                                    $worksheets[(string) $ele['Id']] = $ele['Target'];
582
                                }
583
584 2
                                break;
585
                                // a vbaProject ? (: some macros)
586 693
                            case Namespaces::VBA:
587 3
                                $macros = $ele['Target'];
588
589 3
                                break;
590
                        }
591
                    }
592
593 700
                    if ($macros !== null) {
594 3
                        $macrosCode = $this->getFromZipArchive($zip, 'xl/vbaProject.bin'); //vbaProject.bin always in 'xl' dir and always named vbaProject.bin
595 3
                        if (!empty($macrosCode)) {
596 3
                            $excel->setMacrosCode($macrosCode);
597 3
                            $excel->setHasMacros(true);
598
                            //short-circuit : not reading vbaProject.bin.rel to get Signature =>allways vbaProjectSignature.bin in 'xl' dir
599 3
                            $Certificate = $this->getFromZipArchive($zip, 'xl/vbaProjectSignature.bin');
600 3
                            $excel->setMacrosCertificate($Certificate);
601
                        }
602
                    }
603
604 700
                    $relType = "rel:Relationship[@Type='"
605 700
                        . "$xmlNamespaceBase/styles"
606 700
                        . "']";
607
                    /** @var ?SimpleXMLElement */
608 700
                    $xpath = self::getArrayItem(self::xpathNoFalse($relsWorkbook, $relType));
609
610 700
                    if ($xpath === null) {
611 1
                        $xmlStyles = self::testSimpleXml(null);
612
                    } else {
613 700
                        $stylesTarget = (string) $xpath['Target'];
614 700
                        $stylesTarget = str_starts_with($stylesTarget, '/') ? substr($stylesTarget, 1) : "$dir/$stylesTarget";
615 700
                        $xmlStyles = $this->loadZip($stylesTarget, $mainNS);
616
                    }
617
618 700
                    $palette = self::extractPalette($xmlStyles);
619 700
                    $this->styleReader->setWorkbookPalette($palette);
620 700
                    $fills = self::extractStyles($xmlStyles, 'fills', 'fill');
621 700
                    $fonts = self::extractStyles($xmlStyles, 'fonts', 'font');
622 700
                    $borders = self::extractStyles($xmlStyles, 'borders', 'border');
623 700
                    $xfTags = self::extractStyles($xmlStyles, 'cellXfs', 'xf');
624 700
                    $cellXfTags = self::extractStyles($xmlStyles, 'cellStyleXfs', 'xf');
625
626 700
                    $styles = [];
627 700
                    $cellStyles = [];
628 700
                    $numFmts = null;
629 700
                    if (/*$xmlStyles && */ $xmlStyles->numFmts[0]) {
630 249
                        $numFmts = $xmlStyles->numFmts[0];
631
                    }
632 700
                    if (isset($numFmts)) {
633
                        /** @var SimpleXMLElement $numFmts */
634 249
                        $numFmts->registerXPathNamespace('sml', $mainNS);
635
                    }
636 700
                    $this->styleReader->setNamespace($mainNS);
637 700
                    if (!$this->readDataOnly/* && $xmlStyles*/) {
638 697
                        foreach ($xfTags as $xfTag) {
639
                            /** @var SimpleXMLElement $xfTag */
640 697
                            $xf = self::getAttributes($xfTag);
641 697
                            $numFmt = null;
642
643 697
                            if ($xf['numFmtId']) {
644 695
                                if (isset($numFmts)) {
645
                                    /** @var ?SimpleXMLElement */
646 249
                                    $tmpNumFmt = self::getArrayItem($numFmts->xpath("sml:numFmt[@numFmtId=$xf[numFmtId]]"));
647
648 249
                                    if (isset($tmpNumFmt['formatCode'])) {
649 248
                                        $numFmt = (string) $tmpNumFmt['formatCode'];
650
                                    }
651
                                }
652
653
                                // We shouldn't override any of the built-in MS Excel values (values below id 164)
654
                                //  But there's a lot of naughty homebrew xlsx writers that do use "reserved" id values that aren't actually used
655
                                //  So we make allowance for them rather than lose formatting masks
656
                                if (
657 695
                                    $numFmt === null
658 695
                                    && (int) $xf['numFmtId'] < 164
659 695
                                    && NumberFormat::builtInFormatCode((int) $xf['numFmtId']) !== ''
660
                                ) {
661 684
                                    $numFmt = NumberFormat::builtInFormatCode((int) $xf['numFmtId']);
662
                                }
663
                            }
664 697
                            $quotePrefix = (bool) (string) ($xf['quotePrefix'] ?? '');
665
666 697
                            $style = (object) [
667 697
                                'numFmt' => $numFmt ?? NumberFormat::FORMAT_GENERAL,
668 697
                                'font' => $fonts[(int) ($xf['fontId'])],
669 697
                                'fill' => $fills[(int) ($xf['fillId'])],
670 697
                                'border' => $borders[(int) ($xf['borderId'])],
671 697
                                'alignment' => $xfTag->alignment,
672 697
                                'protection' => $xfTag->protection,
673 697
                                'quotePrefix' => $quotePrefix,
674 697
                            ];
675 697
                            $styles[] = $style;
676
677
                            // add style to cellXf collection
678 697
                            $objStyle = new Style();
679 697
                            $this->styleReader
680 697
                                ->readStyle($objStyle, $style);
681 697
                            foreach ($this->styleReader->getFontCharsets() as $fontName => $charset) {
682 21
                                $excel->addFontCharset($fontName, $charset);
683
                            }
684 697
                            if ($addingFirstCellXf) {
685 697
                                $excel->removeCellXfByIndex(0); // remove the default style
686 697
                                $addingFirstCellXf = false;
687
                            }
688 697
                            $excel->addCellXf($objStyle);
689
                        }
690
691 697
                        foreach ($cellXfTags as $xfTag) {
692
                            /** @var SimpleXMLElement $xfTag */
693 696
                            $xf = self::getAttributes($xfTag);
694 696
                            $numFmt = NumberFormat::FORMAT_GENERAL;
695 696
                            if ($numFmts && $xf['numFmtId']) {
696
                                /** @var ?SimpleXMLElement */
697 249
                                $tmpNumFmt = self::getArrayItem($numFmts->xpath("sml:numFmt[@numFmtId=$xf[numFmtId]]"));
698 249
                                if (isset($tmpNumFmt['formatCode'])) {
699 29
                                    $numFmt = (string) $tmpNumFmt['formatCode'];
700 247
                                } elseif ((int) $xf['numFmtId'] < 165) {
701 247
                                    $numFmt = NumberFormat::builtInFormatCode((int) $xf['numFmtId']);
702
                                }
703
                            }
704
705 696
                            $quotePrefix = (bool) (string) ($xf['quotePrefix'] ?? '');
706
707 696
                            $cellStyle = (object) [
708 696
                                'numFmt' => $numFmt,
709 696
                                'font' => $fonts[(int) ($xf['fontId'])],
710 696
                                'fill' => $fills[((int) $xf['fillId'])],
711 696
                                'border' => $borders[(int) ($xf['borderId'])],
712 696
                                'alignment' => $xfTag->alignment,
713 696
                                'protection' => $xfTag->protection,
714 696
                                'quotePrefix' => $quotePrefix,
715 696
                            ];
716 696
                            $cellStyles[] = $cellStyle;
717
718
                            // add style to cellStyleXf collection
719 696
                            $objStyle = new Style();
720 696
                            $this->styleReader->readStyle($objStyle, $cellStyle);
721 696
                            if ($addingFirstCellStyleXf) {
722 696
                                $excel->removeCellStyleXfByIndex(0); // remove the default style
723 696
                                $addingFirstCellStyleXf = false;
724
                            }
725 696
                            $excel->addCellStyleXf($objStyle);
726
                        }
727
                    }
728 700
                    $this->styleReader->setStyleXml($xmlStyles);
729 700
                    $this->styleReader->setNamespace($mainNS);
730 700
                    $this->styleReader->setStyleBaseData($theme, $styles, $cellStyles);
731 700
                    $dxfs = $this->styleReader->dxfs($this->readDataOnly);
732 700
                    $tableStyles = $this->styleReader->tableStyles($this->readDataOnly);
733 700
                    $styles = $this->styleReader->styles();
734
735
                    // Read content after setting the styles
736 700
                    $sharedStrings = [];
737 700
                    $relType = "rel:Relationship[@Type='"
738 700
                        //. Namespaces::SHARED_STRINGS
739 700
                        . "$xmlNamespaceBase/sharedStrings"
740 700
                        . "']";
741
                    /** @var ?SimpleXMLElement */
742 700
                    $xpath = self::getArrayItem($relsWorkbook->xpath($relType));
743
744 700
                    if ($xpath) {
745 641
                        $sharedStringsTarget = (string) $xpath['Target'];
746 641
                        $sharedStringsTarget = str_starts_with($sharedStringsTarget, '/') ? substr($sharedStringsTarget, 1) : "$dir/$sharedStringsTarget";
747 641
                        $xmlStrings = $this->loadZip($sharedStringsTarget, $mainNS);
748 639
                        if (isset($xmlStrings->si)) {
749 509
                            foreach ($xmlStrings->si as $val) {
750 509
                                if (isset($val->t)) {
751 505
                                    $sharedStrings[] = StringHelper::controlCharacterOOXML2PHP((string) $val->t);
752 42
                                } elseif (isset($val->r)) {
753 42
                                    $sharedStrings[] = $this->parseRichText($val);
754
                                } else {
755 1
                                    $sharedStrings[] = '';
756
                                }
757
                            }
758
                        }
759
                    }
760
761 698
                    $xmlWorkbook = $this->loadZipNoNamespace($relTarget, $mainNS);
762 693
                    $xmlWorkbookNS = $this->loadZip($relTarget, $mainNS);
763
764
                    // Set base date
765 693
                    $excel->setExcelCalendar(Date::CALENDAR_WINDOWS_1900);
766 693
                    if ($xmlWorkbookNS->workbookPr) {
767 684
                        Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900);
768 684
                        $attrs1904 = self::getAttributes($xmlWorkbookNS->workbookPr);
769 684
                        if (isset($attrs1904['date1904'])) {
770 14
                            if (self::boolean((string) $attrs1904['date1904'])) {
771 3
                                Date::setExcelCalendar(Date::CALENDAR_MAC_1904);
772 3
                                $excel->setExcelCalendar(Date::CALENDAR_MAC_1904);
773
                            }
774
                        }
775
                    }
776
777
                    // Set protection
778 693
                    $this->readProtection($excel, $xmlWorkbook);
779
780 693
                    $sheetId = 0; // keep track of new sheet id in final workbook
781 693
                    $oldSheetId = -1; // keep track of old sheet id in final workbook
782 693
                    $countSkippedSheets = 0; // keep track of number of skipped sheets
783 693
                    $mapSheetId = []; // mapping of sheet ids from old to new
784
785 693
                    $charts = $chartDetails = [];
786
787 693
                    if ($xmlWorkbookNS->sheets) {
788 693
                        foreach ($xmlWorkbookNS->sheets->sheet as $eleSheet) {
789 693
                            $eleSheetAttr = self::getAttributes($eleSheet);
790 693
                            ++$oldSheetId;
791
792
                            // Check if sheet should be skipped
793 693
                            if (is_array($this->loadSheetsOnly) && !in_array((string) $eleSheetAttr['name'], $this->loadSheetsOnly)) {
794 6
                                ++$countSkippedSheets;
795 6
                                $mapSheetId[$oldSheetId] = null;
796
797 6
                                continue;
798
                            }
799
800 692
                            $sheetReferenceId = self::getArrayItemString(self::getAttributes($eleSheet, $xmlNamespaceBase), 'id');
801 692
                            if (isset($worksheets[$sheetReferenceId]) === false) {
802 1
                                ++$countSkippedSheets;
803 1
                                $mapSheetId[$oldSheetId] = null;
804
805 1
                                continue;
806
                            }
807
                            // Map old sheet id in original workbook to new sheet id.
808
                            // They will differ if loadSheetsOnly() is being used
809 692
                            $mapSheetId[$oldSheetId] = $oldSheetId - $countSkippedSheets;
810
811
                            // Load sheet
812 692
                            $docSheet = $excel->createSheet();
813
                            //    Use false for $updateFormulaCellReferences to prevent adjustment of worksheet
814
                            //        references in formula cells... during the load, all formulae should be correct,
815
                            //        and we're simply bringing the worksheet name in line with the formula, not the
816
                            //        reverse
817 692
                            $docSheet->setTitle((string) $eleSheetAttr['name'], false, false);
818
819 692
                            $fileWorksheet = (string) $worksheets[$sheetReferenceId];
820
                            // issue 3665 adds test for /.
821
                            // This broke XlsxRootZipFilesTest,
822
                            //  but Excel reports an error with that file.
823
                            //  Testing dir for . avoids this problem.
824
                            //  It might be better just to drop the test.
825 692
                            if ($fileWorksheet[0] == '/' && $dir !== '.') {
826 12
                                $fileWorksheet = substr($fileWorksheet, strlen($dir) + 2);
827
                            }
828 692
                            $xmlSheet = $this->loadZipNoNamespace("$dir/$fileWorksheet", $mainNS);
829 692
                            $xmlSheetNS = $this->loadZip("$dir/$fileWorksheet", $mainNS);
830
831
                            // Shared Formula table is unique to each Worksheet, so we need to reset it here
832 692
                            $this->sharedFormulae = [];
833
834 692
                            if (isset($eleSheetAttr['state']) && (string) $eleSheetAttr['state'] != '') {
835 39
                                $docSheet->setSheetState((string) $eleSheetAttr['state']);
836
                            }
837 692
                            if ($xmlSheetNS) {
838 692
                                $xmlSheetMain = $xmlSheetNS->children($mainNS);
839
                                // Setting Conditional Styles adjusts selected cells, so we need to execute this
840
                                //    before reading the sheet view data to get the actual selected cells
841 692
                                if (!$this->readDataOnly && ($xmlSheet->conditionalFormatting)) {
842 223
                                    (new ConditionalStyles($docSheet, $xmlSheet, $dxfs, $this->styleReader))->load();
843
                                }
844 692
                                if (!$this->readDataOnly && $xmlSheet->extLst) {
845 201
                                    (new ConditionalStyles($docSheet, $xmlSheet, $dxfs, $this->styleReader))->loadFromExt();
846
                                }
847 692
                                if (isset($xmlSheetMain->sheetViews, $xmlSheetMain->sheetViews->sheetView)) {
848 689
                                    $sheetViews = new SheetViews($xmlSheetMain->sheetViews->sheetView, $docSheet);
849 689
                                    $sheetViews->load();
850
                                }
851
852 692
                                $sheetViewOptions = new SheetViewOptions($docSheet, $xmlSheetNS);
853 692
                                $sheetViewOptions->load($this->readDataOnly, $this->styleReader);
854
855 692
                                (new ColumnAndRowAttributes($docSheet, $xmlSheetNS))
856 692
                                    ->load($this->getReadFilter(), $this->readDataOnly, $this->ignoreRowsWithNoCells);
857
                            }
858
859 692
                            $holdSelectedCells = $docSheet->getSelectedCells();
860 692
                            if ($xmlSheetNS && $xmlSheetNS->sheetData && $xmlSheetNS->sheetData->row) {
861 646
                                $cIndex = 1; // Cell Start from 1
862 646
                                foreach ($xmlSheetNS->sheetData->row as $row) {
863 646
                                    $rowIndex = 1;
864 646
                                    foreach ($row->c as $c) {
865 645
                                        $cAttr = self::getAttributes($c);
866 645
                                        $r = (string) $cAttr['r'];
867 645
                                        if ($r == '') {
868 2
                                            $r = Coordinate::stringFromColumnIndex($rowIndex) . $cIndex;
869
                                        }
870 645
                                        $cellDataType = (string) $cAttr['t'];
871 645
                                        $originalCellDataTypeNumeric = $cellDataType === '';
872 645
                                        $value = null;
873 645
                                        $calculatedValue = null;
874
875
                                        // Read cell?
876 645
                                        $coordinates = Coordinate::coordinateFromString($r);
877
878 645
                                        if (!$this->getReadFilter()->readCell($coordinates[0], (int) $coordinates[1], $docSheet->getTitle())) {
879
                                            // Normally, just testing for the f attribute should identify this cell as containing a formula
880
                                            // that we need to read, even though it is outside of the filter range, in case it is a shared formula.
881
                                            // But in some cases, this attribute isn't set; so we need to delve a level deeper and look at
882
                                            // whether or not the cell has a child formula element that is shared.
883 4
                                            if (isset($cAttr->f) || (isset($c->f, $c->f->attributes()['t']) && strtolower((string) $c->f->attributes()['t']) === 'shared')) {
884
                                                $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToError', false);
885
                                            }
886 4
                                            ++$rowIndex;
887
888 4
                                            continue;
889
                                        }
890
891
                                        // Read cell!
892 645
                                        $useFormula = isset($c->f)
893 645
                                            && ((string) $c->f !== '' || (isset($c->f->attributes()['t']) && strtolower((string) $c->f->attributes()['t']) === 'shared'));
894
                                        switch ($cellDataType) {
895 22
                                            case DataType::TYPE_STRING:
896 507
                                                if ((string) $c->v != '') {
897 507
                                                    $value = $sharedStrings[(int) ($c->v)];
898
899 507
                                                    if ($value instanceof RichText) {
900 37
                                                        $value = clone $value;
901
                                                    }
902
                                                } else {
903 16
                                                    $value = '';
904
                                                }
905
906 507
                                                break;
907 18
                                            case DataType::TYPE_BOOL:
908 22
                                                if (!$useFormula) {
909 16
                                                    if (isset($c->v)) {
910 16
                                                        $value = self::castToBoolean($c);
911
                                                    } else {
912 1
                                                        $value = null;
913 1
                                                        $cellDataType = DataType::TYPE_NULL;
914
                                                    }
915
                                                } else {
916
                                                    // Formula
917 6
                                                    $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToBoolean');
918 6
                                                    self::storeFormulaAttributes($c->f, $docSheet, $r);
919
                                                }
920
921 22
                                                break;
922 18
                                            case DataType::TYPE_STRING2:
923 223
                                                if ($useFormula) {
924 221
                                                    $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToString');
925 221
                                                    self::storeFormulaAttributes($c->f, $docSheet, $r);
926
                                                } else {
927 3
                                                    $value = self::castToString($c);
928
                                                }
929
930 223
                                                break;
931 18
                                            case DataType::TYPE_INLINE:
932 15
                                                if ($useFormula) {
933
                                                    $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToError');
934
                                                    self::storeFormulaAttributes($c->f, $docSheet, $r);
935
                                                } else {
936 15
                                                    $value = $this->parseRichText($c->is);
937
                                                }
938
939 15
                                                break;
940 18
                                            case DataType::TYPE_ERROR:
941 189
                                                if (!$useFormula) {
942
                                                    $value = self::castToError($c);
943
                                                } else {
944
                                                    // Formula
945 189
                                                    $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToError');
946 189
                                                    $eattr = $c->attributes();
947 189
                                                    if (isset($eattr['vm'])) {
948 1
                                                        if ($calculatedValue === ExcelError::VALUE()) {
949 1
                                                            $calculatedValue = ExcelError::SPILL();
950
                                                        }
951
                                                    }
952
                                                }
953
954 189
                                                break;
955
                                            default:
956 536
                                                if (!$useFormula) {
957 528
                                                    $value = self::castToString($c);
958 528
                                                    if (is_numeric($value)) {
959 502
                                                        $value += 0;
960 502
                                                        $cellDataType = DataType::TYPE_NUMERIC;
961
                                                    }
962
                                                } else {
963
                                                    // Formula
964 347
                                                    $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToString');
965 347
                                                    if (is_numeric($calculatedValue)) {
966 344
                                                        $calculatedValue += 0;
967
                                                    }
968 347
                                                    self::storeFormulaAttributes($c->f, $docSheet, $r);
969
                                                }
970
971 536
                                                break;
972
                                        }
973
974
                                        // read empty cells or the cells are not empty
975 645
                                        if ($this->readEmptyCells || ($value !== null && $value !== '')) {
976
                                            // Rich text?
977 645
                                            if ($value instanceof RichText && $this->readDataOnly) {
978 1
                                                $value = $value->getPlainText();
979
                                            }
980
981 645
                                            $cell = $docSheet->getCell($r);
982
                                            // Assign value
983 645
                                            if ($cellDataType != '') {
984
                                                // it is possible, that datatype is numeric but with an empty string, which result in an error
985 637
                                                if ($cellDataType === DataType::TYPE_NUMERIC && ($value === '' || $value === null)) {
986 1
                                                    $cellDataType = DataType::TYPE_NULL;
987
                                                }
988 637
                                                if ($cellDataType !== DataType::TYPE_NULL) {
989 637
                                                    $cell->setValueExplicit($value, $cellDataType);
990
                                                }
991
                                            } else {
992 297
                                                $cell->setValue($value);
993
                                            }
994 645
                                            if ($calculatedValue !== null) {
995 360
                                                $cell->setCalculatedValue($calculatedValue, $originalCellDataTypeNumeric);
996
                                            }
997
998
                                            // Style information?
999 645
                                            if (!$this->readDataOnly) {
1000 642
                                                $cAttrS = (int) ($cAttr['s'] ?? 0);
1001
                                                // no style index means 0, it seems
1002 642
                                                $cAttrS = isset($styles[$cAttrS]) ? $cAttrS : 0;
1003 642
                                                $cell->setXfIndex($cAttrS);
1004
                                                // issue 3495
1005 642
                                                if ($cellDataType === DataType::TYPE_FORMULA && $styles[$cAttrS]->quotePrefix === true) { //* @phpstan-ignore-line
1006 2
                                                    $holdSelected = $docSheet->getSelectedCells();
1007 2
                                                    $cell->getStyle()->setQuotePrefix(false);
1008 2
                                                    $docSheet->setSelectedCells($holdSelected);
1009
                                                }
1010
                                            }
1011
                                        }
1012 645
                                        ++$rowIndex;
1013
                                    }
1014 646
                                    ++$cIndex;
1015
                                }
1016
                            }
1017 692
                            $docSheet->setSelectedCells($holdSelectedCells);
1018 692
                            if (!$this->readDataOnly && $xmlSheetNS && $xmlSheetNS->ignoredErrors) {
1019 4
                                foreach ($xmlSheetNS->ignoredErrors->ignoredError as $ignoredError) {
1020 4
                                    $this->processIgnoredErrors($ignoredError, $docSheet);
1021
                                }
1022
                            }
1023
1024 692
                            if (!$this->readDataOnly && $xmlSheetNS && $xmlSheetNS->sheetProtection) {
1025 69
                                $protAttr = $xmlSheetNS->sheetProtection->attributes() ?? [];
1026 69
                                foreach ($protAttr as $key => $value) {
1027 69
                                    $method = 'set' . ucfirst($key);
1028 69
                                    $docSheet->getProtection()->$method(self::boolean((string) $value));
1029
                                }
1030
                            }
1031
1032 692
                            if ($xmlSheet) {
1033 682
                                $this->readSheetProtection($docSheet, $xmlSheet);
1034
                            }
1035
1036 692
                            if ($this->readDataOnly === false) {
1037 689
                                $this->readAutoFilter($xmlSheetNS, $docSheet);
1038 689
                                $this->readBackgroundImage($xmlSheetNS, $docSheet, dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels');
1039
                            }
1040
1041 692
                            $this->readTables($xmlSheetNS, $docSheet, $dir, $fileWorksheet, $zip, $mainNS, $tableStyles, $dxfs);
1042
1043 692
                            if ($xmlSheetNS && $xmlSheetNS->mergeCells && $xmlSheetNS->mergeCells->mergeCell && !$this->readDataOnly) {
1044 67
                                foreach ($xmlSheetNS->mergeCells->mergeCell as $mergeCellx) {
1045 67
                                    $mergeCell = $mergeCellx->attributes();
1046 67
                                    $mergeRef = (string) ($mergeCell['ref'] ?? '');
1047 67
                                    if (str_contains($mergeRef, ':')) {
1048 67
                                        $docSheet->mergeCells($mergeRef, Worksheet::MERGE_CELL_CONTENT_HIDE);
1049
                                    }
1050
                                }
1051
                            }
1052
1053 692
                            if ($xmlSheet && !$this->readDataOnly) {
1054 679
                                $unparsedLoadedData = (new PageSetup($docSheet, $xmlSheet))->load($unparsedLoadedData);
1055
                            }
1056
1057 692
                            if (isset($xmlSheet->extLst->ext)) {
1058 201
                                foreach ($xmlSheet->extLst->ext as $extlst) {
1059 201
                                    $extAttrs = $extlst->attributes() ?? [];
1060 201
                                    $extUri = (string) ($extAttrs['uri'] ?? '');
1061 201
                                    if ($extUri !== '{CCE6A557-97BC-4b89-ADB6-D9C93CAAB3DF}') {
1062 195
                                        continue;
1063
                                    }
1064
                                    // Create dataValidations node if does not exists, maybe is better inside the foreach ?
1065 6
                                    if (!$xmlSheet->dataValidations) {
1066 1
                                        $xmlSheet->addChild('dataValidations');
1067
                                    }
1068
1069 6
                                    foreach ($extlst->children(Namespaces::DATA_VALIDATIONS1)->dataValidations->dataValidation as $item) {
1070 6
                                        $item = self::testSimpleXml($item);
1071 6
                                        $node = self::testSimpleXml($xmlSheet->dataValidations)->addChild('dataValidation');
1072 6
                                        foreach ($item->attributes() ?? [] as $attr) {
1073 6
                                            $node->addAttribute($attr->getName(), $attr);
1074
                                        }
1075 6
                                        $node->addAttribute('sqref', $item->children(Namespaces::DATA_VALIDATIONS2)->sqref);
1076 6
                                        if (isset($item->formula1)) {
1077 6
                                            $childNode = $node->addChild('formula1');
1078 6
                                            if ($childNode !== null) { // null should never happen
1079
                                                // see https://github.com/phpstan/phpstan/issues/8236
1080 6
                                                $childNode[0] = (string) $item->formula1->children(Namespaces::DATA_VALIDATIONS2)->f; // @phpstan-ignore-line
1081
                                            }
1082
                                        }
1083
                                    }
1084
                                }
1085
                            }
1086
1087 692
                            if ($xmlSheet && $xmlSheet->dataValidations && !$this->readDataOnly) {
1088 22
                                (new DataValidations($docSheet, $xmlSheet))->load();
1089
                            }
1090
1091
                            // unparsed sheet AlternateContent
1092 692
                            if ($xmlSheet && !$this->readDataOnly) {
1093 679
                                $mc = $xmlSheet->children(Namespaces::COMPATIBILITY);
1094 679
                                if ($mc->AlternateContent) {
1095 4
                                    foreach ($mc->AlternateContent as $alternateContent) {
1096 4
                                        $alternateContent = self::testSimpleXml($alternateContent);
1097
                                        /** @var mixed[][][][] $unparsedLoadedData */
1098 4
                                        $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['AlternateContents'][] = $alternateContent->asXML();
1099
                                    }
1100
                                }
1101
                            }
1102
1103
                            // Add hyperlinks
1104 692
                            if (!$this->readDataOnly) {
1105 689
                                $hyperlinkReader = new Hyperlinks($docSheet);
1106
                                // Locate hyperlink relations
1107 689
                                $relationsFileName = dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels';
1108 689
                                if ($zip->locateName($relationsFileName) !== false) {
1109 568
                                    $relsWorksheet = $this->loadZip($relationsFileName, Namespaces::RELATIONSHIPS);
1110 568
                                    $hyperlinkReader->readHyperlinks($relsWorksheet);
1111
                                }
1112
1113
                                // Loop through hyperlinks
1114 689
                                if ($xmlSheetNS && $xmlSheetNS->children($mainNS)->hyperlinks) {
1115 19
                                    $hyperlinkReader->setHyperlinks($xmlSheetNS->children($mainNS)->hyperlinks);
1116
                                }
1117
                            }
1118
1119
                            // Add comments
1120 692
                            $comments = [];
1121 692
                            $vmlComments = [];
1122 692
                            if (!$this->readDataOnly) {
1123
                                // Locate comment relations
1124 689
                                $commentRelations = dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels';
1125 689
                                if ($zip->locateName($commentRelations) !== false) {
1126 568
                                    $relsWorksheet = $this->loadZip($commentRelations, Namespaces::RELATIONSHIPS);
1127 568
                                    foreach ($relsWorksheet->Relationship as $elex) {
1128 402
                                        $ele = self::getAttributes($elex);
1129 402
                                        if ($ele['Type'] == Namespaces::COMMENTS) {
1130 34
                                            $comments[(string) $ele['Id']] = (string) $ele['Target'];
1131
                                        }
1132 402
                                        if ($ele['Type'] == Namespaces::VML) {
1133 38
                                            $vmlComments[(string) $ele['Id']] = (string) $ele['Target'];
1134
                                        }
1135
                                    }
1136
                                }
1137
1138
                                // Loop through comments
1139 689
                                foreach ($comments as $relName => $relPath) {
1140
                                    // Load comments file
1141 34
                                    $relPath = File::realpath(dirname("$dir/$fileWorksheet") . '/' . $relPath);
1142
                                    // okay to ignore namespace - using xpath
1143 34
                                    $commentsFile = $this->loadZip($relPath, '');
1144
1145
                                    // Utility variables
1146 34
                                    $authors = [];
1147 34
                                    $commentsFile->registerXpathNamespace('com', $mainNS);
1148 34
                                    $authorPath = self::xpathNoFalse($commentsFile, 'com:authors/com:author');
1149 34
                                    foreach ($authorPath as $author) {
1150
                                        /** @var SimpleXMLElement $author */
1151 34
                                        $authors[] = (string) $author;
1152
                                    }
1153
1154
                                    // Loop through contents
1155 34
                                    $contentPath = self::xpathNoFalse($commentsFile, 'com:commentList/com:comment');
1156 34
                                    foreach ($contentPath as $comment) {
1157
                                        /** @var SimpleXMLElement $comment */
1158 34
                                        $commentx = $comment->attributes();
1159
                                        /** @var array{ref: scalar, authorId?: scalar}  $commentx */
1160 34
                                        $commentModel = $docSheet->getComment((string) $commentx['ref']);
1161 34
                                        if (isset($commentx['authorId'])) {
1162 34
                                            $commentModel->setAuthor($authors[(int) $commentx['authorId']]);
1163
                                        }
1164
                                        /** @var SimpleXMLElement */
1165 34
                                        $temp = $comment->children($mainNS);
1166 34
                                        $commentModel->setText($this->parseRichText($temp->text));
1167
                                    }
1168
                                }
1169
1170
                                // later we will remove from it real vmlComments
1171 689
                                $unparsedVmlDrawings = $vmlComments;
1172 689
                                $vmlDrawingContents = [];
1173
1174
                                // Loop through VML comments
1175 689
                                foreach ($vmlComments as $relName => $relPath) {
1176
                                    // Load VML comments file
1177 38
                                    $relPath = File::realpath(dirname("$dir/$fileWorksheet") . '/' . $relPath);
1178
1179
                                    try {
1180
                                        // no namespace okay - processed with Xpath
1181 38
                                        $vmlCommentsFile = $this->loadZip($relPath, '', true);
1182 38
                                        $vmlCommentsFile->registerXPathNamespace('v', Namespaces::URN_VML);
1183
                                    } catch (Throwable) {
1184
                                        //Ignore unparsable vmlDrawings. Later they will be moved from $unparsedVmlDrawings to $unparsedLoadedData
1185
                                        continue;
1186
                                    }
1187
1188
                                    // Locate VML drawings image relations
1189 38
                                    $drowingImages = [];
1190 38
                                    $VMLDrawingsRelations = dirname($relPath) . '/_rels/' . basename($relPath) . '.rels';
1191 38
                                    $vmlDrawingContents[$relName] = $this->getSecurityScannerOrThrow()->scan($this->getFromZipArchive($zip, $relPath));
1192 38
                                    if ($zip->locateName($VMLDrawingsRelations) !== false) {
1193 18
                                        $relsVMLDrawing = $this->loadZip($VMLDrawingsRelations, Namespaces::RELATIONSHIPS);
1194 18
                                        foreach ($relsVMLDrawing->Relationship as $elex) {
1195 9
                                            $ele = self::getAttributes($elex);
1196 9
                                            if ($ele['Type'] == Namespaces::IMAGE) {
1197 9
                                                $drowingImages[(string) $ele['Id']] = (string) $ele['Target'];
1198
                                            }
1199
                                        }
1200
                                    }
1201
1202 38
                                    $shapes = self::xpathNoFalse($vmlCommentsFile, '//v:shape');
1203 38
                                    foreach ($shapes as $shape) {
1204
                                        /** @var SimpleXMLElement $shape */
1205 37
                                        $shape->registerXPathNamespace('v', Namespaces::URN_VML);
1206
1207 37
                                        if (isset($shape['style'])) {
1208 37
                                            $style = (string) $shape['style'];
1209 37
                                            $fillColor = strtoupper(substr((string) $shape['fillcolor'], 1));
1210 37
                                            $column = null;
1211 37
                                            $row = null;
1212 37
                                            $textHAlign = null;
1213 37
                                            $fillImageRelId = null;
1214 37
                                            $fillImageTitle = '';
1215
1216 37
                                            $clientData = $shape->xpath('.//x:ClientData');
1217 37
                                            $textboxDirection = '';
1218 37
                                            $textboxPath = $shape->xpath('.//v:textbox');
1219 37
                                            $textbox = (string) ($textboxPath[0]['style'] ?? '');
1220 37
                                            if (preg_match('/rtl/i', $textbox) === 1) {
1221 1
                                                $textboxDirection = Comment::TEXTBOX_DIRECTION_RTL;
1222 36
                                            } elseif (preg_match('/ltr/i', $textbox) === 1) {
1223 1
                                                $textboxDirection = Comment::TEXTBOX_DIRECTION_LTR;
1224
                                            }
1225 37
                                            if (is_array($clientData) && !empty($clientData)) {
1226
                                                /** @var SimpleXMLElement */
1227 35
                                                $clientData = $clientData[0];
1228
1229 35
                                                if (isset($clientData['ObjectType']) && (string) $clientData['ObjectType'] == 'Note') {
1230 33
                                                    $temp = $clientData->xpath('.//x:Row');
1231 33
                                                    if (is_array($temp)) {
1232 33
                                                        $row = $temp[0];
1233
                                                    }
1234
1235 33
                                                    $temp = $clientData->xpath('.//x:Column');
1236 33
                                                    if (is_array($temp)) {
1237 33
                                                        $column = $temp[0];
1238
                                                    }
1239 33
                                                    $temp = $clientData->xpath('.//x:TextHAlign');
1240 33
                                                    if (!empty($temp)) {
1241 2
                                                        $textHAlign = strtolower($temp[0]);
1242
                                                    }
1243
                                                }
1244
                                            }
1245 37
                                            $rowx = (string) $row;
1246 37
                                            $colx = (string) $column;
1247 37
                                            if (is_numeric($rowx) && is_numeric($colx) && $textHAlign !== null) {
1248 2
                                                $docSheet->getComment([1 + (int) $colx, 1 + (int) $rowx], false)->setAlignment((string) $textHAlign);
1249
                                            }
1250 37
                                            if (is_numeric($rowx) && is_numeric($colx) && $textboxDirection !== '') {
1251 2
                                                $docSheet->getComment([1 + (int) $colx, 1 + (int) $rowx], false)->setTextboxDirection($textboxDirection);
1252
                                            }
1253
1254 37
                                            $fillImageRelNode = $shape->xpath('.//v:fill/@o:relid');
1255 37
                                            if (is_array($fillImageRelNode) && !empty($fillImageRelNode)) {
1256
                                                /** @var SimpleXMLElement */
1257 5
                                                $fillImageRelNode = $fillImageRelNode[0];
1258
1259 5
                                                if (isset($fillImageRelNode['relid'])) {
1260 5
                                                    $fillImageRelId = (string) $fillImageRelNode['relid'];
1261
                                                }
1262
                                            }
1263
1264 37
                                            $fillImageTitleNode = $shape->xpath('.//v:fill/@o:title');
1265 37
                                            if (is_array($fillImageTitleNode) && !empty($fillImageTitleNode)) {
1266
                                                /** @var SimpleXMLElement */
1267 3
                                                $fillImageTitleNode = $fillImageTitleNode[0];
1268
1269 3
                                                if (isset($fillImageTitleNode['title'])) {
1270 3
                                                    $fillImageTitle = (string) $fillImageTitleNode['title'];
1271
                                                }
1272
                                            }
1273
1274 37
                                            if (($column !== null) && ($row !== null)) {
1275
                                                // Set comment properties
1276 33
                                                $comment = $docSheet->getComment([(int) $column + 1, (int) $row + 1]);
1277 33
                                                $comment->getFillColor()->setRGB($fillColor);
1278 33
                                                if (isset($drowingImages[$fillImageRelId])) {
1279 5
                                                    $objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing();
1280 5
                                                    $objDrawing->setName($fillImageTitle);
1281 5
                                                    $imagePath = str_replace(['../', '/xl/'], 'xl/', $drowingImages[$fillImageRelId]);
1282 5
                                                    $objDrawing->setPath(
1283 5
                                                        'zip://' . File::realpath($filename) . '#' . $imagePath,
1284 5
                                                        true,
1285 5
                                                        $zip
1286 5
                                                    );
1287 5
                                                    $comment->setBackgroundImage($objDrawing);
1288
                                                }
1289
1290
                                                // Parse style
1291 33
                                                $styleArray = explode(';', str_replace(' ', '', $style));
1292 33
                                                foreach ($styleArray as $stylePair) {
1293 33
                                                    $stylePair = explode(':', $stylePair);
1294
1295 33
                                                    if ($stylePair[0] == 'margin-left') {
1296 29
                                                        $comment->setMarginLeft($stylePair[1]);
1297
                                                    }
1298 33
                                                    if ($stylePair[0] == 'margin-top') {
1299 29
                                                        $comment->setMarginTop($stylePair[1]);
1300
                                                    }
1301 33
                                                    if ($stylePair[0] == 'width') {
1302 29
                                                        $comment->setWidth($stylePair[1]);
1303
                                                    }
1304 33
                                                    if ($stylePair[0] == 'height') {
1305 29
                                                        $comment->setHeight($stylePair[1]);
1306
                                                    }
1307 33
                                                    if ($stylePair[0] == 'visibility') {
1308 33
                                                        $comment->setVisible($stylePair[1] == 'visible');
1309
                                                    }
1310
                                                }
1311
1312 33
                                                unset($unparsedVmlDrawings[$relName]);
1313
                                            }
1314
                                        }
1315
                                    }
1316
                                }
1317
1318
                                // unparsed vmlDrawing
1319 689
                                if ($unparsedVmlDrawings) {
1320 7
                                    foreach ($unparsedVmlDrawings as $rId => $relPath) {
1321
                                        /** @var mixed[][][] $unparsedLoadedData */
1322 7
                                        $rId = substr($rId, 3); // rIdXXX
1323
                                        /** @var mixed[][] */
1324 7
                                        $unparsedVmlDrawing = &$unparsedLoadedData['sheets'][$docSheet->getCodeName()]['vmlDrawings'];
1325 7
                                        $unparsedVmlDrawing[$rId] = [];
1326 7
                                        $unparsedVmlDrawing[$rId]['filePath'] = self::dirAdd("$dir/$fileWorksheet", $relPath);
1327 7
                                        $unparsedVmlDrawing[$rId]['relFilePath'] = $relPath;
1328 7
                                        $unparsedVmlDrawing[$rId]['content'] = $this->getSecurityScannerOrThrow()->scan($this->getFromZipArchive($zip, $unparsedVmlDrawing[$rId]['filePath']));
1329 7
                                        unset($unparsedVmlDrawing);
1330
                                    }
1331
                                }
1332
1333
                                // Header/footer images
1334 689
                                if ($xmlSheetNS && $xmlSheetNS->legacyDrawingHF) {
1335 3
                                    $vmlHfRid = '';
1336 3
                                    $vmlHfRidAttr = $xmlSheetNS->legacyDrawingHF->attributes(Namespaces::SCHEMA_OFFICE_DOCUMENT);
1337 3
                                    if ($vmlHfRidAttr !== null && isset($vmlHfRidAttr['id'])) {
1338 3
                                        $vmlHfRid = (string) $vmlHfRidAttr['id'][0];
1339
                                    }
1340 3
                                    if ($zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels') !== false) {
1341 3
                                        $relsWorksheet = $this->loadZipNoNamespace(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels', Namespaces::RELATIONSHIPS);
1342 3
                                        $vmlRelationship = '';
1343
1344 3
                                        foreach ($relsWorksheet->Relationship as $ele) {
1345 3
                                            if ((string) $ele['Type'] == Namespaces::VML && (string) $ele['Id'] === $vmlHfRid) {
1346 3
                                                $vmlRelationship = self::dirAdd("$dir/$fileWorksheet", $ele['Target']);
1347
1348 3
                                                break;
1349
                                            }
1350
                                        }
1351
1352 3
                                        if ($vmlRelationship != '') {
1353
                                            // Fetch linked images
1354 3
                                            $relsVML = $this->loadZipNoNamespace(dirname($vmlRelationship) . '/_rels/' . basename($vmlRelationship) . '.rels', Namespaces::RELATIONSHIPS);
1355 3
                                            $drawings = [];
1356 3
                                            if (isset($relsVML->Relationship)) {
1357 3
                                                foreach ($relsVML->Relationship as $ele) {
1358 3
                                                    if ($ele['Type'] == Namespaces::IMAGE) {
1359 3
                                                        $drawings[(string) $ele['Id']] = self::dirAdd($vmlRelationship, $ele['Target']);
1360
                                                    }
1361
                                                }
1362
                                            }
1363
                                            // Fetch VML document
1364 3
                                            $vmlDrawing = $this->loadZipNoNamespace($vmlRelationship, '');
1365 3
                                            $vmlDrawing->registerXPathNamespace('v', Namespaces::URN_VML);
1366
1367 3
                                            $hfImages = [];
1368
1369 3
                                            $shapes = self::xpathNoFalse($vmlDrawing, '//v:shape');
1370 3
                                            foreach ($shapes as $idx => $shape) {
1371
                                                /** @var SimpleXMLElement $shape */
1372 3
                                                $shape->registerXPathNamespace('v', Namespaces::URN_VML);
1373 3
                                                $imageData = $shape->xpath('//v:imagedata');
1374
1375 3
                                                if (empty($imageData)) {
1376
                                                    continue;
1377
                                                }
1378
1379 3
                                                $imageData = $imageData[$idx];
1380
1381 3
                                                $imageData = self::getAttributes($imageData, Namespaces::URN_MSOFFICE);
1382
                                                /** @var array{width: int, height: int, margin-left?: int, margin-top: int} */
1383 3
                                                $style = self::toCSSArray((string) $shape['style']);
1384
1385 3
                                                if (array_key_exists((string) $imageData['relid'], $drawings)) {
1386 3
                                                    $shapeId = (string) $shape['id'];
1387 3
                                                    $hfImages[$shapeId] = new HeaderFooterDrawing();
1388 3
                                                    if (isset($imageData['title'])) {
1389 3
                                                        $hfImages[$shapeId]->setName((string) $imageData['title']);
1390
                                                    }
1391
1392 3
                                                    $hfImages[$shapeId]->setPath('zip://' . File::realpath($filename) . '#' . $drawings[(string) $imageData['relid']], false, $zip);
1393 3
                                                    $hfImages[$shapeId]->setResizeProportional(false);
1394 3
                                                    $hfImages[$shapeId]->setWidth($style['width']);
1395 3
                                                    $hfImages[$shapeId]->setHeight($style['height']);
1396 3
                                                    if (isset($style['margin-left'])) {
1397 3
                                                        $hfImages[$shapeId]->setOffsetX($style['margin-left']);
1398
                                                    }
1399 3
                                                    $hfImages[$shapeId]->setOffsetY($style['margin-top']);
1400 3
                                                    $hfImages[$shapeId]->setResizeProportional(true);
1401
                                                }
1402
                                            }
1403
1404 3
                                            $docSheet->getHeaderFooter()->setImages($hfImages);
1405
                                        }
1406
                                    }
1407
                                }
1408
                            }
1409
1410
                            // TODO: Autoshapes from twoCellAnchors!
1411 692
                            $drawingFilename = dirname("$dir/$fileWorksheet")
1412 692
                                . '/_rels/'
1413 692
                                . basename($fileWorksheet)
1414 692
                                . '.rels';
1415 692
                            if (str_starts_with($drawingFilename, 'xl//xl/')) {
1416
                                $drawingFilename = substr($drawingFilename, 4);
1417
                            }
1418 692
                            if (str_starts_with($drawingFilename, '/xl//xl/')) {
1419
                                $drawingFilename = substr($drawingFilename, 5);
1420
                            }
1421 692
                            if ($zip->locateName($drawingFilename) !== false) {
1422 571
                                $relsWorksheet = $this->loadZip($drawingFilename, Namespaces::RELATIONSHIPS);
1423 571
                                $drawings = [];
1424 571
                                foreach ($relsWorksheet->Relationship as $elex) {
1425 404
                                    $ele = self::getAttributes($elex);
1426 404
                                    if ((string) $ele['Type'] === "$xmlNamespaceBase/drawing") {
1427 133
                                        $eleTarget = (string) $ele['Target'];
1428 133
                                        if (str_starts_with($eleTarget, '/xl/')) {
1429 4
                                            $drawings[(string) $ele['Id']] = substr($eleTarget, 1);
1430
                                        } else {
1431 130
                                            $drawings[(string) $ele['Id']] = self::dirAdd("$dir/$fileWorksheet", $ele['Target']);
1432
                                        }
1433
                                    }
1434
                                }
1435
1436 571
                                if ($xmlSheetNS->drawing && !$this->readDataOnly) {
1437 132
                                    $unparsedDrawings = [];
1438 132
                                    $fileDrawing = null;
1439 132
                                    foreach ($xmlSheetNS->drawing as $drawing) {
1440 132
                                        $drawingRelId = self::getArrayItemString(self::getAttributes($drawing, $xmlNamespaceBase), 'id');
1441 132
                                        $fileDrawing = $drawings[$drawingRelId];
1442 132
                                        $drawingFilename = dirname($fileDrawing) . '/_rels/' . basename($fileDrawing) . '.rels';
1443 132
                                        $relsDrawing = $this->loadZip($drawingFilename, Namespaces::RELATIONSHIPS);
1444
1445 132
                                        $images = [];
1446 132
                                        $hyperlinks = [];
1447 132
                                        if ($relsDrawing && $relsDrawing->Relationship) {
1448 111
                                            foreach ($relsDrawing->Relationship as $elex) {
1449 111
                                                $ele = self::getAttributes($elex);
1450 111
                                                $eleType = (string) $ele['Type'];
1451 111
                                                if ($eleType === Namespaces::HYPERLINK) {
1452 3
                                                    $hyperlinks[(string) $ele['Id']] = (string) $ele['Target'];
1453
                                                }
1454 111
                                                if ($eleType === "$xmlNamespaceBase/image") {
1455 61
                                                    $eleTarget = (string) $ele['Target'];
1456 61
                                                    if (str_starts_with($eleTarget, '/xl/')) {
1457 1
                                                        $eleTarget = substr($eleTarget, 1);
1458 1
                                                        $images[(string) $ele['Id']] = $eleTarget;
1459
                                                    } else {
1460 60
                                                        $images[(string) $ele['Id']] = self::dirAdd($fileDrawing, $eleTarget);
1461
                                                    }
1462 74
                                                } elseif ($eleType === "$xmlNamespaceBase/chart") {
1463 70
                                                    if ($this->includeCharts) {
1464 69
                                                        $eleTarget = (string) $ele['Target'];
1465 69
                                                        if (str_starts_with($eleTarget, '/xl/')) {
1466 3
                                                            $index = substr($eleTarget, 1);
1467
                                                        } else {
1468 67
                                                            $index = self::dirAdd($fileDrawing, $eleTarget);
1469
                                                        }
1470 69
                                                        $charts[$index] = [
1471 69
                                                            'id' => (string) $ele['Id'],
1472 69
                                                            'sheet' => $docSheet->getTitle(),
1473 69
                                                        ];
1474
                                                    }
1475
                                                }
1476
                                            }
1477
                                        }
1478
1479 132
                                        $xmlDrawing = $this->loadZipNoNamespace($fileDrawing, '');
1480 132
                                        $xmlDrawingChildren = $xmlDrawing->children(Namespaces::SPREADSHEET_DRAWING);
1481
1482 132
                                        if ($xmlDrawingChildren->oneCellAnchor) {
1483 23
                                            foreach ($xmlDrawingChildren->oneCellAnchor as $oneCellAnchor) {
1484 23
                                                $oneCellAnchor = self::testSimpleXml($oneCellAnchor);
1485 23
                                                if ($oneCellAnchor->pic->blipFill) {
1486 16
                                                    $objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing();
1487 16
                                                    $blip = $oneCellAnchor->pic->blipFill->children(Namespaces::DRAWINGML)->blip;
1488 16
                                                    if (isset($blip, $blip->alphaModFix)) {
1489 1
                                                        $temp = (string) $blip->alphaModFix->attributes()->amt;
1490 1
                                                        if (is_numeric($temp)) {
1491 1
                                                            $objDrawing->setOpacity((int) $temp);
1492
                                                        }
1493
                                                    }
1494 16
                                                    $xfrm = $oneCellAnchor->pic->spPr->children(Namespaces::DRAWINGML)->xfrm;
1495 16
                                                    $outerShdw = $oneCellAnchor->pic->spPr->children(Namespaces::DRAWINGML)->effectLst->outerShdw;
1496
1497 16
                                                    $objDrawing->setName(self::getArrayItemString(self::getAttributes($oneCellAnchor->pic->nvPicPr->cNvPr), 'name'));
1498 16
                                                    $objDrawing->setDescription(self::getArrayItemString(self::getAttributes($oneCellAnchor->pic->nvPicPr->cNvPr), 'descr'));
1499 16
                                                    $embedImageKey = self::getArrayItemString(
1500 16
                                                        self::getAttributes($blip, $xmlNamespaceBase),
1501 16
                                                        'embed'
1502 16
                                                    );
1503 16
                                                    if (isset($images[$embedImageKey])) {
1504 16
                                                        $objDrawing->setPath(
1505 16
                                                            'zip://' . File::realpath($filename) . '#'
1506 16
                                                            . $images[$embedImageKey],
1507 16
                                                            false,
1508 16
                                                            $zip
1509 16
                                                        );
1510
                                                    } else {
1511
                                                        $linkImageKey = self::getArrayItemString(
1512
                                                            $blip->attributes('http://schemas.openxmlformats.org/officeDocument/2006/relationships'),
1513
                                                            'link'
1514
                                                        );
1515
                                                        if (isset($images[$linkImageKey])) {
1516
                                                            $url = str_replace('xl/drawings/', '', $images[$linkImageKey]);
1517
                                                            $objDrawing->setPath($url, false, allowExternal: $this->allowExternalImages);
1518
                                                        }
1519
                                                        if ($objDrawing->getPath() === '') {
1520
                                                            continue;
1521
                                                        }
1522
                                                    }
1523 16
                                                    $objDrawing->setCoordinates(Coordinate::stringFromColumnIndex(((int) $oneCellAnchor->from->col) + 1) . ($oneCellAnchor->from->row + 1));
1524
1525 16
                                                    $objDrawing->setOffsetX((int) Drawing::EMUToPixels($oneCellAnchor->from->colOff));
1526 16
                                                    $objDrawing->setOffsetY(Drawing::EMUToPixels($oneCellAnchor->from->rowOff));
1527 16
                                                    $objDrawing->setResizeProportional(false);
1528 16
                                                    $objDrawing->setWidth(Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($oneCellAnchor->ext), 'cx')));
1529 16
                                                    $objDrawing->setHeight(Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($oneCellAnchor->ext), 'cy')));
1530 16
                                                    if ($xfrm) {
1531 16
                                                        $objDrawing->setRotation((int) Drawing::angleToDegrees(self::getArrayItemIntOrSxml(self::getAttributes($xfrm), 'rot')));
1532 16
                                                        $objDrawing->setFlipVertical((bool) self::getArrayItem(self::getAttributes($xfrm), 'flipV'));
1533 16
                                                        $objDrawing->setFlipHorizontal((bool) self::getArrayItem(self::getAttributes($xfrm), 'flipH'));
1534
                                                    }
1535 16
                                                    if ($outerShdw) {
1536 3
                                                        $shadow = $objDrawing->getShadow();
1537 3
                                                        $shadow->setVisible(true);
1538 3
                                                        $shadow->setBlurRadius(Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($outerShdw), 'blurRad')));
1539 3
                                                        $shadow->setDistance(Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($outerShdw), 'dist')));
1540 3
                                                        $shadow->setDirection(Drawing::angleToDegrees(self::getArrayItemIntOrSxml(self::getAttributes($outerShdw), 'dir')));
1541 3
                                                        $shadow->setAlignment(self::getArrayItemString(self::getAttributes($outerShdw), 'algn'));
1542 3
                                                        $clr = $outerShdw->srgbClr ?? $outerShdw->prstClr;
1543 3
                                                        $shadow->getColor()->setRGB(self::getArrayItemString(self::getAttributes($clr), 'val'));
1544 3
                                                        if ($clr->alpha) {
1545 3
                                                            $alpha = StringHelper::convertToString(self::getArrayItem(self::getAttributes($clr->alpha), 'val'));
1546 3
                                                            if (is_numeric($alpha)) {
1547 3
                                                                $alpha = (int) ($alpha / 1000);
1548 3
                                                                $shadow->setAlpha($alpha);
1549
                                                            }
1550
                                                        }
1551
                                                    }
1552
1553 16
                                                    $this->readHyperLinkDrawing($objDrawing, $oneCellAnchor, $hyperlinks);
1554
1555 16
                                                    $objDrawing->setWorksheet($docSheet);
1556 7
                                                } elseif ($this->includeCharts && $oneCellAnchor->graphicFrame) {
1557
                                                    // Exported XLSX from Google Sheets positions charts with a oneCellAnchor
1558 4
                                                    $coordinates = Coordinate::stringFromColumnIndex(((int) $oneCellAnchor->from->col) + 1) . ($oneCellAnchor->from->row + 1);
1559 4
                                                    $offsetX = Drawing::EMUToPixels($oneCellAnchor->from->colOff);
1560 4
                                                    $offsetY = Drawing::EMUToPixels($oneCellAnchor->from->rowOff);
1561 4
                                                    $width = Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($oneCellAnchor->ext), 'cx'));
1562 4
                                                    $height = Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($oneCellAnchor->ext), 'cy'));
1563
1564 4
                                                    $graphic = $oneCellAnchor->graphicFrame->children(Namespaces::DRAWINGML)->graphic;
1565 4
                                                    $chartRef = $graphic->graphicData->children(Namespaces::CHART)->chart;
1566 4
                                                    $thisChart = (string) self::getAttributes($chartRef, $xmlNamespaceBase);
1567
1568 4
                                                    $chartDetails[$docSheet->getTitle() . '!' . $thisChart] = [
1569 4
                                                        'fromCoordinate' => $coordinates,
1570 4
                                                        'fromOffsetX' => $offsetX,
1571 4
                                                        'fromOffsetY' => $offsetY,
1572 4
                                                        'width' => $width,
1573 4
                                                        'height' => $height,
1574 4
                                                        'worksheetTitle' => $docSheet->getTitle(),
1575 4
                                                        'oneCellAnchor' => true,
1576 4
                                                    ];
1577
                                                }
1578
                                            }
1579
                                        }
1580 132
                                        if ($xmlDrawingChildren->twoCellAnchor) {
1581 94
                                            foreach ($xmlDrawingChildren->twoCellAnchor as $twoCellAnchor) {
1582 94
                                                $twoCellAnchor = self::testSimpleXml($twoCellAnchor);
1583 94
                                                if ($twoCellAnchor->pic->blipFill) {
1584 47
                                                    $objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing();
1585 47
                                                    $blip = $twoCellAnchor->pic->blipFill->children(Namespaces::DRAWINGML)->blip;
1586 47
                                                    if (isset($blip, $blip->alphaModFix)) {
1587 3
                                                        $temp = (string) $blip->alphaModFix->attributes()->amt;
1588 3
                                                        if (is_numeric($temp)) {
1589 3
                                                            $objDrawing->setOpacity((int) $temp);
1590
                                                        }
1591
                                                    }
1592 47
                                                    if (isset($twoCellAnchor->pic->blipFill->children(Namespaces::DRAWINGML)->srcRect)) {
1593 9
                                                        $objDrawing->setSrcRect($twoCellAnchor->pic->blipFill->children(Namespaces::DRAWINGML)->srcRect->attributes());
1594
                                                    }
1595 47
                                                    $xfrm = $twoCellAnchor->pic->spPr->children(Namespaces::DRAWINGML)->xfrm;
1596 47
                                                    $outerShdw = $twoCellAnchor->pic->spPr->children(Namespaces::DRAWINGML)->effectLst->outerShdw;
1597 47
                                                    $editAs = $twoCellAnchor->attributes();
1598 47
                                                    if (isset($editAs, $editAs['editAs'])) {
1599 42
                                                        $objDrawing->setEditAs($editAs['editAs']);
1600
                                                    }
1601 47
                                                    $objDrawing->setName((string) self::getArrayItemString(self::getAttributes($twoCellAnchor->pic->nvPicPr->cNvPr), 'name'));
1602 47
                                                    $objDrawing->setDescription(self::getArrayItemString(self::getAttributes($twoCellAnchor->pic->nvPicPr->cNvPr), 'descr'));
1603 47
                                                    $embedImageKey = self::getArrayItemString(
1604 47
                                                        self::getAttributes($blip, $xmlNamespaceBase),
1605 47
                                                        'embed'
1606 47
                                                    );
1607 47
                                                    if (isset($images[$embedImageKey])) {
1608 42
                                                        $objDrawing->setPath(
1609 42
                                                            'zip://' . File::realpath($filename) . '#'
1610 42
                                                            . $images[$embedImageKey],
1611 42
                                                            false,
1612 42
                                                            $zip
1613 42
                                                        );
1614
                                                    } else {
1615 5
                                                        $linkImageKey = self::getArrayItemString(
1616 5
                                                            $blip->attributes('http://schemas.openxmlformats.org/officeDocument/2006/relationships'),
1617 5
                                                            'link'
1618 5
                                                        );
1619 5
                                                        if (isset($images[$linkImageKey])) {
1620 5
                                                            $url = str_replace('xl/drawings/', '', $images[$linkImageKey]);
1621 5
                                                            $objDrawing->setPath($url, false, allowExternal: $this->allowExternalImages);
1622
                                                        }
1623 4
                                                        if ($objDrawing->getPath() === '') {
1624 3
                                                            continue;
1625
                                                        }
1626
                                                    }
1627 43
                                                    $objDrawing->setCoordinates(Coordinate::stringFromColumnIndex(((int) $twoCellAnchor->from->col) + 1) . ($twoCellAnchor->from->row + 1));
1628
1629 43
                                                    $objDrawing->setOffsetX(Drawing::EMUToPixels($twoCellAnchor->from->colOff));
1630 43
                                                    $objDrawing->setOffsetY(Drawing::EMUToPixels($twoCellAnchor->from->rowOff));
1631
1632 43
                                                    $objDrawing->setCoordinates2(Coordinate::stringFromColumnIndex(((int) $twoCellAnchor->to->col) + 1) . ($twoCellAnchor->to->row + 1));
1633
1634 43
                                                    $objDrawing->setOffsetX2(Drawing::EMUToPixels($twoCellAnchor->to->colOff));
1635 43
                                                    $objDrawing->setOffsetY2(Drawing::EMUToPixels($twoCellAnchor->to->rowOff));
1636
1637 43
                                                    $objDrawing->setResizeProportional(false);
1638
1639 43
                                                    if ($xfrm) {
1640 43
                                                        $objDrawing->setWidth(Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($xfrm->ext), 'cx')));
1641 43
                                                        $objDrawing->setHeight(Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($xfrm->ext), 'cy')));
1642 43
                                                        $objDrawing->setRotation(Drawing::angleToDegrees(self::getArrayItemIntOrSxml(self::getAttributes($xfrm), 'rot')));
1643 43
                                                        $objDrawing->setFlipVertical((bool) self::getArrayItem(self::getAttributes($xfrm), 'flipV'));
1644 43
                                                        $objDrawing->setFlipHorizontal((bool) self::getArrayItem(self::getAttributes($xfrm), 'flipH'));
1645
                                                    }
1646 43
                                                    if ($outerShdw) {
1647 1
                                                        $shadow = $objDrawing->getShadow();
1648 1
                                                        $shadow->setVisible(true);
1649 1
                                                        $shadow->setBlurRadius(Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($outerShdw), 'blurRad')));
1650 1
                                                        $shadow->setDistance(Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($outerShdw), 'dist')));
1651 1
                                                        $shadow->setDirection(Drawing::angleToDegrees(self::getArrayItemIntOrSxml(self::getAttributes($outerShdw), 'dir')));
1652 1
                                                        $shadow->setAlignment(self::getArrayItemString(self::getAttributes($outerShdw), 'algn'));
1653 1
                                                        $clr = $outerShdw->srgbClr ?? $outerShdw->prstClr;
1654 1
                                                        $shadow->getColor()->setRGB(self::getArrayItemString(self::getAttributes($clr), 'val'));
1655 1
                                                        if ($clr->alpha) {
1656 1
                                                            $alpha = StringHelper::convertToString(self::getArrayItem(self::getAttributes($clr->alpha), 'val'));
1657 1
                                                            if (is_numeric($alpha)) {
1658 1
                                                                $alpha = (int) ($alpha / 1000);
1659 1
                                                                $shadow->setAlpha($alpha);
1660
                                                            }
1661
                                                        }
1662
                                                    }
1663
1664 43
                                                    $this->readHyperLinkDrawing($objDrawing, $twoCellAnchor, $hyperlinks);
1665
1666 43
                                                    $objDrawing->setWorksheet($docSheet);
1667 71
                                                } elseif (($this->includeCharts) && ($twoCellAnchor->graphicFrame)) {
1668 65
                                                    $fromCoordinate = Coordinate::stringFromColumnIndex(((int) $twoCellAnchor->from->col) + 1) . ($twoCellAnchor->from->row + 1);
1669 65
                                                    $fromOffsetX = Drawing::EMUToPixels($twoCellAnchor->from->colOff);
1670 65
                                                    $fromOffsetY = Drawing::EMUToPixels($twoCellAnchor->from->rowOff);
1671 65
                                                    $toCoordinate = Coordinate::stringFromColumnIndex(((int) $twoCellAnchor->to->col) + 1) . ($twoCellAnchor->to->row + 1);
1672 65
                                                    $toOffsetX = Drawing::EMUToPixels($twoCellAnchor->to->colOff);
1673 65
                                                    $toOffsetY = Drawing::EMUToPixels($twoCellAnchor->to->rowOff);
1674 65
                                                    $graphic = $twoCellAnchor->graphicFrame->children(Namespaces::DRAWINGML)->graphic;
1675 65
                                                    $chartRef = $graphic->graphicData->children(Namespaces::CHART)->chart;
1676 65
                                                    $thisChart = (string) self::getAttributes($chartRef, $xmlNamespaceBase);
1677
1678 65
                                                    $chartDetails[$docSheet->getTitle() . '!' . $thisChart] = [
1679 65
                                                        'fromCoordinate' => $fromCoordinate,
1680 65
                                                        'fromOffsetX' => $fromOffsetX,
1681 65
                                                        'fromOffsetY' => $fromOffsetY,
1682 65
                                                        'toCoordinate' => $toCoordinate,
1683 65
                                                        'toOffsetX' => $toOffsetX,
1684 65
                                                        'toOffsetY' => $toOffsetY,
1685 65
                                                        'worksheetTitle' => $docSheet->getTitle(),
1686 65
                                                    ];
1687
                                                }
1688
                                            }
1689
                                        }
1690 131
                                        if ($xmlDrawingChildren->absoluteAnchor) {
1691 1
                                            foreach ($xmlDrawingChildren->absoluteAnchor as $absoluteAnchor) {
1692 1
                                                if (($this->includeCharts) && ($absoluteAnchor->graphicFrame)) {
1693 1
                                                    $graphic = $absoluteAnchor->graphicFrame->children(Namespaces::DRAWINGML)->graphic;
1694 1
                                                    $chartRef = $graphic->graphicData->children(Namespaces::CHART)->chart;
1695 1
                                                    $thisChart = (string) self::getAttributes($chartRef, $xmlNamespaceBase);
1696 1
                                                    $width = Drawing::EMUToPixels((int) self::getArrayItemString(self::getAttributes($absoluteAnchor->ext), 'cx')[0]);
1697 1
                                                    $height = Drawing::EMUToPixels((int) self::getArrayItemString(self::getAttributes($absoluteAnchor->ext), 'cy')[0]);
1698
1699 1
                                                    $chartDetails[$docSheet->getTitle() . '!' . $thisChart] = [
1700 1
                                                        'fromCoordinate' => 'A1',
1701 1
                                                        'fromOffsetX' => 0,
1702 1
                                                        'fromOffsetY' => 0,
1703 1
                                                        'width' => $width,
1704 1
                                                        'height' => $height,
1705 1
                                                        'worksheetTitle' => $docSheet->getTitle(),
1706 1
                                                    ];
1707
                                                }
1708
                                            }
1709
                                        }
1710 131
                                        if (empty($relsDrawing) && $xmlDrawing->count() == 0) {
1711
                                            // Save Drawing without rels and children as unparsed
1712 26
                                            $unparsedDrawings[$drawingRelId] = $xmlDrawing->asXML();
1713
                                        }
1714
                                    }
1715
1716
                                    // store original rId of drawing files
1717
                                    /** @var mixed[][][][] $unparsedLoadedData */
1718 131
                                    $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['drawingOriginalIds'] = [];
1719 131
                                    foreach ($relsWorksheet->Relationship as $elex) {
1720 131
                                        $ele = self::getAttributes($elex);
1721 131
                                        if ((string) $ele['Type'] === "$xmlNamespaceBase/drawing") {
1722 131
                                            $drawingRelId = (string) $ele['Id'];
1723 131
                                            $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['drawingOriginalIds'][(string) $ele['Target']] = $drawingRelId;
1724 131
                                            if (isset($unparsedDrawings[$drawingRelId])) {
1725 26
                                                $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['Drawings'][$drawingRelId] = $unparsedDrawings[$drawingRelId];
1726
                                            }
1727
                                        }
1728
                                    }
1729 131
                                    if ($xmlSheet->legacyDrawing && !$this->readDataOnly) {
1730 13
                                        foreach ($xmlSheet->legacyDrawing as $drawing) {
1731 13
                                            $drawingRelId = self::getArrayItemString(self::getAttributes($drawing, $xmlNamespaceBase), 'id');
1732 13
                                            if (isset($vmlDrawingContents[$drawingRelId])) {
1733 13
                                                if (self::onlyNoteVml($vmlDrawingContents[$drawingRelId]) === false) {
1734 5
                                                    $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['legacyDrawing'] = $vmlDrawingContents[$drawingRelId];
1735
                                                }
1736
                                            }
1737
                                        }
1738
                                    }
1739
1740
                                    // unparsed drawing AlternateContent
1741 131
                                    $xmlAltDrawing = $this->loadZip((string) $fileDrawing, Namespaces::COMPATIBILITY);
1742
1743 131
                                    if ($xmlAltDrawing->AlternateContent) {
1744 4
                                        foreach ($xmlAltDrawing->AlternateContent as $alternateContent) {
1745 4
                                            $alternateContent = self::testSimpleXml($alternateContent);
1746
                                            /** @var mixed[][][][][] $unparsedLoadedData */
1747 4
                                            $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['drawingAlternateContents'][] = $alternateContent->asXML();
1748
                                        }
1749
                                    }
1750
                                }
1751
                            }
1752
1753
                            /** @var mixed[][][][] $unparsedLoadedData */
1754 691
                            $this->readFormControlProperties($excel, $dir, $fileWorksheet, $docSheet, $unparsedLoadedData);
1755 691
                            $this->readPrinterSettings($excel, $dir, $fileWorksheet, $docSheet, $unparsedLoadedData);
1756
1757
                            // Loop through definedNames
1758 691
                            if ($xmlWorkbook->definedNames) {
1759 379
                                foreach ($xmlWorkbook->definedNames->definedName as $definedName) {
1760
                                    // Extract range
1761 108
                                    $extractedRange = (string) $definedName;
1762 108
                                    if (($spos = strpos($extractedRange, '!')) !== false) {
1763 89
                                        $extractedRange = substr($extractedRange, 0, $spos) . str_replace('$', '', substr($extractedRange, $spos));
1764
                                    } else {
1765 34
                                        $extractedRange = str_replace('$', '', $extractedRange);
1766
                                    }
1767
1768
                                    // Valid range?
1769 108
                                    if ($extractedRange == '') {
1770
                                        continue;
1771
                                    }
1772
1773
                                    // Some definedNames are only applicable if we are on the same sheet...
1774 108
                                    if ((string) $definedName['localSheetId'] != '' && (string) $definedName['localSheetId'] == $oldSheetId) {
1775
                                        // Switch on type
1776 50
                                        switch ((string) $definedName['name']) {
1777 50
                                            case '_xlnm._FilterDatabase':
1778 20
                                                if ((string) $definedName['hidden'] !== '1') {
1779
                                                    $extractedRange = explode(',', $extractedRange);
1780
                                                    foreach ($extractedRange as $range) {
1781
                                                        $autoFilterRange = $range;
1782
                                                        if (str_contains($autoFilterRange, ':')) {
1783
                                                            $docSheet->getAutoFilter()->setRange($autoFilterRange);
1784
                                                        }
1785
                                                    }
1786
                                                }
1787
1788 20
                                                break;
1789 30
                                            case '_xlnm.Print_Titles':
1790
                                                // Split $extractedRange
1791 3
                                                $extractedRange = explode(',', $extractedRange);
1792
1793
                                                // Set print titles
1794 3
                                                foreach ($extractedRange as $range) {
1795 3
                                                    $matches = [];
1796 3
                                                    $range = str_replace('$', '', $range);
1797
1798
                                                    // check for repeating columns, e g. 'A:A' or 'A:D'
1799 3
                                                    if (preg_match('/!?([A-Z]+)\:([A-Z]+)$/', $range, $matches)) {
1800
                                                        $docSheet->getPageSetup()->setColumnsToRepeatAtLeft([$matches[1], $matches[2]]);
1801 3
                                                    } elseif (preg_match('/!?(\d+)\:(\d+)$/', $range, $matches)) {
1802
                                                        // check for repeating rows, e.g. '1:1' or '1:5'
1803 3
                                                        $docSheet->getPageSetup()->setRowsToRepeatAtTop([(int) $matches[1], (int) $matches[2]]);
1804
                                                    }
1805
                                                }
1806
1807 3
                                                break;
1808 29
                                            case '_xlnm.Print_Area':
1809 11
                                                $rangeSets = preg_split("/('?(?:.*?)'?(?:![A-Z0-9]+:[A-Z0-9]+)),?/", $extractedRange, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE) ?: [];
1810 11
                                                $newRangeSets = [];
1811 11
                                                foreach ($rangeSets as $rangeSet) {
1812 11
                                                    [, $rangeSet] = Worksheet::extractSheetTitle($rangeSet, true);
1813 11
                                                    if (empty($rangeSet)) {
1814
                                                        continue;
1815
                                                    }
1816 11
                                                    if (!str_contains($rangeSet, ':')) {
1817
                                                        $rangeSet = $rangeSet . ':' . $rangeSet;
1818
                                                    }
1819 11
                                                    $newRangeSets[] = str_replace('$', '', $rangeSet);
1820
                                                }
1821 11
                                                if (count($newRangeSets) > 0) {
1822 11
                                                    $docSheet->getPageSetup()->setPrintArea(implode(',', $newRangeSets));
1823
                                                }
1824
1825 11
                                                break;
1826
                                            default:
1827 19
                                                break;
1828
                                        }
1829
                                    }
1830
                                }
1831
                            }
1832
1833
                            // Next sheet id
1834 691
                            ++$sheetId;
1835
                        }
1836
1837
                        // Loop through definedNames
1838 692
                        if ($xmlWorkbook->definedNames) {
1839 379
                            foreach ($xmlWorkbook->definedNames->definedName as $definedName) {
1840
                                // Extract range
1841 108
                                $extractedRange = (string) $definedName;
1842
1843
                                // Valid range?
1844 108
                                if ($extractedRange == '') {
1845
                                    continue;
1846
                                }
1847
1848
                                // Some definedNames are only applicable if we are on the same sheet...
1849 108
                                if ((string) $definedName['localSheetId'] != '') {
1850
                                    // Local defined name
1851
                                    // Switch on type
1852 50
                                    switch ((string) $definedName['name']) {
1853 50
                                        case '_xlnm._FilterDatabase':
1854 30
                                        case '_xlnm.Print_Titles':
1855 29
                                        case '_xlnm.Print_Area':
1856 33
                                            break;
1857
                                        default:
1858 19
                                            if ($mapSheetId[(int) $definedName['localSheetId']] !== null) {
1859 19
                                                $range = Worksheet::extractSheetTitle($extractedRange, true);
1860 19
                                                $scope = $excel->getSheet($mapSheetId[(int) $definedName['localSheetId']]);
1861 19
                                                if (str_contains((string) $definedName, '!')) {
1862 19
                                                    $range[0] = str_replace("''", "'", $range[0]);
1863 19
                                                    $range[0] = str_replace("'", '', $range[0]);
1864 19
                                                    if ($worksheet = $excel->getSheetByName($range[0])) {
1865 19
                                                        $excel->addDefinedName(DefinedName::createInstance((string) $definedName['name'], $worksheet, $extractedRange, true, $scope));
1866
                                                    } else {
1867 14
                                                        $excel->addDefinedName(DefinedName::createInstance((string) $definedName['name'], $scope, $extractedRange, true, $scope));
1868
                                                    }
1869
                                                } else {
1870
                                                    $excel->addDefinedName(DefinedName::createInstance((string) $definedName['name'], $scope, $extractedRange, true));
1871
                                                }
1872
                                            }
1873
1874 19
                                            break;
1875
                                    }
1876 78
                                } elseif (!isset($definedName['localSheetId'])) {
1877
                                    // "Global" definedNames
1878 78
                                    $locatedSheet = null;
1879 78
                                    if (str_contains((string) $definedName, '!')) {
1880
                                        // Modify range, and extract the first worksheet reference
1881
                                        // Need to split on a comma or a space if not in quotes, and extract the first part.
1882 58
                                        $definedNameValueParts = preg_split("/[ ,](?=([^']*'[^']*')*[^']*$)/miuU", $extractedRange);
1883 58
                                        if (is_array($definedNameValueParts)) {
1884
                                            // Extract sheet name
1885 58
                                            [$extractedSheetName] = Worksheet::extractSheetTitle((string) $definedNameValueParts[0], true, true);
1886
1887
                                            // Locate sheet
1888 58
                                            $locatedSheet = $excel->getSheetByName("$extractedSheetName");
1889
                                        }
1890
                                    }
1891
1892 78
                                    if ($locatedSheet === null && !DefinedName::testIfFormula($extractedRange)) {
1893 2
                                        $extractedRange = '#REF!';
1894
                                    }
1895 78
                                    $excel->addDefinedName(DefinedName::createInstance((string) $definedName['name'], $locatedSheet, $extractedRange, false));
1896
                                }
1897
                            }
1898
                        }
1899
                    }
1900
1901 692
                    (new WorkbookView($excel))->viewSettings($xmlWorkbook, $mainNS, $mapSheetId, $this->readDataOnly);
1902
1903 691
                    break;
1904
            }
1905
        }
1906
1907 691
        if (!$this->readDataOnly) {
1908 688
            $contentTypes = $this->loadZip('[Content_Types].xml');
1909
1910
            // Default content types
1911 688
            foreach ($contentTypes->Default as $contentType) {
1912 686
                switch ($contentType['ContentType']) {
1913 686
                    case 'application/vnd.openxmlformats-officedocument.spreadsheetml.printerSettings':
1914 292
                        $unparsedLoadedData['default_content_types'][(string) $contentType['Extension']] = (string) $contentType['ContentType'];
1915
1916 292
                        break;
1917
                }
1918
            }
1919
1920
            // Override content types
1921 688
            foreach ($contentTypes->Override as $contentType) {
1922 687
                switch ($contentType['ContentType']) {
1923 687
                    case 'application/vnd.openxmlformats-officedocument.drawingml.chart+xml':
1924 71
                        if ($this->includeCharts) {
1925 69
                            $chartEntryRef = ltrim((string) $contentType['PartName'], '/');
1926 69
                            $chartElements = $this->loadZip($chartEntryRef);
1927 69
                            $chartReader = new Chart($chartNS, $drawingNS);
1928 69
                            $objChart = $chartReader->readChart($chartElements, basename($chartEntryRef, '.xml'));
1929 69
                            if (isset($charts[$chartEntryRef])) {
1930 69
                                $chartPositionRef = $charts[$chartEntryRef]['sheet'] . '!' . $charts[$chartEntryRef]['id'];
1931 69
                                if (isset($chartDetails[$chartPositionRef]) && $excel->getSheetByName($charts[$chartEntryRef]['sheet']) !== null) {
1932 69
                                    $excel->getSheetByName($charts[$chartEntryRef]['sheet'])->addChart($objChart);
1933 69
                                    $objChart->setWorksheet($excel->getSheetByName($charts[$chartEntryRef]['sheet']));
1934
                                    // For oneCellAnchor or absoluteAnchor positioned charts,
1935
                                    //     toCoordinate is not in the data. Does it need to be calculated?
1936 69
                                    if (array_key_exists('toCoordinate', $chartDetails[$chartPositionRef])) {
1937
                                        // twoCellAnchor
1938 65
                                        $objChart->setTopLeftPosition($chartDetails[$chartPositionRef]['fromCoordinate'], $chartDetails[$chartPositionRef]['fromOffsetX'], $chartDetails[$chartPositionRef]['fromOffsetY']);
1939 65
                                        $objChart->setBottomRightPosition($chartDetails[$chartPositionRef]['toCoordinate'], $chartDetails[$chartPositionRef]['toOffsetX'], $chartDetails[$chartPositionRef]['toOffsetY']);
1940
                                    } else {
1941
                                        // oneCellAnchor or absoluteAnchor (e.g. Chart sheet)
1942 5
                                        $objChart->setTopLeftPosition($chartDetails[$chartPositionRef]['fromCoordinate'], $chartDetails[$chartPositionRef]['fromOffsetX'], $chartDetails[$chartPositionRef]['fromOffsetY']);
1943 5
                                        $objChart->setBottomRightPosition('', $chartDetails[$chartPositionRef]['width'], $chartDetails[$chartPositionRef]['height']);
1944 5
                                        if (array_key_exists('oneCellAnchor', $chartDetails[$chartPositionRef])) {
1945 4
                                            $objChart->setOneCellAnchor($chartDetails[$chartPositionRef]['oneCellAnchor']);
1946
                                        }
1947
                                    }
1948
                                }
1949
                            }
1950
                        }
1951
1952 71
                        break;
1953
1954
                        // unparsed
1955 687
                    case 'application/vnd.ms-excel.controlproperties+xml':
1956 4
                        $unparsedLoadedData['override_content_types'][(string) $contentType['PartName']] = (string) $contentType['ContentType'];
1957
1958 4
                        break;
1959
                }
1960
            }
1961
        }
1962
1963
        /** @var array<array<array<array<string>|string>>> $unparsedLoadedData */
1964 691
        $excel->setUnparsedLoadedData($unparsedLoadedData);
1965
1966 691
        $zip->close();
1967
1968 691
        return $excel;
1969
    }
1970
1971 78
    private function parseRichText(?SimpleXMLElement $is): RichText
1972
    {
1973 78
        $value = new RichText();
1974
1975 78
        if (isset($is->t)) {
1976 22
            $value->createText(StringHelper::controlCharacterOOXML2PHP((string) $is->t));
1977 58
        } elseif ($is !== null) {
1978 58
            if (is_object($is->r)) {
1979 58
                foreach ($is->r as $run) {
1980 55
                    if (!isset($run->rPr)) {
1981 38
                        $value->createText(StringHelper::controlCharacterOOXML2PHP((string) $run->t));
1982
                    } else {
1983 51
                        $objText = $value->createTextRun(StringHelper::controlCharacterOOXML2PHP((string) $run->t));
1984 51
                        $objFont = $objText->getFont() ?? new StyleFont();
1985
1986 51
                        if (isset($run->rPr->rFont)) {
1987 51
                            $attr = $run->rPr->rFont->attributes();
1988 51
                            if (isset($attr['val'])) {
1989 51
                                $objFont->setName((string) $attr['val']);
1990
                            }
1991
                        }
1992 51
                        if (isset($run->rPr->sz)) {
1993 51
                            $attr = $run->rPr->sz->attributes();
1994 51
                            if (isset($attr['val'])) {
1995 51
                                $objFont->setSize((float) $attr['val']);
1996
                            }
1997
                        }
1998 51
                        if (isset($run->rPr->color)) {
1999 49
                            $objFont->setColor(new Color($this->styleReader->readColor($run->rPr->color)));
2000
                        }
2001 51
                        if (isset($run->rPr->b)) {
2002 44
                            $attr = $run->rPr->b->attributes();
2003
                            if (
2004 44
                                (isset($attr['val']) && self::boolean((string) $attr['val']))
2005 44
                                || (!isset($attr['val']))
2006
                            ) {
2007 41
                                $objFont->setBold(true);
2008
                            }
2009
                        }
2010 51
                        if (isset($run->rPr->i)) {
2011 17
                            $attr = $run->rPr->i->attributes();
2012
                            if (
2013 17
                                (isset($attr['val']) && self::boolean((string) $attr['val']))
2014 17
                                || (!isset($attr['val']))
2015
                            ) {
2016 9
                                $objFont->setItalic(true);
2017
                            }
2018
                        }
2019 51
                        if (isset($run->rPr->vertAlign)) {
2020
                            $attr = $run->rPr->vertAlign->attributes();
2021
                            if (isset($attr['val'])) {
2022
                                $vertAlign = strtolower((string) $attr['val']);
2023
                                if ($vertAlign == 'superscript') {
2024
                                    $objFont->setSuperscript(true);
2025
                                }
2026
                                if ($vertAlign == 'subscript') {
2027
                                    $objFont->setSubscript(true);
2028
                                }
2029
                            }
2030
                        }
2031 51
                        if (isset($run->rPr->u)) {
2032 13
                            $attr = $run->rPr->u->attributes();
2033 13
                            if (!isset($attr['val'])) {
2034 1
                                $objFont->setUnderline(StyleFont::UNDERLINE_SINGLE);
2035
                            } else {
2036 12
                                $objFont->setUnderline((string) $attr['val']);
2037
                            }
2038
                        }
2039 51
                        if (isset($run->rPr->strike)) {
2040 12
                            $attr = $run->rPr->strike->attributes();
2041
                            if (
2042 12
                                (isset($attr['val']) && self::boolean((string) $attr['val']))
2043 12
                                || (!isset($attr['val']))
2044
                            ) {
2045
                                $objFont->setStrikethrough(true);
2046
                            }
2047
                        }
2048
                    }
2049
                }
2050
            }
2051
        }
2052
2053 78
        return $value;
2054
    }
2055
2056 2
    private function readRibbon(Spreadsheet $excel, string $customUITarget, ZipArchive $zip): void
2057
    {
2058 2
        $baseDir = dirname($customUITarget);
2059 2
        $nameCustomUI = basename($customUITarget);
2060
        // get the xml file (ribbon)
2061 2
        $localRibbon = $this->getFromZipArchive($zip, $customUITarget);
2062 2
        $customUIImagesNames = [];
2063 2
        $customUIImagesBinaries = [];
2064
        // something like customUI/_rels/customUI.xml.rels
2065 2
        $pathRels = $baseDir . '/_rels/' . $nameCustomUI . '.rels';
2066 2
        $dataRels = $this->getFromZipArchive($zip, $pathRels);
2067 2
        if ($dataRels) {
2068
            // exists and not empty if the ribbon have some pictures (other than internal MSO)
2069
            $UIRels = simplexml_load_string(
2070
                $this->getSecurityScannerOrThrow()
2071
                    ->scan($dataRels),
2072
                SimpleXMLElement::class,
2073
                $this->parseHuge ? LIBXML_PARSEHUGE : 0
2074
            );
2075
            if (false !== $UIRels) {
2076
                // we need to save id and target to avoid parsing customUI.xml and "guess" if it's a pseudo callback who load the image
2077
                foreach ($UIRels->Relationship as $ele) {
2078
                    if ((string) $ele['Type'] === Namespaces::SCHEMA_OFFICE_DOCUMENT . '/image') {
2079
                        // an image ?
2080
                        $customUIImagesNames[(string) $ele['Id']] = (string) $ele['Target'];
2081
                        $customUIImagesBinaries[(string) $ele['Target']] = $this->getFromZipArchive($zip, $baseDir . '/' . (string) $ele['Target']);
2082
                    }
2083
                }
2084
            }
2085
        }
2086 2
        if ($localRibbon) {
2087 2
            $excel->setRibbonXMLData($customUITarget, $localRibbon);
2088 2
            if (count($customUIImagesNames) > 0 && count($customUIImagesBinaries) > 0) {
2089
                $excel->setRibbonBinObjects($customUIImagesNames, $customUIImagesBinaries);
2090
            } else {
2091 2
                $excel->setRibbonBinObjects(null, null);
2092
            }
2093
        } else {
2094
            $excel->setRibbonXMLData(null, null);
2095
            $excel->setRibbonBinObjects(null, null);
2096
        }
2097
    }
2098
2099
    /** @param null|bool|mixed[]|SimpleXMLElement $array */
2100 716
    private static function getArrayItem(null|array|bool|SimpleXMLElement $array, int|string $key = 0): mixed
2101
    {
2102 716
        return ($array === null || is_bool($array)) ? null : ($array[$key] ?? null);
2103
    }
2104
2105
    /** @param null|bool|mixed[]|SimpleXMLElement $array */
2106 708
    private static function getArrayItemString(null|array|bool|SimpleXMLElement $array, int|string $key = 0): string
2107
    {
2108 708
        $retVal = self::getArrayItem($array, $key);
2109
2110 708
        return StringHelper::convertToString($retVal, false);
2111
    }
2112
2113
    /** @param null|bool|mixed[]|SimpleXMLElement $array */
2114 60
    private static function getArrayItemIntOrSxml(null|array|bool|SimpleXMLElement $array, int|string $key = 0): int|SimpleXMLElement
2115
    {
2116 60
        $retVal = self::getArrayItem($array, $key);
2117
2118 60
        return (is_int($retVal) || $retVal instanceof SimpleXMLElement) ? $retVal : 0;
2119
    }
2120
2121 375
    private static function dirAdd(null|SimpleXMLElement|string $base, null|SimpleXMLElement|string $add): string
2122
    {
2123 375
        $base = (string) $base;
2124 375
        $add = (string) $add;
2125
2126 375
        return (string) preg_replace('~[^/]+/\.\./~', '', dirname($base) . "/$add");
2127
    }
2128
2129
    /** @return mixed[] */
2130 3
    private static function toCSSArray(string $style): array
2131
    {
2132 3
        $style = self::stripWhiteSpaceFromStyleString($style);
2133
2134 3
        $temp = explode(';', $style);
2135 3
        $style = [];
2136 3
        foreach ($temp as $item) {
2137 3
            $item = explode(':', $item);
2138
2139 3
            if (str_contains($item[1], 'px')) {
2140 2
                $item[1] = str_replace('px', '', $item[1]);
2141
            }
2142 3
            if (str_contains($item[1], 'pt')) {
2143 2
                $item[1] = str_replace('pt', '', $item[1]);
2144 2
                $item[1] = (string) Font::fontSizeToPixels((int) $item[1]);
2145
            }
2146 3
            if (str_contains($item[1], 'in')) {
2147
                $item[1] = str_replace('in', '', $item[1]);
2148
                $item[1] = (string) Font::inchSizeToPixels((int) $item[1]);
2149
            }
2150 3
            if (str_contains($item[1], 'cm')) {
2151
                $item[1] = str_replace('cm', '', $item[1]);
2152
                $item[1] = (string) Font::centimeterSizeToPixels((int) $item[1]);
2153
            }
2154
2155 3
            $style[$item[0]] = $item[1];
2156
        }
2157
2158 3
        return $style;
2159
    }
2160
2161 6
    public static function stripWhiteSpaceFromStyleString(string $string): string
2162
    {
2163 6
        return trim(str_replace(["\r", "\n", ' '], '', $string), ';');
2164
    }
2165
2166 91
    private static function boolean(string $value): bool
2167
    {
2168 91
        if (is_numeric($value)) {
2169 70
            return (bool) $value;
2170
        }
2171
2172 34
        return $value === 'true' || $value === 'TRUE';
2173
    }
2174
2175
    /** @param string[] $hyperlinks */
2176 57
    private function readHyperLinkDrawing(\PhpOffice\PhpSpreadsheet\Worksheet\Drawing $objDrawing, SimpleXMLElement $cellAnchor, array $hyperlinks): void
2177
    {
2178 57
        $hlinkClick = $cellAnchor->pic->nvPicPr->cNvPr->children(Namespaces::DRAWINGML)->hlinkClick;
2179
2180 57
        if ($hlinkClick->count() === 0) {
2181 55
            return;
2182
        }
2183
2184 2
        $hlinkId = (string) self::getAttributes($hlinkClick, Namespaces::SCHEMA_OFFICE_DOCUMENT)['id'];
2185 2
        $hyperlink = new Hyperlink(
2186 2
            $hyperlinks[$hlinkId],
2187 2
            self::getArrayItemString(self::getAttributes($cellAnchor->pic->nvPicPr->cNvPr), 'name')
2188 2
        );
2189 2
        $objDrawing->setHyperlink($hyperlink);
2190
    }
2191
2192 693
    private function readProtection(Spreadsheet $excel, SimpleXMLElement $xmlWorkbook): void
2193
    {
2194 693
        if (!$xmlWorkbook->workbookProtection) {
2195 673
            return;
2196
        }
2197
2198 24
        $excel->getSecurity()->setLockRevision(self::getLockValue($xmlWorkbook->workbookProtection, 'lockRevision'));
2199 24
        $excel->getSecurity()->setLockStructure(self::getLockValue($xmlWorkbook->workbookProtection, 'lockStructure'));
2200 24
        $excel->getSecurity()->setLockWindows(self::getLockValue($xmlWorkbook->workbookProtection, 'lockWindows'));
2201
2202 24
        if ($xmlWorkbook->workbookProtection['revisionsPassword']) {
2203 1
            $excel->getSecurity()->setRevisionsPassword(
2204 1
                (string) $xmlWorkbook->workbookProtection['revisionsPassword'],
2205 1
                true
2206 1
            );
2207
        }
2208
2209 24
        if ($xmlWorkbook->workbookProtection['workbookPassword']) {
2210 2
            $excel->getSecurity()->setWorkbookPassword(
2211 2
                (string) $xmlWorkbook->workbookProtection['workbookPassword'],
2212 2
                true
2213 2
            );
2214
        }
2215
    }
2216
2217 24
    private static function getLockValue(SimpleXMLElement $protection, string $key): ?bool
2218
    {
2219 24
        $returnValue = null;
2220 24
        $protectKey = $protection[$key];
2221 24
        if (!empty($protectKey)) {
2222 10
            $protectKey = (string) $protectKey;
2223 10
            $returnValue = $protectKey !== 'false' && (bool) $protectKey;
2224
        }
2225
2226 24
        return $returnValue;
2227
    }
2228
2229
    /** @param mixed[][][][] $unparsedLoadedData */
2230 691
    private function readFormControlProperties(Spreadsheet $excel, string $dir, string $fileWorksheet, Worksheet $docSheet, array &$unparsedLoadedData): void
2231
    {
2232 691
        $zip = $this->zip;
2233 691
        if ($zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels') === false) {
2234 344
            return;
2235
        }
2236
2237 570
        $filename = dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels';
2238 570
        $relsWorksheet = $this->loadZipNoNamespace($filename, Namespaces::RELATIONSHIPS);
2239 570
        $ctrlProps = [];
2240 570
        foreach ($relsWorksheet->Relationship as $ele) {
2241 398
            if ((string) $ele['Type'] === Namespaces::SCHEMA_OFFICE_DOCUMENT . '/ctrlProp') {
2242 4
                $ctrlProps[(string) $ele['Id']] = $ele;
2243
            }
2244
        }
2245
2246
        /** @var mixed[][] */
2247 570
        $unparsedCtrlProps = &$unparsedLoadedData['sheets'][$docSheet->getCodeName()]['ctrlProps'];
2248 570
        foreach ($ctrlProps as $rId => $ctrlProp) {
2249 4
            $rId = substr($rId, 3); // rIdXXX
2250 4
            $unparsedCtrlProps[$rId] = [];
2251 4
            $unparsedCtrlProps[$rId]['filePath'] = self::dirAdd("$dir/$fileWorksheet", $ctrlProp['Target']);
2252 4
            $unparsedCtrlProps[$rId]['relFilePath'] = (string) $ctrlProp['Target'];
2253 4
            $unparsedCtrlProps[$rId]['content'] = $this->getSecurityScannerOrThrow()->scan($this->getFromZipArchive($zip, $unparsedCtrlProps[$rId]['filePath']));
2254
        }
2255 570
        unset($unparsedCtrlProps);
2256
    }
2257
2258
    /** @param mixed[][][][] $unparsedLoadedData */
2259 691
    private function readPrinterSettings(Spreadsheet $excel, string $dir, string $fileWorksheet, Worksheet $docSheet, array &$unparsedLoadedData): void
2260
    {
2261 691
        if ($this->readDataOnly) {
2262 3
            return;
2263
        }
2264 688
        $zip = $this->zip;
2265 688
        if ($zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels') === false) {
2266 343
            return;
2267
        }
2268
2269 567
        $filename = dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels';
2270 567
        $relsWorksheet = $this->loadZipNoNamespace($filename, Namespaces::RELATIONSHIPS);
2271 567
        $sheetPrinterSettings = [];
2272 567
        foreach ($relsWorksheet->Relationship as $ele) {
2273 396
            if ((string) $ele['Type'] === Namespaces::SCHEMA_OFFICE_DOCUMENT . '/printerSettings') {
2274 284
                $sheetPrinterSettings[(string) $ele['Id']] = $ele;
2275
            }
2276
        }
2277
2278
        /** @var mixed[][] */
2279 567
        $unparsedPrinterSettings = &$unparsedLoadedData['sheets'][$docSheet->getCodeName()]['printerSettings'];
2280 567
        foreach ($sheetPrinterSettings as $rId => $printerSettings) {
2281 284
            $rId = substr($rId, 3); // rIdXXX
2282 284
            if (!str_ends_with($rId, 'ps')) {
2283 284
                $rId = $rId . 'ps'; // rIdXXX, add 'ps' suffix to avoid identical resource identifier collision with unparsed vmlDrawing
2284
            }
2285 284
            $unparsedPrinterSettings[$rId] = [];
2286 284
            $target = (string) str_replace('/xl/', '../', (string) $printerSettings['Target']);
2287 284
            $unparsedPrinterSettings[$rId]['filePath'] = self::dirAdd("$dir/$fileWorksheet", $target);
2288 284
            $unparsedPrinterSettings[$rId]['relFilePath'] = $target;
2289 284
            $unparsedPrinterSettings[$rId]['content'] = $this->getSecurityScannerOrThrow()->scan($this->getFromZipArchive($zip, $unparsedPrinterSettings[$rId]['filePath']));
2290
        }
2291 567
        unset($unparsedPrinterSettings);
2292
    }
2293
2294
    /** @return array{string, string} */
2295 704
    private function getWorkbookBaseName(): array
2296
    {
2297 704
        $workbookBasename = '';
2298 704
        $xmlNamespaceBase = '';
2299
2300
        // check if it is an OOXML archive
2301 704
        $rels = $this->loadZip(self::INITIAL_FILE);
2302 704
        foreach ($rels->children(Namespaces::RELATIONSHIPS)->Relationship as $rel) {
2303 704
            $rel = self::getAttributes($rel);
2304 704
            $type = (string) $rel['Type'];
2305
            switch ($type) {
2306 696
                case Namespaces::OFFICE_DOCUMENT:
2307 682
                case Namespaces::PURL_OFFICE_DOCUMENT:
2308 704
                    $basename = basename((string) $rel['Target']);
2309 704
                    $xmlNamespaceBase = dirname($type);
2310 704
                    if (preg_match('/workbook.*\.xml/', $basename)) {
2311 704
                        $workbookBasename = $basename;
2312
                    }
2313
2314 704
                    break;
2315
            }
2316
        }
2317
2318 704
        return [$workbookBasename, $xmlNamespaceBase];
2319
    }
2320
2321 682
    private function readSheetProtection(Worksheet $docSheet, SimpleXMLElement $xmlSheet): void
2322
    {
2323 682
        if ($this->readDataOnly || !$xmlSheet->sheetProtection) {
2324 624
            return;
2325
        }
2326
2327 68
        $algorithmName = (string) $xmlSheet->sheetProtection['algorithmName'];
2328 68
        $protection = $docSheet->getProtection();
2329 68
        $protection->setAlgorithm($algorithmName);
2330
2331 68
        if ($algorithmName) {
2332 2
            $protection->setPassword((string) $xmlSheet->sheetProtection['hashValue'], true);
2333 2
            $protection->setSalt((string) $xmlSheet->sheetProtection['saltValue']);
2334 2
            $protection->setSpinCount((int) $xmlSheet->sheetProtection['spinCount']);
2335
        } else {
2336 67
            $protection->setPassword((string) $xmlSheet->sheetProtection['password'], true);
2337
        }
2338
2339 68
        if ($xmlSheet->protectedRanges->protectedRange) {
2340 4
            foreach ($xmlSheet->protectedRanges->protectedRange as $protectedRange) {
2341 4
                $docSheet->protectCells((string) $protectedRange['sqref'], (string) $protectedRange['password'], true, (string) $protectedRange['name'], (string) $protectedRange['securityDescriptor']);
2342
            }
2343
        }
2344
    }
2345
2346 689
    private function readAutoFilter(
2347
        SimpleXMLElement $xmlSheet,
2348
        Worksheet $docSheet
2349
    ): void {
2350 689
        if ($xmlSheet && $xmlSheet->autoFilter) {
2351 18
            (new AutoFilter($docSheet, $xmlSheet))->load();
2352
        }
2353
    }
2354
2355 689
    private function readBackgroundImage(
2356
        SimpleXMLElement $xmlSheet,
2357
        Worksheet $docSheet,
2358
        string $relsName
2359
    ): void {
2360 689
        if ($xmlSheet && $xmlSheet->picture) {
2361 1
            $id = (string) self::getArrayItemString(self::getAttributes($xmlSheet->picture, Namespaces::SCHEMA_OFFICE_DOCUMENT), 'id');
2362 1
            $rels = $this->loadZip($relsName);
2363 1
            foreach ($rels->Relationship as $rel) {
2364 1
                $attrs = $rel->attributes() ?? [];
2365 1
                $rid = (string) ($attrs['Id'] ?? '');
2366 1
                $target = (string) ($attrs['Target'] ?? '');
2367 1
                if ($rid === $id && substr($target, 0, 2) === '..') {
2368 1
                    $target = 'xl' . substr($target, 2);
2369 1
                    $content = $this->getFromZipArchive($this->zip, $target);
2370 1
                    $docSheet->setBackgroundImage($content);
2371
                }
2372
            }
2373
        }
2374
    }
2375
2376
    /**
2377
     * @param TableDxfsStyle[] $tableStyles
2378
     * @param Style[] $dxfs
2379
     */
2380 692
    private function readTables(
2381
        SimpleXMLElement $xmlSheet,
2382
        Worksheet $docSheet,
2383
        string $dir,
2384
        string $fileWorksheet,
2385
        ZipArchive $zip,
2386
        string $namespaceTable,
2387
        array $tableStyles,
2388
        array $dxfs
2389
    ): void {
2390 692
        if ($xmlSheet && $xmlSheet->tableParts) {
2391
            /** @var array{count: scalar} */
2392 37
            $attributes = $xmlSheet->tableParts->attributes() ?? ['count' => 0];
2393 37
            if (((int) $attributes['count']) > 0) {
2394 33
                $this->readTablesInTablesFile($xmlSheet, $dir, $fileWorksheet, $zip, $docSheet, $namespaceTable, $tableStyles, $dxfs);
2395
            }
2396
        }
2397
    }
2398
2399
    /**
2400
     * @param TableDxfsStyle[] $tableStyles
2401
     * @param Style[] $dxfs
2402
     */
2403 33
    private function readTablesInTablesFile(
2404
        SimpleXMLElement $xmlSheet,
2405
        string $dir,
2406
        string $fileWorksheet,
2407
        ZipArchive $zip,
2408
        Worksheet $docSheet,
2409
        string $namespaceTable,
2410
        array $tableStyles,
2411
        array $dxfs
2412
    ): void {
2413 33
        foreach ($xmlSheet->tableParts->tablePart as $tablePart) {
2414 33
            $relation = self::getAttributes($tablePart, Namespaces::SCHEMA_OFFICE_DOCUMENT);
2415 33
            $tablePartRel = (string) $relation['id'];
2416 33
            $relationsFileName = dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels';
2417
2418 33
            if ($zip->locateName($relationsFileName) !== false) {
2419 33
                $relsTableReferences = $this->loadZip($relationsFileName, Namespaces::RELATIONSHIPS);
2420 33
                foreach ($relsTableReferences->Relationship as $relationship) {
2421 33
                    $relationshipAttributes = self::getAttributes($relationship, '');
2422
2423 33
                    if ((string) $relationshipAttributes['Id'] === $tablePartRel) {
2424 33
                        $relationshipFileName = (string) $relationshipAttributes['Target'];
2425 33
                        $relationshipFilePath = dirname("$dir/$fileWorksheet") . '/' . $relationshipFileName;
2426 33
                        $relationshipFilePath = File::realpath($relationshipFilePath);
2427
2428 33
                        if ($this->fileExistsInArchive($this->zip, $relationshipFilePath)) {
2429 33
                            $tableXml = $this->loadZip($relationshipFilePath, $namespaceTable);
2430 33
                            (new TableReader($docSheet, $tableXml))->load($tableStyles, $dxfs);
2431
                        }
2432
                    }
2433
                }
2434
            }
2435
        }
2436
    }
2437
2438
    /** @return mixed[] */
2439 700
    private static function extractStyles(?SimpleXMLElement $sxml, string $node1, string $node2): array
2440
    {
2441 700
        $array = [];
2442 700
        if ($sxml && $sxml->{$node1}->{$node2}) {
2443
            /** @var SimpleXMLElement */
2444 700
            $temp = $sxml->{$node1}->{$node2};
2445 700
            foreach ($temp as $node) {
2446 700
                $array[] = $node;
2447
            }
2448
        }
2449
2450 700
        return $array;
2451
    }
2452
2453
    /** @return string[] */
2454 700
    private static function extractPalette(?SimpleXMLElement $sxml): array
2455
    {
2456 700
        $array = [];
2457 700
        if ($sxml && $sxml->colors->indexedColors) {
2458 16
            foreach ($sxml->colors->indexedColors->rgbColor as $node) {
2459 16
                $attr = $node->attributes();
2460 16
                if (isset($attr['rgb'])) {
2461 16
                    $array[] = (string) $attr['rgb'];
2462
                }
2463
            }
2464
        }
2465
2466 700
        return $array;
2467
    }
2468
2469 4
    private function processIgnoredErrors(SimpleXMLElement $xml, Worksheet $sheet): void
2470
    {
2471 4
        $cellCollection = $sheet->getCellCollection();
2472 4
        $attributes = self::getAttributes($xml);
2473 4
        $sqref = (string) ($attributes['sqref'] ?? '');
2474 4
        $numberStoredAsText = (string) ($attributes['numberStoredAsText'] ?? '');
2475 4
        $formula = (string) ($attributes['formula'] ?? '');
2476 4
        $formulaRange = (string) ($attributes['formulaRange'] ?? '');
2477 4
        $twoDigitTextYear = (string) ($attributes['twoDigitTextYear'] ?? '');
2478 4
        $evalError = (string) ($attributes['evalError'] ?? '');
2479 4
        if (!empty($sqref)) {
2480 4
            $explodedSqref = explode(' ', $sqref);
2481 4
            $pattern1 = '/^([A-Z]{1,3})([0-9]{1,7})(:([A-Z]{1,3})([0-9]{1,7}))?$/';
2482 4
            foreach ($explodedSqref as $sqref1) {
2483 4
                if (preg_match($pattern1, $sqref1, $matches) === 1) {
2484 4
                    $firstRow = $matches[2];
2485 4
                    $firstCol = $matches[1];
2486 4
                    if (array_key_exists(3, $matches)) {
2487 3
                        $lastCol = $matches[4];
2488 3
                        $lastRow = $matches[5];
2489
                    } else {
2490 3
                        $lastCol = $firstCol;
2491 3
                        $lastRow = $firstRow;
2492
                    }
2493 4
                    ++$lastCol;
2494 4
                    for ($row = $firstRow; $row <= $lastRow; ++$row) {
2495 4
                        for ($col = $firstCol; $col !== $lastCol; ++$col) {
2496
                            /** @var string $col */
2497 4
                            if (!$cellCollection->has2("$col$row")) {
2498 1
                                continue;
2499
                            }
2500 4
                            if ($numberStoredAsText === '1') {
2501 4
                                $sheet->getCell("$col$row")->getIgnoredErrors()->setNumberStoredAsText(true);
2502
                            }
2503 4
                            if ($formula === '1') {
2504 1
                                $sheet->getCell("$col$row")->getIgnoredErrors()->setFormula(true);
2505
                            }
2506 4
                            if ($formulaRange === '1') {
2507 1
                                $sheet->getCell("$col$row")->getIgnoredErrors()->setFormulaRange(true);
2508
                            }
2509 4
                            if ($twoDigitTextYear === '1') {
2510 1
                                $sheet->getCell("$col$row")->getIgnoredErrors()->setTwoDigitTextYear(true);
2511
                            }
2512 4
                            if ($evalError === '1') {
2513 1
                                $sheet->getCell("$col$row")->getIgnoredErrors()->setEvalError(true);
2514
                            }
2515
                        }
2516
                    }
2517
                }
2518
            }
2519
        }
2520
    }
2521
2522 368
    private static function storeFormulaAttributes(SimpleXMLElement $f, Worksheet $docSheet, string $r): void
2523
    {
2524 368
        $formulaAttributes = [];
2525 368
        $attributes = $f->attributes();
2526 368
        if (isset($attributes['t'])) {
2527 249
            $formulaAttributes['t'] = (string) $attributes['t'];
2528
        }
2529 368
        if (isset($attributes['ref'])) {
2530 249
            $formulaAttributes['ref'] = (string) $attributes['ref'];
2531
        }
2532 368
        if (!empty($formulaAttributes)) {
2533 249
            $docSheet->getCell($r)->setFormulaAttributes($formulaAttributes);
2534
        }
2535
    }
2536
2537 13
    private static function onlyNoteVml(string $data): bool
2538
    {
2539 13
        $data = str_replace('<br>', '<br/>', $data);
2540
2541
        try {
2542 13
            $sxml = @simplexml_load_string($data);
2543
        } catch (Throwable) {
2544
            $sxml = false;
2545
        }
2546
2547 13
        if ($sxml === false) {
2548 1
            return false;
2549
        }
2550 12
        $shapes = $sxml->children(Namespaces::URN_VML);
2551 12
        foreach ($shapes->shape as $shape) {
2552 12
            $clientData = $shape->children(Namespaces::URN_EXCEL);
2553 12
            if (!isset($clientData->ClientData)) {
2554
                return false;
2555
            }
2556 12
            $attrs = $clientData->ClientData->attributes();
2557 12
            if (!isset($attrs['ObjectType'])) {
2558
                return false;
2559
            }
2560 12
            $objectType = (string) $attrs['ObjectType'];
2561 12
            if ($objectType !== 'Note') {
2562 4
                return false;
2563
            }
2564
        }
2565
2566 9
        return true;
2567
    }
2568
}
2569