Failed Conditions
Pull Request — master (#4514)
by Owen
12:56
created

Xlsx::readTablesInTablesFile()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 28
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 6

Importance

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