Passed
Push — master ( fcb5ef...e65bc8 )
by
unknown
13:57 queued 18s
created

Xlsx::readTablesInTablesFile()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 28
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 6

Importance

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

How to fix   Many Parameters   

Many Parameters

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

There are several approaches to avoid long parameter lists:

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