Passed
Pull Request — master (#4122)
by Owen
15:47
created

Xlsx::onlyNoteVml()   B

Complexity

Conditions 7
Paths 12

Size

Total Lines 30
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

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