Passed
Pull Request — master (#4147)
by Owen
14:30
created

Xlsx::castToFormula()   A

Complexity

Conditions 6
Paths 5

Size

Total Lines 29
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 6.0052

Importance

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