Xlsx   F
last analyzed

Complexity

Total Complexity 528

Size/Duplication

Total Lines 2442
Duplicated Lines 0 %

Test Coverage

Coverage 95.92%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 528
eloc 1452
dl 0
loc 2442
ccs 1410
cts 1470
cp 0.9592
rs 0.8
c 2
b 0
f 0

43 Methods

Rating   Name   Duplication   Size   Complexity  
A canRead() 0 17 3
A castToBoolean() 0 10 4
A castToError() 0 3 2
A __construct() 0 5 1
A castToString() 0 3 2
A getFromZipArchive() 0 27 5
A falseToArray() 0 3 2
A getAttributes() 0 3 2
A listWorksheetNames() 0 30 5
A testSimpleXml() 0 3 2
A xpathNoFalse() 0 3 1
A loadZip() 0 14 2
A loadZipNonamespace() 0 11 2
A readProtection() 0 21 4
A boolean() 0 7 3
D parseRichText() 0 83 29
A dirAdd() 0 6 1
A getWorkbookBaseName() 0 24 5
A readBackgroundImage() 0 16 6
A getLockValue() 0 10 3
A getArrayItemIntOrSxml() 0 5 3
A readTablesInTablesFile() 0 28 6
A storeFormulaAttributes() 0 12 4
A fileExistsInArchive() 0 18 3
C processIgnoredErrors() 0 45 13
A readHyperLinkDrawing() 0 14 2
A stripWhiteSpaceFromStyleString() 0 3 1
B onlyNoteVml() 0 30 7
A readFormControlProperties() 0 25 5
A getArrayItem() 0 3 3
A readSheetProtection() 0 21 6
F loadSpreadsheetFromFile() 0 1519 330
A replacePrefixes() 0 3 1
A extractPalette() 0 13 5
C listWorksheetInfo() 0 88 17
B readPrinterSettings() 0 29 6
A getArrayItemString() 0 5 1
B readRibbon() 0 38 8
A castToFormula() 0 28 6
A toCSSArray() 0 29 6
A readAutoFilter() 0 6 3
A extractStyles() 0 10 4
A readTables() 0 14 4

How to fix   Complexity   

Complex Class

Complex classes like Xlsx often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Xlsx, and based on these observations, apply Extract Interface, too.

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