• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

PHPOffice / PhpSpreadsheet / 21013444139

14 Jan 2026 11:20PM UTC coverage: 96.199% (+0.2%) from 95.962%
21013444139

Pull #4657

github

web-flow
Merge dcaff346a into 48f2fe37d
Pull Request #4657: Handling Unions as Function Arguments

19 of 20 new or added lines in 1 file covered. (95.0%)

359 existing lines in 16 files now uncovered.

46287 of 48116 relevant lines covered (96.2%)

387.01 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.03
/src/PhpSpreadsheet/Reader/Xlsx.php
1
<?php
2

3
namespace PhpOffice\PhpSpreadsheet\Reader;
4

5
use Composer\Pcre\Preg;
6
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
7
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
8
use PhpOffice\PhpSpreadsheet\Cell\DataType;
9
use PhpOffice\PhpSpreadsheet\Cell\Hyperlink;
10
use PhpOffice\PhpSpreadsheet\Comment;
11
use PhpOffice\PhpSpreadsheet\DefinedName;
12
use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner;
13
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\AutoFilter;
14
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Chart;
15
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\ColumnAndRowAttributes;
16
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\ConditionalStyles;
17
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\DataValidations;
18
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Hyperlinks;
19
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Namespaces;
20
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\PageSetup;
21
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Properties as PropertyReader;
22
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\SharedFormula;
23
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\SheetViewOptions;
24
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\SheetViews;
25
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Styles;
26
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\TableReader;
27
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Theme;
28
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\WorkbookView;
29
use PhpOffice\PhpSpreadsheet\ReferenceHelper;
30
use PhpOffice\PhpSpreadsheet\RichText\RichText;
31
use PhpOffice\PhpSpreadsheet\Shared\Date;
32
use PhpOffice\PhpSpreadsheet\Shared\Drawing;
33
use PhpOffice\PhpSpreadsheet\Shared\File;
34
use PhpOffice\PhpSpreadsheet\Shared\Font;
35
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
36
use PhpOffice\PhpSpreadsheet\Spreadsheet;
37
use PhpOffice\PhpSpreadsheet\Style\Color;
38
use PhpOffice\PhpSpreadsheet\Style\Font as StyleFont;
39
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
40
use PhpOffice\PhpSpreadsheet\Style\Style;
41
use PhpOffice\PhpSpreadsheet\Worksheet\HeaderFooterDrawing;
42
use PhpOffice\PhpSpreadsheet\Worksheet\Table\TableDxfsStyle;
43
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
44
use SimpleXMLElement;
45
use Throwable;
46
use XMLReader;
47
use ZipArchive;
48

49
class Xlsx extends BaseReader
50
{
51
    const INITIAL_FILE = '_rels/.rels';
52

53
    /**
54
     * ReferenceHelper instance.
55
     */
56
    private ReferenceHelper $referenceHelper;
57

58
    private ZipArchive $zip;
59

60
    private Styles $styleReader;
61

62
    /** @var SharedFormula[] */
63
    private array $sharedFormulae = [];
64

65
    private bool $parseHuge = false;
66

67
    /**
68
     * Allow use of LIBXML_PARSEHUGE.
69
     * This option can lead to memory leaks and failures,
70
     * and is not recommended. But some very large spreadsheets
71
     * seem to require it.
72
     */
UNCOV
73
    public function setParseHuge(bool $parseHuge): void
×
74
    {
UNCOV
75
        $this->parseHuge = $parseHuge;
×
76
    }
77

78
    /**
79
     * Create a new Xlsx Reader instance.
80
     */
81
    public function __construct()
816✔
82
    {
83
        parent::__construct();
816✔
84
        $this->referenceHelper = ReferenceHelper::getInstance();
816✔
85
        $this->securityScanner = XmlScanner::getInstance($this);
816✔
86
    }
87

88
    /**
89
     * Can the current IReader read the file?
90
     */
91
    public function canRead(string $filename): bool
39✔
92
    {
93
        if (!File::testFileNoThrow($filename, self::INITIAL_FILE)) {
39✔
94
            return false;
16✔
95
        }
96

97
        $result = false;
23✔
98
        $this->zip = $zip = new ZipArchive();
23✔
99

100
        if ($zip->open($filename) === true) {
23✔
101
            [$workbookBasename] = $this->getWorkbookBaseName();
23✔
102
            $result = !empty($workbookBasename);
23✔
103

104
            $zip->close();
23✔
105
        }
106

107
        return $result;
23✔
108
    }
109

110
    public static function testSimpleXml(mixed $value): SimpleXMLElement
788✔
111
    {
112
        return ($value instanceof SimpleXMLElement) ? $value : new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><root></root>');
788✔
113
    }
114

115
    public static function getAttributes(?SimpleXMLElement $value, string $ns = ''): SimpleXMLElement
784✔
116
    {
117
        return self::testSimpleXml($value === null ? $value : $value->attributes($ns));
784✔
118
    }
119

120
    // Phpstan thinks, correctly, that xpath can return false.
121
    /** @return mixed[] */
122
    private static function xpathNoFalse(SimpleXMLElement $sxml, string $path): array
753✔
123
    {
124
        return self::falseToArray($sxml->xpath($path));
753✔
125
    }
126

127
    /** @return mixed[] */
128
    public static function falseToArray(mixed $value): array
753✔
129
    {
130
        return is_array($value) ? $value : [];
753✔
131
    }
132

133
    private function loadZip(string $filename, string $ns = '', bool $replaceUnclosedBr = false): SimpleXMLElement
784✔
134
    {
135
        $contents = $this->getFromZipArchive($this->zip, $filename);
784✔
136
        if ($replaceUnclosedBr) {
784✔
137
            $contents = str_replace('<br>', '<br/>', $contents);
47✔
138
        }
139
        $rels = @simplexml_load_string(
784✔
140
            $this->getSecurityScannerOrThrow()->scan($contents),
784✔
141
            SimpleXMLElement::class,
784✔
142
            $this->parseHuge ? LIBXML_PARSEHUGE : 0,
784✔
143
            $ns
784✔
144
        );
784✔
145

146
        return self::testSimpleXml($rels);
784✔
147
    }
148

149
    // This function is just to identify cases where I'm not sure
150
    // why empty namespace is required.
151
    private function loadZipNonamespace(string $filename, string $ns): SimpleXMLElement
751✔
152
    {
153
        $contents = $this->getFromZipArchive($this->zip, $filename);
751✔
154
        $rels = simplexml_load_string(
751✔
155
            $this->getSecurityScannerOrThrow()->scan($contents),
751✔
156
            SimpleXMLElement::class,
751✔
157
            $this->parseHuge ? LIBXML_PARSEHUGE : 0,
751✔
158
            ($ns === '' ? $ns : '')
751✔
159
        );
751✔
160

161
        return self::testSimpleXml($rels);
746✔
162
    }
163

164
    private const REL_TO_MAIN = [
165
        Namespaces::PURL_OFFICE_DOCUMENT => Namespaces::PURL_MAIN,
166
        Namespaces::THUMBNAIL => '',
167
    ];
168

169
    private const REL_TO_DRAWING = [
170
        Namespaces::PURL_RELATIONSHIPS => Namespaces::PURL_DRAWING,
171
    ];
172

173
    private const REL_TO_CHART = [
174
        Namespaces::PURL_RELATIONSHIPS => Namespaces::PURL_CHART,
175
    ];
176

177
    /**
178
     * Reads names of the worksheets from a file, without parsing the whole file to a Spreadsheet object.
179
     *
180
     * @return string[]
181
     */
182
    public function listWorksheetNames(string $filename): array
18✔
183
    {
184
        File::assertFile($filename, self::INITIAL_FILE);
18✔
185

186
        $worksheetNames = [];
15✔
187

188
        $this->zip = $zip = new ZipArchive();
15✔
189
        $zip->open($filename);
15✔
190

191
        //    The files we're looking at here are small enough that simpleXML is more efficient than XMLReader
192
        $rels = $this->loadZip(self::INITIAL_FILE, Namespaces::RELATIONSHIPS);
15✔
193
        foreach ($rels->Relationship as $relx) {
15✔
194
            $rel = self::getAttributes($relx);
15✔
195
            $relType = (string) $rel['Type'];
15✔
196
            $mainNS = self::REL_TO_MAIN[$relType] ?? Namespaces::MAIN;
15✔
197
            if ($mainNS !== '') {
15✔
198
                $xmlWorkbook = $this->loadZip((string) $rel['Target'], $mainNS);
15✔
199

200
                if ($xmlWorkbook->sheets) {
15✔
201
                    foreach ($xmlWorkbook->sheets->sheet as $eleSheet) {
15✔
202
                        // Check if sheet should be skipped
203
                        $worksheetNames[] = (string) self::getAttributes($eleSheet)['name'];
15✔
204
                    }
205
                }
206
            }
207
        }
208

209
        $zip->close();
15✔
210

211
        return $worksheetNames;
15✔
212
    }
213

214
    /**
215
     * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
216
     *
217
     * @return array<int, array{worksheetName: string, lastColumnLetter: string, lastColumnIndex: int, totalRows: int, totalColumns: int, sheetState: string}>
218
     */
219
    public function listWorksheetInfo(string $filename): array
20✔
220
    {
221
        File::assertFile($filename, self::INITIAL_FILE);
20✔
222

223
        $worksheetInfo = [];
17✔
224

225
        $this->zip = $zip = new ZipArchive();
17✔
226
        $zip->open($filename);
17✔
227

228
        $rels = $this->loadZip(self::INITIAL_FILE, Namespaces::RELATIONSHIPS);
17✔
229
        foreach ($rels->Relationship as $relx) {
17✔
230
            $rel = self::getAttributes($relx);
17✔
231
            $relType = (string) $rel['Type'];
17✔
232
            $mainNS = self::REL_TO_MAIN[$relType] ?? Namespaces::MAIN;
17✔
233
            if ($mainNS !== '') {
17✔
234
                $relTarget = (string) $rel['Target'];
17✔
235
                $dir = dirname($relTarget);
17✔
236
                $namespace = dirname($relType);
17✔
237
                $relsWorkbook = $this->loadZip("$dir/_rels/" . basename($relTarget) . '.rels', Namespaces::RELATIONSHIPS);
17✔
238

239
                $worksheets = [];
17✔
240
                foreach ($relsWorkbook->Relationship as $elex) {
17✔
241
                    $ele = self::getAttributes($elex);
17✔
242
                    if (
243
                        ((string) $ele['Type'] === "$namespace/worksheet")
17✔
244
                        || ((string) $ele['Type'] === "$namespace/chartsheet")
17✔
245
                    ) {
246
                        $worksheets[(string) $ele['Id']] = $ele['Target'];
17✔
247
                    }
248
                }
249

250
                $xmlWorkbook = $this->loadZip($relTarget, $mainNS);
17✔
251
                if ($xmlWorkbook->sheets) {
17✔
252
                    $dir = dirname($relTarget);
17✔
253

254
                    foreach ($xmlWorkbook->sheets->sheet as $eleSheet) {
17✔
255
                        $tmpInfo = [
17✔
256
                            'worksheetName' => (string) self::getAttributes($eleSheet)['name'],
17✔
257
                            'lastColumnLetter' => 'A',
17✔
258
                            'lastColumnIndex' => 0,
17✔
259
                            'totalRows' => 0,
17✔
260
                            'totalColumns' => 0,
17✔
261
                        ];
17✔
262
                        $sheetState = (string) (self::getAttributes($eleSheet)['state'] ?? Worksheet::SHEETSTATE_VISIBLE);
17✔
263
                        $tmpInfo['sheetState'] = $sheetState;
17✔
264

265
                        $fileWorksheet = (string) $worksheets[self::getArrayItemString(self::getAttributes($eleSheet, $namespace), 'id')];
17✔
266
                        $fileWorksheetPath = str_starts_with($fileWorksheet, '/') ? substr($fileWorksheet, 1) : "$dir/$fileWorksheet";
17✔
267

268
                        $xml = new XMLReader();
17✔
269
                        $xml->xml(
17✔
270
                            $this->getSecurityScannerOrThrow()
17✔
271
                                ->scan(
17✔
272
                                    $this->getFromZipArchive(
17✔
273
                                        $this->zip,
17✔
274
                                        $fileWorksheetPath
17✔
275
                                    )
17✔
276
                                ),
17✔
277
                            null,
17✔
278
                            $this->parseHuge ? LIBXML_PARSEHUGE : 0
17✔
279
                        );
17✔
280
                        $xml->setParserProperty(2, true);
17✔
281

282
                        $currCells = 0;
17✔
283
                        $currRow = 0;
17✔
284
                        while ($xml->read()) {
17✔
285
                            if ($xml->localName == 'row' && $xml->nodeType == XMLReader::ELEMENT && $xml->namespaceURI === $mainNS) {
17✔
286
                                $row = (int) $xml->getAttribute('r');
17✔
287
                                if ($this->readEmptyCells) {
17✔
288
                                    $tmpInfo['totalRows'] = $row;
17✔
289
                                } else {
290
                                    $currRow = $row;
1✔
291
                                }
292
                                $tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells);
17✔
293
                                $currCells = 0;
17✔
294
                            } elseif ($xml->localName == 'c' && $xml->nodeType == XMLReader::ELEMENT && $xml->namespaceURI === $mainNS) {
17✔
295
                                if ($this->readEmptyCells || !$xml->isEmptyElement) {
17✔
296
                                    if ($currRow !== 0) {
17✔
297
                                        $tmpInfo['totalRows'] = $currRow;
1✔
298
                                        $currRow = 0;
1✔
299
                                    }
300
                                    $cell = $xml->getAttribute('r');
17✔
301
                                    $currCells = $cell ? max($currCells, Coordinate::indexesFromString($cell)[0]) : ($currCells + 1);
17✔
302
                                }
303
                            }
304
                        }
305
                        $tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells);
17✔
306
                        $xml->close();
17✔
307

308
                        $tmpInfo['lastColumnIndex'] = $tmpInfo['totalColumns'] - 1;
17✔
309
                        $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1);
17✔
310

311
                        $worksheetInfo[] = $tmpInfo;
17✔
312
                    }
313
                }
314
            }
315
        }
316

317
        $zip->close();
17✔
318

319
        return $worksheetInfo;
17✔
320
    }
321

322
    private static function castToBoolean(SimpleXMLElement $c): bool
22✔
323
    {
324
        $value = isset($c->v) ? (string) $c->v : null;
22✔
325
        if ($value == '0') {
22✔
326
            return false;
15✔
327
        } elseif ($value == '1') {
18✔
328
            return true;
18✔
329
        }
330

UNCOV
331
        return (bool) $c->v;
×
332
    }
333

334
    private static function castToError(?SimpleXMLElement $c): ?string
189✔
335
    {
336
        return isset($c, $c->v) ? (string) $c->v : null;
189✔
337
    }
338

339
    private static function castToString(?SimpleXMLElement $c): ?string
566✔
340
    {
341
        return isset($c, $c->v) ? (string) $c->v : null;
566✔
342
    }
343

344
    public static function replacePrefixes(string $formula): string
388✔
345
    {
346
        return str_replace(['_xlfn.', '_xlws.'], '', $formula);
388✔
347
    }
348

349
    private function castToFormula(?SimpleXMLElement $c, string $r, string &$cellDataType, mixed &$value, mixed &$calculatedValue, string $castBaseType, bool $updateSharedCells = true): void
376✔
350
    {
351
        if ($c === null) {
376✔
UNCOV
352
            return;
×
353
        }
354
        $attr = $c->f->attributes();
376✔
355
        $cellDataType = DataType::TYPE_FORMULA;
376✔
356
        $formula = self::replacePrefixes((string) $c->f);
376✔
357
        $value = "=$formula";
376✔
358
        $calculatedValue = self::$castBaseType($c);
376✔
359

360
        // Shared formula?
361
        if (isset($attr['t']) && strtolower((string) $attr['t']) == 'shared') {
376✔
362
            $instance = (string) $attr['si'];
219✔
363

364
            if (!isset($this->sharedFormulae[(string) $attr['si']])) {
219✔
365
                $this->sharedFormulae[$instance] = new SharedFormula($r, $value);
219✔
366
            } elseif ($updateSharedCells === true) {
218✔
367
                // It's only worth the overhead of adjusting the shared formula for this cell if we're actually loading
368
                //     the cell, which may not be the case if we're using a read filter.
369
                $master = Coordinate::indexesFromString($this->sharedFormulae[$instance]->master());
218✔
370
                $current = Coordinate::indexesFromString($r);
218✔
371

372
                $difference = [0, 0];
218✔
373
                $difference[0] = $current[0] - $master[0];
218✔
374
                $difference[1] = $current[1] - $master[1];
218✔
375

376
                $value = $this->referenceHelper->updateFormulaReferences($this->sharedFormulae[$instance]->formula(), 'A1', $difference[0], $difference[1]);
218✔
377
            }
378
        }
379
    }
380

381
    private function fileExistsInArchive(ZipArchive $archive, string $fileName = ''): bool
737✔
382
    {
383
        // Root-relative paths
384
        if (str_contains($fileName, '//')) {
737✔
385
            $fileName = substr($fileName, strpos($fileName, '//') + 1);
1✔
386
        }
387
        $fileName = File::realpath($fileName);
737✔
388

389
        // Sadly, some 3rd party xlsx generators don't use consistent case for filenaming
390
        //    so we need to load case-insensitively from the zip file
391

392
        // Apache POI fixes
393
        $contents = $archive->locateName($fileName, ZipArchive::FL_NOCASE);
737✔
394
        if ($contents === false) {
737✔
395
            $contents = $archive->locateName(substr($fileName, 1), ZipArchive::FL_NOCASE);
4✔
396
        }
397

398
        return $contents !== false;
737✔
399
    }
400

401
    private function getFromZipArchive(ZipArchive $archive, string $fileName = ''): string
784✔
402
    {
403
        // Root-relative paths
404
        if (str_contains($fileName, '//')) {
784✔
405
            $fileName = substr($fileName, strpos($fileName, '//') + 1);
2✔
406
        }
407
        // Relative paths generated by dirname($filename) when $filename
408
        // has no path (i.e.files in root of the zip archive)
409
        $fileName = Preg::replace('/^\.\//', '', $fileName);
784✔
410
        $fileName = File::realpath($fileName);
784✔
411

412
        // Sadly, some 3rd party xlsx generators don't use consistent case for filenaming
413
        //    so we need to load case-insensitively from the zip file
414

415
        $contents = $archive->getFromName($fileName, 0, ZipArchive::FL_NOCASE);
784✔
416

417
        // Apache POI fixes
418
        if ($contents === false) {
784✔
419
            $contents = $archive->getFromName(substr($fileName, 1), 0, ZipArchive::FL_NOCASE);
56✔
420
        }
421

422
        // Has the file been saved with Windoze directory separators rather than unix?
423
        if ($contents === false) {
784✔
424
            $contents = $archive->getFromName(str_replace('/', '\\', $fileName), 0, ZipArchive::FL_NOCASE);
53✔
425
        }
426

427
        return ($contents === false) ? '' : $contents;
784✔
428
    }
429

430
    /**
431
     * Loads Spreadsheet from file.
432
     */
433
    protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
756✔
434
    {
435
        File::assertFile($filename, self::INITIAL_FILE);
756✔
436

437
        // Initialisations
438
        $excel = $this->newSpreadsheet();
753✔
439
        $excel->setValueBinder($this->valueBinder);
753✔
440
        $excel->removeSheetByIndex(0);
753✔
441
        $addingFirstCellStyleXf = true;
753✔
442
        $addingFirstCellXf = true;
753✔
443

444
        /** @var mixed[][][][] */
445
        $unparsedLoadedData = [];
753✔
446

447
        $this->zip = $zip = new ZipArchive();
753✔
448
        $zip->open($filename);
753✔
449

450
        //    Read the theme first, because we need the colour scheme when reading the styles
451
        [$workbookBasename, $xmlNamespaceBase] = $this->getWorkbookBaseName();
753✔
452
        $drawingNS = self::REL_TO_DRAWING[$xmlNamespaceBase] ?? Namespaces::DRAWINGML;
753✔
453
        $chartNS = self::REL_TO_CHART[$xmlNamespaceBase] ?? Namespaces::CHART;
753✔
454
        $wbRels = $this->loadZip("xl/_rels/{$workbookBasename}.rels", Namespaces::RELATIONSHIPS);
753✔
455
        $theme = null;
753✔
456
        $this->styleReader = new Styles();
753✔
457
        foreach ($wbRels->Relationship as $relx) {
753✔
458
            $rel = self::getAttributes($relx);
752✔
459
            $relTarget = (string) $rel['Target'];
752✔
460
            if (str_starts_with($relTarget, '/xl/')) {
752✔
461
                $relTarget = substr($relTarget, 4);
12✔
462
            }
463
            switch ($rel['Type']) {
752✔
464
                case "$xmlNamespaceBase/sheetMetadata":
752✔
465
                    if ($this->fileExistsInArchive($zip, "xl/{$relTarget}")) {
38✔
466
                        $excel->returnArrayAsArray();
38✔
467
                    }
468

469
                    break;
38✔
470
                case "$xmlNamespaceBase/theme":
752✔
471
                    if (!$this->fileExistsInArchive($zip, "xl/{$relTarget}")) {
735✔
472
                        break; // issue3770
3✔
473
                    }
474
                    $themeOrderArray = ['lt1', 'dk1', 'lt2', 'dk2'];
732✔
475
                    $themeOrderAdditional = count($themeOrderArray);
732✔
476

477
                    $xmlTheme = $this->loadZip("xl/{$relTarget}", $drawingNS);
732✔
478
                    $xmlThemeName = self::getAttributes($xmlTheme);
732✔
479
                    $xmlTheme = $xmlTheme->children($drawingNS);
732✔
480
                    $themeName = (string) $xmlThemeName['name'];
732✔
481

482
                    $colourScheme = self::getAttributes($xmlTheme->themeElements->clrScheme);
732✔
483
                    $colourSchemeName = (string) $colourScheme['name'];
732✔
484
                    $excel->getTheme()->setThemeColorName($colourSchemeName);
732✔
485
                    $colourScheme = $xmlTheme->themeElements->clrScheme->children($drawingNS);
732✔
486

487
                    $themeColours = [];
732✔
488
                    foreach ($colourScheme as $k => $xmlColour) {
732✔
489
                        $themePos = array_search($k, $themeOrderArray);
732✔
490
                        if ($themePos === false) {
732✔
491
                            $themePos = $themeOrderAdditional++;
732✔
492
                        }
493
                        if (isset($xmlColour->sysClr)) {
732✔
494
                            $xmlColourData = self::getAttributes($xmlColour->sysClr);
701✔
495
                            $themeColours[$themePos] = (string) $xmlColourData['lastClr'];
701✔
496
                            $excel->getTheme()->setThemeColor($k, (string) $xmlColourData['lastClr']);
701✔
497
                        } elseif (isset($xmlColour->srgbClr)) {
732✔
498
                            $xmlColourData = self::getAttributes($xmlColour->srgbClr);
732✔
499
                            $themeColours[$themePos] = (string) $xmlColourData['val'];
732✔
500
                            $excel->getTheme()->setThemeColor($k, (string) $xmlColourData['val']);
732✔
501
                        }
502
                    }
503
                    $theme = new Theme($themeName, $colourSchemeName, $themeColours);
732✔
504
                    $this->styleReader->setTheme($theme);
732✔
505

506
                    $fontScheme = self::getAttributes($xmlTheme->themeElements->fontScheme);
732✔
507
                    $fontSchemeName = (string) $fontScheme['name'];
732✔
508
                    $excel->getTheme()->setThemeFontName($fontSchemeName);
732✔
509
                    $majorFonts = [];
732✔
510
                    $minorFonts = [];
732✔
511
                    $fontScheme = $xmlTheme->themeElements->fontScheme->children($drawingNS);
732✔
512
                    $majorLatin = self::getAttributes($fontScheme->majorFont->latin)['typeface'] ?? '';
732✔
513
                    $majorEastAsian = self::getAttributes($fontScheme->majorFont->ea)['typeface'] ?? '';
732✔
514
                    $majorComplexScript = self::getAttributes($fontScheme->majorFont->cs)['typeface'] ?? '';
732✔
515
                    $minorLatin = self::getAttributes($fontScheme->minorFont->latin)['typeface'] ?? '';
732✔
516
                    $minorEastAsian = self::getAttributes($fontScheme->minorFont->ea)['typeface'] ?? '';
732✔
517
                    $minorComplexScript = self::getAttributes($fontScheme->minorFont->cs)['typeface'] ?? '';
732✔
518

519
                    foreach ($fontScheme->majorFont->font as $xmlFont) {
732✔
520
                        $fontAttributes = self::getAttributes($xmlFont);
698✔
521
                        $script = (string) ($fontAttributes['script'] ?? '');
698✔
522
                        if (!empty($script)) {
698✔
523
                            $majorFonts[$script] = (string) ($fontAttributes['typeface'] ?? '');
698✔
524
                        }
525
                    }
526
                    foreach ($fontScheme->minorFont->font as $xmlFont) {
732✔
527
                        $fontAttributes = self::getAttributes($xmlFont);
698✔
528
                        $script = (string) ($fontAttributes['script'] ?? '');
698✔
529
                        if (!empty($script)) {
698✔
530
                            $minorFonts[$script] = (string) ($fontAttributes['typeface'] ?? '');
698✔
531
                        }
532
                    }
533
                    $excel->getTheme()->setMajorFontValues($majorLatin, $majorEastAsian, $majorComplexScript, $majorFonts);
732✔
534
                    $excel->getTheme()->setMinorFontValues($minorLatin, $minorEastAsian, $minorComplexScript, $minorFonts);
732✔
535

536
                    break;
732✔
537
            }
538
        }
539

540
        $rels = $this->loadZip(self::INITIAL_FILE, Namespaces::RELATIONSHIPS);
753✔
541

542
        $propertyReader = new PropertyReader($this->getSecurityScannerOrThrow(), $excel->getProperties());
753✔
543
        $charts = $chartDetails = [];
753✔
544
        foreach ($rels->Relationship as $relx) {
753✔
545
            $rel = self::getAttributes($relx);
753✔
546
            $relTarget = (string) $rel['Target'];
753✔
547
            // issue 3553
548
            if ($relTarget[0] === '/') {
753✔
549
                $relTarget = substr($relTarget, 1);
7✔
550
            }
551
            $relType = (string) $rel['Type'];
753✔
552
            $mainNS = self::REL_TO_MAIN[$relType] ?? Namespaces::MAIN;
753✔
553
            switch ($relType) {
554
                case Namespaces::CORE_PROPERTIES:
747✔
555
                    $propertyReader->readCoreProperties($this->getFromZipArchive($zip, $relTarget));
732✔
556

557
                    break;
732✔
558
                case "$xmlNamespaceBase/extended-properties":
753✔
559
                    $propertyReader->readExtendedProperties($this->getFromZipArchive($zip, $relTarget));
723✔
560

561
                    break;
723✔
562
                case "$xmlNamespaceBase/custom-properties":
753✔
563
                    $propertyReader->readCustomProperties($this->getFromZipArchive($zip, $relTarget));
70✔
564

565
                    break;
70✔
566
                    //Ribbon
567
                case Namespaces::EXTENSIBILITY:
753✔
568
                    $customUI = $relTarget;
2✔
569
                    if ($customUI) {
2✔
570
                        $this->readRibbon($excel, $customUI, $zip);
2✔
571
                    }
572

573
                    break;
2✔
574
                case "$xmlNamespaceBase/officeDocument":
753✔
575
                    $dir = dirname($relTarget);
753✔
576

577
                    // Do not specify namespace in next stmt - do it in Xpath
578
                    $relsWorkbook = $this->loadZip("$dir/_rels/" . basename($relTarget) . '.rels', Namespaces::RELATIONSHIPS);
753✔
579
                    $relsWorkbook->registerXPathNamespace('rel', Namespaces::RELATIONSHIPS);
753✔
580

581
                    $worksheets = [];
753✔
582
                    $macros = $customUI = null;
753✔
583
                    foreach ($relsWorkbook->Relationship as $elex) {
753✔
584
                        $ele = self::getAttributes($elex);
753✔
585
                        switch ($ele['Type']) {
753✔
586
                            case Namespaces::WORKSHEET:
747✔
587
                            case Namespaces::PURL_WORKSHEET:
747✔
588
                                $worksheets[(string) $ele['Id']] = $ele['Target'];
753✔
589

590
                                break;
753✔
591
                            case Namespaces::CHARTSHEET:
747✔
592
                                if ($this->includeCharts === true) {
2✔
593
                                    $worksheets[(string) $ele['Id']] = $ele['Target'];
1✔
594
                                }
595

596
                                break;
2✔
597
                                // a vbaProject ? (: some macros)
598
                            case Namespaces::VBA:
747✔
599
                                $macros = $ele['Target'];
3✔
600

601
                                break;
3✔
602
                        }
603
                    }
604

605
                    if ($macros !== null) {
753✔
606
                        $macrosCode = $this->getFromZipArchive($zip, 'xl/vbaProject.bin'); //vbaProject.bin always in 'xl' dir and always named vbaProject.bin
3✔
607
                        if (!empty($macrosCode)) {
3✔
608
                            $excel->setMacrosCode($macrosCode);
3✔
609
                            $excel->setHasMacros(true);
3✔
610
                            //short-circuit : not reading vbaProject.bin.rel to get Signature =>allways vbaProjectSignature.bin in 'xl' dir
611
                            $Certificate = $this->getFromZipArchive($zip, 'xl/vbaProjectSignature.bin');
3✔
612
                            $excel->setMacrosCertificate($Certificate);
3✔
613
                        }
614
                    }
615

616
                    $relType = "rel:Relationship[@Type='"
753✔
617
                        . "$xmlNamespaceBase/styles"
753✔
618
                        . "']";
753✔
619
                    /** @var ?SimpleXMLElement */
620
                    $xpath = self::getArrayItem(self::xpathNoFalse($relsWorkbook, $relType));
753✔
621

622
                    if ($xpath === null) {
753✔
623
                        $xmlStyles = self::testSimpleXml(null);
1✔
624
                    } else {
625
                        $stylesTarget = (string) $xpath['Target'];
753✔
626
                        $stylesTarget = str_starts_with($stylesTarget, '/') ? substr($stylesTarget, 1) : "$dir/$stylesTarget";
753✔
627
                        $xmlStyles = $this->loadZip($stylesTarget, $mainNS);
753✔
628
                    }
629

630
                    $palette = self::extractPalette($xmlStyles);
753✔
631
                    $this->styleReader->setWorkbookPalette($palette);
753✔
632
                    $fills = self::extractStyles($xmlStyles, 'fills', 'fill');
753✔
633
                    $fonts = self::extractStyles($xmlStyles, 'fonts', 'font');
753✔
634
                    $borders = self::extractStyles($xmlStyles, 'borders', 'border');
753✔
635
                    $xfTags = self::extractStyles($xmlStyles, 'cellXfs', 'xf');
753✔
636
                    $cellXfTags = self::extractStyles($xmlStyles, 'cellStyleXfs', 'xf');
753✔
637

638
                    $styles = [];
753✔
639
                    $cellStyles = [];
753✔
640
                    $numFmts = null;
753✔
641
                    if (/*$xmlStyles && */ $xmlStyles->numFmts[0]) {
753✔
642
                        $numFmts = $xmlStyles->numFmts[0];
263✔
643
                    }
644
                    if (isset($numFmts)) {
753✔
645
                        /** @var SimpleXMLElement $numFmts */
646
                        $numFmts->registerXPathNamespace('sml', $mainNS);
263✔
647
                    }
648
                    $this->styleReader->setNamespace($mainNS);
753✔
649
                    if (!$this->readDataOnly/* && $xmlStyles*/) {
753✔
650
                        foreach ($xfTags as $xfTag) {
750✔
651
                            /** @var SimpleXMLElement $xfTag */
652
                            $xf = self::getAttributes($xfTag);
750✔
653
                            $numFmt = null;
750✔
654

655
                            if ($xf['numFmtId']) {
750✔
656
                                if (isset($numFmts)) {
748✔
657
                                    /** @var ?SimpleXMLElement */
658
                                    $tmpNumFmt = self::getArrayItem($numFmts->xpath("sml:numFmt[@numFmtId=$xf[numFmtId]]"));
263✔
659

660
                                    if (isset($tmpNumFmt['formatCode'])) {
263✔
661
                                        $numFmt = (string) $tmpNumFmt['formatCode'];
262✔
662
                                    }
663
                                }
664

665
                                // We shouldn't override any of the built-in MS Excel values (values below id 164)
666
                                //  But there's a lot of naughty homebrew xlsx writers that do use "reserved" id values that aren't actually used
667
                                //  So we make allowance for them rather than lose formatting masks
668
                                if (
669
                                    $numFmt === null
748✔
670
                                    && (int) $xf['numFmtId'] < 164
748✔
671
                                    && NumberFormat::builtInFormatCode((int) $xf['numFmtId']) !== ''
748✔
672
                                ) {
673
                                    $numFmt = NumberFormat::builtInFormatCode((int) $xf['numFmtId']);
726✔
674
                                }
675
                            }
676
                            $quotePrefix = (bool) (string) ($xf['quotePrefix'] ?? '');
750✔
677

678
                            $style = (object) [
750✔
679
                                'numFmt' => $numFmt ?? NumberFormat::FORMAT_GENERAL,
750✔
680
                                'font' => $fonts[(int) ($xf['fontId'])],
750✔
681
                                'fill' => $fills[(int) ($xf['fillId'])],
750✔
682
                                'border' => $borders[(int) ($xf['borderId'])],
750✔
683
                                'alignment' => $xfTag->alignment,
750✔
684
                                'protection' => $xfTag->protection,
750✔
685
                                'quotePrefix' => $quotePrefix,
750✔
686
                            ];
750✔
687
                            $styles[] = $style;
750✔
688

689
                            // add style to cellXf collection
690
                            $objStyle = new Style();
750✔
691
                            $this->styleReader
750✔
692
                                ->readStyle($objStyle, $style);
750✔
693
                            foreach ($this->styleReader->getFontCharsets() as $fontName => $charset) {
750✔
694
                                $excel->addFontCharset($fontName, $charset);
33✔
695
                            }
696
                            if ($addingFirstCellXf) {
750✔
697
                                $excel->removeCellXfByIndex(0); // remove the default style
750✔
698
                                $addingFirstCellXf = false;
750✔
699
                            }
700
                            $excel->addCellXf($objStyle);
750✔
701
                        }
702

703
                        foreach ($cellXfTags as $xfTag) {
750✔
704
                            /** @var SimpleXMLElement $xfTag */
705
                            $xf = self::getAttributes($xfTag);
749✔
706
                            $numFmt = NumberFormat::FORMAT_GENERAL;
749✔
707
                            if ($numFmts && $xf['numFmtId']) {
749✔
708
                                /** @var ?SimpleXMLElement */
709
                                $tmpNumFmt = self::getArrayItem($numFmts->xpath("sml:numFmt[@numFmtId=$xf[numFmtId]]"));
263✔
710
                                if (isset($tmpNumFmt['formatCode'])) {
263✔
711
                                    $numFmt = (string) $tmpNumFmt['formatCode'];
41✔
712
                                } elseif ((int) $xf['numFmtId'] < 165) {
261✔
713
                                    $numFmt = NumberFormat::builtInFormatCode((int) $xf['numFmtId']);
261✔
714
                                }
715
                            }
716

717
                            $quotePrefix = (bool) (string) ($xf['quotePrefix'] ?? '');
749✔
718

719
                            $cellStyle = (object) [
749✔
720
                                'numFmt' => $numFmt,
749✔
721
                                'font' => $fonts[(int) ($xf['fontId'])],
749✔
722
                                'fill' => $fills[((int) $xf['fillId'])],
749✔
723
                                'border' => $borders[(int) ($xf['borderId'])],
749✔
724
                                'alignment' => $xfTag->alignment,
749✔
725
                                'protection' => $xfTag->protection,
749✔
726
                                'quotePrefix' => $quotePrefix,
749✔
727
                            ];
749✔
728
                            $cellStyles[] = $cellStyle;
749✔
729

730
                            // add style to cellStyleXf collection
731
                            $objStyle = new Style();
749✔
732
                            $this->styleReader->readStyle($objStyle, $cellStyle);
749✔
733
                            if ($addingFirstCellStyleXf) {
749✔
734
                                $excel->removeCellStyleXfByIndex(0); // remove the default style
749✔
735
                                $addingFirstCellStyleXf = false;
749✔
736
                            }
737
                            $excel->addCellStyleXf($objStyle);
749✔
738
                        }
739
                    }
740
                    $this->styleReader->setStyleXml($xmlStyles);
753✔
741
                    $this->styleReader->setNamespace($mainNS);
753✔
742
                    $this->styleReader->setStyleBaseData($theme, $styles, $cellStyles);
753✔
743
                    $dxfs = $this->styleReader->dxfs($this->readDataOnly);
753✔
744
                    $tableStyles = $this->styleReader->tableStyles($this->readDataOnly);
753✔
745
                    $styles = $this->styleReader->styles();
753✔
746

747
                    // Read content after setting the styles
748
                    $sharedStrings = [];
753✔
749
                    $relType = "rel:Relationship[@Type='"
753✔
750
                        //. Namespaces::SHARED_STRINGS
753✔
751
                        . "$xmlNamespaceBase/sharedStrings"
753✔
752
                        . "']";
753✔
753
                    /** @var ?SimpleXMLElement */
754
                    $xpath = self::getArrayItem($relsWorkbook->xpath($relType));
753✔
755

756
                    if ($xpath) {
753✔
757
                        $sharedStringsTarget = (string) $xpath['Target'];
684✔
758
                        $sharedStringsTarget = str_starts_with($sharedStringsTarget, '/') ? substr($sharedStringsTarget, 1) : "$dir/$sharedStringsTarget";
684✔
759
                        $xmlStrings = $this->loadZip($sharedStringsTarget, $mainNS);
684✔
760
                        if (isset($xmlStrings->si)) {
682✔
761
                            foreach ($xmlStrings->si as $val) {
540✔
762
                                if (isset($val->t)) {
540✔
763
                                    $sharedStrings[] = StringHelper::controlCharacterOOXML2PHP((string) $val->t);
536✔
764
                                } elseif (isset($val->r)) {
45✔
765
                                    $sharedStrings[] = $this->parseRichText($val);
45✔
766
                                } else {
767
                                    $sharedStrings[] = '';
1✔
768
                                }
769
                            }
770
                        }
771
                    }
772

773
                    $xmlWorkbook = $this->loadZipNoNamespace($relTarget, $mainNS);
751✔
774
                    $xmlWorkbookNS = $this->loadZip($relTarget, $mainNS);
746✔
775

776
                    // Set base date
777
                    $excel->setExcelCalendar(Date::CALENDAR_WINDOWS_1900);
746✔
778
                    if ($xmlWorkbookNS->workbookPr) {
746✔
779
                        Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900);
737✔
780
                        $attrs1904 = self::getAttributes($xmlWorkbookNS->workbookPr);
737✔
781
                        if (isset($attrs1904['date1904'])) {
737✔
782
                            if (self::boolean((string) $attrs1904['date1904'])) {
27✔
783
                                Date::setExcelCalendar(Date::CALENDAR_MAC_1904);
4✔
784
                                $excel->setExcelCalendar(Date::CALENDAR_MAC_1904);
4✔
785
                            }
786
                        }
787
                    }
788

789
                    // Set protection
790
                    $this->readProtection($excel, $xmlWorkbook);
746✔
791

792
                    $sheetId = 0; // keep track of new sheet id in final workbook
746✔
793
                    $oldSheetId = -1; // keep track of old sheet id in final workbook
746✔
794
                    $countSkippedSheets = 0; // keep track of number of skipped sheets
746✔
795
                    $mapSheetId = []; // mapping of sheet ids from old to new
746✔
796

797
                    $charts = $chartDetails = [];
746✔
798

799
                    // Add richData (contains relation of in-cell images)
800
                    $richData = [];
746✔
801
                    $relationsFileName = $dir . '/richData/_rels/richValueRel.xml.rels';
746✔
802
                    if ($zip->locateName($relationsFileName)) {
746✔
803
                        $relsWorksheet = $this->loadZip($relationsFileName, Namespaces::RELATIONSHIPS);
6✔
804
                        foreach ($relsWorksheet->Relationship as $elex) {
6✔
805
                            $ele = self::getAttributes($elex);
6✔
806
                            if ($ele['Type'] == Namespaces::IMAGE) {
6✔
807
                                $richData['image'][(string) $ele['Id']] = (string) $ele['Target'];
6✔
808
                            }
809
                        }
810
                    }
811

812
                    $sheetCreated = false;
746✔
813
                    if ($xmlWorkbookNS->sheets) {
746✔
814
                        foreach ($xmlWorkbookNS->sheets->sheet as $eleSheet) {
746✔
815
                            $eleSheetAttr = self::getAttributes($eleSheet);
746✔
816
                            ++$oldSheetId;
746✔
817

818
                            // Check if sheet should be skipped
819
                            if (is_array($this->loadSheetsOnly) && !in_array((string) $eleSheetAttr['name'], $this->loadSheetsOnly)) {
746✔
820
                                ++$countSkippedSheets;
9✔
821
                                $mapSheetId[$oldSheetId] = null;
9✔
822

823
                                continue;
9✔
824
                            }
825

826
                            $sheetReferenceId = self::getArrayItemString(self::getAttributes($eleSheet, $xmlNamespaceBase), 'id');
742✔
827
                            if (isset($worksheets[$sheetReferenceId]) === false) {
742✔
828
                                ++$countSkippedSheets;
1✔
829
                                $mapSheetId[$oldSheetId] = null;
1✔
830

831
                                continue;
1✔
832
                            }
833
                            // Map old sheet id in original workbook to new sheet id.
834
                            // They will differ if loadSheetsOnly() is being used
835
                            $mapSheetId[$oldSheetId] = $oldSheetId - $countSkippedSheets;
742✔
836

837
                            // Load sheet
838
                            $docSheet = $excel->createSheet();
742✔
839
                            $sheetCreated = true;
742✔
840
                            //    Use false for $updateFormulaCellReferences to prevent adjustment of worksheet
841
                            //        references in formula cells... during the load, all formulae should be correct,
842
                            //        and we're simply bringing the worksheet name in line with the formula, not the
843
                            //        reverse
844
                            $docSheet->setTitle((string) $eleSheetAttr['name'], false, false);
742✔
845

846
                            $fileWorksheet = (string) $worksheets[$sheetReferenceId];
742✔
847
                            // issue 3665 adds test for /.
848
                            // This broke XlsxRootZipFilesTest,
849
                            //  but Excel reports an error with that file.
850
                            //  Testing dir for . avoids this problem.
851
                            //  It might be better just to drop the test.
852
                            if ($fileWorksheet[0] == '/' && $dir !== '.') {
742✔
853
                                $fileWorksheet = substr($fileWorksheet, strlen($dir) + 2);
12✔
854
                            }
855
                            $xmlSheet = $this->loadZipNoNamespace("$dir/$fileWorksheet", $mainNS);
742✔
856
                            $xmlSheetNS = $this->loadZip("$dir/$fileWorksheet", $mainNS);
742✔
857

858
                            // Shared Formula table is unique to each Worksheet, so we need to reset it here
859
                            $this->sharedFormulae = [];
742✔
860

861
                            if (isset($eleSheetAttr['state']) && (string) $eleSheetAttr['state'] != '') {
742✔
862
                                $docSheet->setSheetState((string) $eleSheetAttr['state']);
53✔
863
                            }
864
                            if ($xmlSheetNS) {
742✔
865
                                $xmlSheetMain = $xmlSheetNS->children($mainNS);
742✔
866
                                // Setting Conditional Styles adjusts selected cells, so we need to execute this
867
                                //    before reading the sheet view data to get the actual selected cells
868
                                if (!$this->readDataOnly && ($xmlSheet->conditionalFormatting)) {
742✔
869
                                    (new ConditionalStyles($docSheet, $xmlSheet, $dxfs, $this->styleReader))->load();
224✔
870
                                }
871
                                if (!$this->readDataOnly && $xmlSheet->extLst) {
742✔
872
                                    (new ConditionalStyles($docSheet, $xmlSheet, $dxfs, $this->styleReader))->loadFromExt();
202✔
873
                                }
874
                                if (isset($xmlSheetMain->sheetViews, $xmlSheetMain->sheetViews->sheetView)) {
742✔
875
                                    $sheetViews = new SheetViews($xmlSheetMain->sheetViews->sheetView, $docSheet);
739✔
876
                                    $sheetViews->load();
739✔
877
                                }
878

879
                                $sheetViewOptions = new SheetViewOptions($docSheet, $xmlSheetNS);
742✔
880
                                $sheetViewOptions->load($this->readDataOnly, $this->styleReader);
742✔
881

882
                                (new ColumnAndRowAttributes($docSheet, $xmlSheetNS))
742✔
883
                                    ->load($this->getReadFilter(), $this->readDataOnly, $this->ignoreRowsWithNoCells);
742✔
884
                            }
885

886
                            $holdSelectedCells = $docSheet->getSelectedCells();
742✔
887
                            if ($xmlSheetNS && $xmlSheetNS->sheetData && $xmlSheetNS->sheetData->row) {
742✔
888
                                $cIndex = 1; // Cell Start from 1
692✔
889
                                foreach ($xmlSheetNS->sheetData->row as $row) {
692✔
890
                                    $rowIndex = 1;
692✔
891
                                    foreach ($row->c as $c) {
692✔
892
                                        $cAttr = self::getAttributes($c);
691✔
893
                                        $r = (string) $cAttr['r'];
691✔
894
                                        if ($r == '') {
691✔
895
                                            $r = Coordinate::stringFromColumnIndex($rowIndex) . $cIndex;
2✔
896
                                        }
897
                                        $cellDataType = (string) $cAttr['t'];
691✔
898
                                        $originalCellDataTypeNumeric = $cellDataType === '';
691✔
899
                                        $value = null;
691✔
900
                                        $calculatedValue = null;
691✔
901

902
                                        // Read cell?
903
                                        $coordinates = Coordinate::coordinateFromString($r);
691✔
904

905
                                        if (!$this->getReadFilter()->readCell($coordinates[0], (int) $coordinates[1], $docSheet->getTitle())) {
691✔
906
                                            // Normally, just testing for the f attribute should identify this cell as containing a formula
907
                                            // that we need to read, even though it is outside of the filter range, in case it is a shared formula.
908
                                            // But in some cases, this attribute isn't set; so we need to delve a level deeper and look at
909
                                            // whether or not the cell has a child formula element that is shared.
910
                                            if (isset($cAttr->f) || (isset($c->f, $c->f->attributes()['t']) && strtolower((string) $c->f->attributes()['t']) === 'shared')) {
4✔
UNCOV
911
                                                $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToError', false);
×
912
                                            }
913
                                            ++$rowIndex;
4✔
914

915
                                            continue;
4✔
916
                                        }
917

918
                                        // Read cell!
919
                                        $useFormula = isset($c->f)
691✔
920
                                            && ((string) $c->f !== '' || (isset($c->f->attributes()['t']) && strtolower((string) $c->f->attributes()['t']) === 'shared'));
691✔
921
                                        switch ($cellDataType) {
922
                                            case DataType::TYPE_STRING:
20✔
923
                                                if ((string) $c->v != '') {
535✔
924
                                                    $value = $sharedStrings[(int) ($c->v)];
535✔
925

926
                                                    if ($value instanceof RichText) {
535✔
927
                                                        $value = clone $value;
37✔
928
                                                    }
929
                                                } else {
930
                                                    $value = '';
17✔
931
                                                }
932

933
                                                break;
535✔
934
                                            case DataType::TYPE_BOOL:
16✔
935
                                                if (!$useFormula) {
22✔
936
                                                    if (isset($c->v)) {
16✔
937
                                                        $value = self::castToBoolean($c);
16✔
938
                                                    } else {
939
                                                        $value = null;
1✔
940
                                                        $cellDataType = DataType::TYPE_NULL;
1✔
941
                                                    }
942
                                                } else {
943
                                                    // Formula
944
                                                    $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToBoolean');
6✔
945
                                                    self::storeFormulaAttributes($c->f, $docSheet, $r);
6✔
946
                                                }
947

948
                                                break;
22✔
949
                                            case DataType::TYPE_STRING2:
16✔
950
                                                if ($useFormula) {
226✔
951
                                                    $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToString');
224✔
952
                                                    self::storeFormulaAttributes($c->f, $docSheet, $r);
224✔
953
                                                } else {
954
                                                    $value = self::castToString($c);
3✔
955
                                                }
956

957
                                                break;
226✔
958
                                            case DataType::TYPE_INLINE:
16✔
959
                                                if ($useFormula) {
18✔
UNCOV
960
                                                    $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToError');
×
UNCOV
961
                                                    self::storeFormulaAttributes($c->f, $docSheet, $r);
×
962
                                                } else {
963
                                                    $value = $this->parseRichText($c->is);
18✔
964
                                                }
965

966
                                                break;
18✔
967
                                            case DataType::TYPE_ERROR:
16✔
968
                                                if (isset($cAttr->vm, $richData['image']['rId' . $cAttr->vm]) && !$useFormula) {
195✔
969
                                                    $imagePath = $dir . '/' . str_replace('../', '', $richData['image']['rId' . $cAttr->vm]);
6✔
970
                                                    $objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing();
6✔
971
                                                    $objDrawing->setPath(
6✔
972
                                                        'zip://' . File::realpath($filename) . '#' . $imagePath,
6✔
973
                                                        false,
6✔
974
                                                        $zip
6✔
975
                                                    );
6✔
976

977
                                                    $objDrawing->setCoordinates($r);
6✔
978
                                                    $objDrawing->setResizeProportional(false);
6✔
979
                                                    $objDrawing->setInCell(true);
6✔
980
                                                    $objDrawing->setWorksheet($docSheet);
6✔
981

982
                                                    $value = $objDrawing;
6✔
983
                                                    $cellDataType = DataType::TYPE_DRAWING_IN_CELL;
6✔
984
                                                    $c->t = DataType::TYPE_ERROR;
6✔
985

986
                                                    break;
6✔
987
                                                }
988

989
                                                if (!$useFormula) {
189✔
UNCOV
990
                                                    $value = self::castToError($c);
×
991
                                                } else {
992
                                                    // Formula
993
                                                    $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToError');
189✔
994
                                                    $eattr = $c->attributes();
189✔
995
                                                    if (isset($eattr['vm'])) {
189✔
996
                                                        if ($calculatedValue === ExcelError::VALUE()) {
1✔
997
                                                            $calculatedValue = ExcelError::SPILL();
1✔
998
                                                        }
999
                                                    }
1000
                                                }
1001

1002
                                                break;
189✔
1003
                                            default:
1004
                                                if (!$useFormula) {
554✔
1005
                                                    $value = self::castToString($c);
545✔
1006
                                                    if (is_numeric($value)) {
545✔
1007
                                                        $value += 0;
509✔
1008
                                                        $cellDataType = DataType::TYPE_NUMERIC;
509✔
1009
                                                    }
1010
                                                } else {
1011
                                                    // Formula
1012
                                                    $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToString');
351✔
1013
                                                    if (is_numeric($calculatedValue)) {
351✔
1014
                                                        $calculatedValue += 0;
348✔
1015
                                                    }
1016
                                                    self::storeFormulaAttributes($c->f, $docSheet, $r);
351✔
1017
                                                }
1018

1019
                                                break;
554✔
1020
                                        }
1021

1022
                                        // read empty cells or the cells are not empty
1023
                                        if ($this->readEmptyCells || ($value !== null && $value !== '')) {
691✔
1024
                                            // Rich text?
1025
                                            if ($value instanceof RichText && $this->readDataOnly) {
691✔
1026
                                                $value = $value->getPlainText();
1✔
1027
                                            }
1028

1029
                                            $cell = $docSheet->getCell($r);
691✔
1030
                                            // Assign value
1031
                                            if ($cellDataType != '') {
691✔
1032
                                                // it is possible, that datatype is numeric but with an empty string, which result in an error
1033
                                                if ($cellDataType === DataType::TYPE_NUMERIC && ($value === '' || $value === null)) {
679✔
1034
                                                    $cellDataType = DataType::TYPE_NULL;
1✔
1035
                                                }
1036
                                                if ($cellDataType !== DataType::TYPE_NULL) {
679✔
1037
                                                    $cell->setValueExplicit($value, $cellDataType);
679✔
1038
                                                }
1039
                                            } else {
1040
                                                $cell->setValue($value);
307✔
1041
                                            }
1042
                                            if ($calculatedValue !== null) {
691✔
1043
                                                $cell->setCalculatedValue($calculatedValue, $originalCellDataTypeNumeric);
367✔
1044
                                            }
1045

1046
                                            // Style information?
1047
                                            if (!$this->readDataOnly) {
691✔
1048
                                                $cAttrS = (int) ($cAttr['s'] ?? 0);
688✔
1049
                                                // no style index means 0, it seems
1050
                                                $cAttrS = isset($styles[$cAttrS]) ? $cAttrS : 0;
688✔
1051
                                                $cell->setXfIndex($cAttrS);
688✔
1052
                                                // issue 3495
1053
                                                if ($cellDataType === DataType::TYPE_FORMULA && $styles[$cAttrS]->quotePrefix === true) { //* @phpstan-ignore-line
688✔
1054
                                                    $holdSelected = $docSheet->getSelectedCells();
2✔
1055
                                                    $cell->getStyle()->setQuotePrefix(false);
2✔
1056
                                                    $docSheet->setSelectedCells($holdSelected);
2✔
1057
                                                }
1058
                                            }
1059
                                        }
1060
                                        ++$rowIndex;
691✔
1061
                                    }
1062
                                    ++$cIndex;
692✔
1063
                                }
1064
                            }
1065
                            $docSheet->setSelectedCells($holdSelectedCells);
742✔
1066
                            if (!$this->readDataOnly && $xmlSheetNS && $xmlSheetNS->ignoredErrors) {
742✔
1067
                                foreach ($xmlSheetNS->ignoredErrors->ignoredError as $ignoredError) {
4✔
1068
                                    $this->processIgnoredErrors($ignoredError, $docSheet);
4✔
1069
                                }
1070
                            }
1071

1072
                            if (!$this->readDataOnly && $xmlSheetNS && $xmlSheetNS->sheetProtection) {
742✔
1073
                                $protAttr = $xmlSheetNS->sheetProtection->attributes() ?? [];
70✔
1074
                                foreach ($protAttr as $key => $value) {
70✔
1075
                                    $method = 'set' . ucfirst($key);
70✔
1076
                                    $docSheet->getProtection()->$method(self::boolean((string) $value));
70✔
1077
                                }
1078
                            }
1079

1080
                            if ($xmlSheet) {
742✔
1081
                                $this->readSheetProtection($docSheet, $xmlSheet);
732✔
1082
                            }
1083

1084
                            if ($this->readDataOnly === false) {
742✔
1085
                                $this->readAutoFilter($xmlSheetNS, $docSheet);
739✔
1086
                                $this->readBackgroundImage($xmlSheetNS, $docSheet, dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels');
739✔
1087
                            }
1088

1089
                            $this->readTables($xmlSheetNS, $docSheet, $dir, $fileWorksheet, $zip, $mainNS, $tableStyles, $dxfs);
742✔
1090

1091
                            if ($xmlSheetNS && $xmlSheetNS->mergeCells && $xmlSheetNS->mergeCells->mergeCell && !$this->readDataOnly) {
742✔
1092
                                foreach ($xmlSheetNS->mergeCells->mergeCell as $mergeCellx) {
70✔
1093
                                    $mergeCell = $mergeCellx->attributes();
70✔
1094
                                    $mergeRef = (string) ($mergeCell['ref'] ?? '');
70✔
1095
                                    if (str_contains($mergeRef, ':')) {
70✔
1096
                                        $docSheet->mergeCells($mergeRef, Worksheet::MERGE_CELL_CONTENT_HIDE);
70✔
1097
                                    }
1098
                                }
1099
                            }
1100

1101
                            if ($xmlSheet && !$this->readDataOnly) {
742✔
1102
                                $unparsedLoadedData = (new PageSetup($docSheet, $xmlSheet))->load($unparsedLoadedData);
729✔
1103
                            }
1104

1105
                            if (isset($xmlSheet->extLst->ext)) {
742✔
1106
                                foreach ($xmlSheet->extLst->ext as $extlst) {
202✔
1107
                                    $extAttrs = $extlst->attributes() ?? [];
202✔
1108
                                    $extUri = (string) ($extAttrs['uri'] ?? '');
202✔
1109
                                    if ($extUri !== '{CCE6A557-97BC-4b89-ADB6-D9C93CAAB3DF}') {
202✔
1110
                                        continue;
196✔
1111
                                    }
1112
                                    // Create dataValidations node if does not exists, maybe is better inside the foreach ?
1113
                                    if (!$xmlSheet->dataValidations) {
6✔
1114
                                        $xmlSheet->addChild('dataValidations');
1✔
1115
                                    }
1116

1117
                                    foreach ($extlst->children(Namespaces::DATA_VALIDATIONS1)->dataValidations->dataValidation as $item) {
6✔
1118
                                        $item = self::testSimpleXml($item);
6✔
1119
                                        $node = self::testSimpleXml($xmlSheet->dataValidations)->addChild('dataValidation');
6✔
1120
                                        foreach ($item->attributes() ?? [] as $attr) {
6✔
1121
                                            $node->addAttribute($attr->getName(), $attr);
6✔
1122
                                        }
1123
                                        $node->addAttribute('sqref', $item->children(Namespaces::DATA_VALIDATIONS2)->sqref);
6✔
1124
                                        if (isset($item->formula1)) {
6✔
1125
                                            $childNode = $node->addChild('formula1');
6✔
1126
                                            if ($childNode !== null) { // null should never happen
6✔
1127
                                                // see https://github.com/phpstan/phpstan/issues/8236
1128
                                                // resolved with Phpstan 2.1.23
1129
                                                $childNode[0] = (string) $item->formula1->children(Namespaces::DATA_VALIDATIONS2)->f;
6✔
1130
                                            }
1131
                                        }
1132
                                    }
1133
                                }
1134
                            }
1135

1136
                            if ($xmlSheet && $xmlSheet->dataValidations && !$this->readDataOnly) {
742✔
1137
                                (new DataValidations($docSheet, $xmlSheet))->load();
24✔
1138
                            }
1139

1140
                            // unparsed sheet AlternateContent
1141
                            if ($xmlSheet && !$this->readDataOnly) {
742✔
1142
                                $mc = $xmlSheet->children(Namespaces::COMPATIBILITY);
729✔
1143
                                if ($mc->AlternateContent) {
729✔
1144
                                    foreach ($mc->AlternateContent as $alternateContent) {
4✔
1145
                                        $alternateContent = self::testSimpleXml($alternateContent);
4✔
1146
                                        /** @var mixed[][][][] $unparsedLoadedData */
1147
                                        $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['AlternateContents'][] = $alternateContent->asXML();
4✔
1148
                                    }
1149
                                }
1150
                            }
1151

1152
                            // Add hyperlinks
1153
                            if (!$this->readDataOnly) {
742✔
1154
                                $hyperlinkReader = new Hyperlinks($docSheet);
739✔
1155
                                // Locate hyperlink relations
1156
                                $relationsFileName = dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels';
739✔
1157
                                if ($zip->locateName($relationsFileName) !== false) {
739✔
1158
                                    $relsWorksheet = $this->loadZip($relationsFileName, Namespaces::RELATIONSHIPS);
609✔
1159
                                    $hyperlinkReader->readHyperlinks($relsWorksheet);
609✔
1160
                                }
1161

1162
                                // Loop through hyperlinks
1163
                                if ($xmlSheetNS && $xmlSheetNS->children($mainNS)->hyperlinks) {
739✔
1164
                                    $hyperlinkReader->setHyperlinks($xmlSheetNS->children($mainNS)->hyperlinks);
20✔
1165
                                }
1166
                            }
1167

1168
                            // Add comments
1169
                            $comments = [];
742✔
1170
                            $vmlComments = [];
742✔
1171
                            if (!$this->readDataOnly) {
742✔
1172
                                // Locate comment relations
1173
                                $commentRelations = dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels';
739✔
1174
                                if ($zip->locateName($commentRelations) !== false) {
739✔
1175
                                    $relsWorksheet = $this->loadZip($commentRelations, Namespaces::RELATIONSHIPS);
609✔
1176
                                    foreach ($relsWorksheet->Relationship as $elex) {
609✔
1177
                                        $ele = self::getAttributes($elex);
429✔
1178
                                        if ($ele['Type'] == Namespaces::COMMENTS) {
429✔
1179
                                            $comments[(string) $ele['Id']] = (string) $ele['Target'];
43✔
1180
                                        }
1181
                                        if ($ele['Type'] == Namespaces::VML) {
429✔
1182
                                            $vmlComments[(string) $ele['Id']] = (string) $ele['Target'];
47✔
1183
                                        }
1184
                                    }
1185
                                }
1186

1187
                                // Loop through comments
1188
                                foreach ($comments as $relName => $relPath) {
739✔
1189
                                    // Load comments file
1190
                                    $relPath = File::realpath(dirname("$dir/$fileWorksheet") . '/' . $relPath);
43✔
1191
                                    // okay to ignore namespace - using xpath
1192
                                    $commentsFile = $this->loadZip($relPath, '');
43✔
1193

1194
                                    // Utility variables
1195
                                    $authors = [];
43✔
1196
                                    $commentsFile->registerXpathNamespace('com', $mainNS);
43✔
1197
                                    $authorPath = self::xpathNoFalse($commentsFile, 'com:authors/com:author');
43✔
1198
                                    foreach ($authorPath as $author) {
43✔
1199
                                        /** @var SimpleXMLElement $author */
1200
                                        $authors[] = (string) $author;
43✔
1201
                                    }
1202

1203
                                    // Loop through contents
1204
                                    $contentPath = self::xpathNoFalse($commentsFile, 'com:commentList/com:comment');
43✔
1205
                                    foreach ($contentPath as $comment) {
43✔
1206
                                        /** @var SimpleXMLElement $comment */
1207
                                        $commentx = $comment->attributes();
43✔
1208
                                        /** @var array{ref: scalar, authorId?: scalar}  $commentx */
1209
                                        $commentModel = $docSheet->getComment((string) $commentx['ref']);
43✔
1210
                                        if (isset($commentx['authorId'])) {
43✔
1211
                                            $commentModel->setAuthor($authors[(int) $commentx['authorId']]);
43✔
1212
                                        }
1213
                                        /** @var SimpleXMLElement */
1214
                                        $temp = $comment->children($mainNS);
43✔
1215
                                        $commentModel->setText($this->parseRichText($temp->text));
43✔
1216
                                    }
1217
                                }
1218

1219
                                // later we will remove from it real vmlComments
1220
                                $unparsedVmlDrawings = $vmlComments;
739✔
1221
                                $vmlDrawingContents = [];
739✔
1222

1223
                                // Loop through VML comments
1224
                                foreach ($vmlComments as $relName => $relPath) {
739✔
1225
                                    // Load VML comments file
1226
                                    $relPath = File::realpath(dirname("$dir/$fileWorksheet") . '/' . $relPath);
47✔
1227

1228
                                    try {
1229
                                        // no namespace okay - processed with Xpath
1230
                                        $vmlCommentsFile = $this->loadZip($relPath, '', true);
47✔
1231
                                        $vmlCommentsFile->registerXPathNamespace('v', Namespaces::URN_VML);
47✔
UNCOV
1232
                                    } catch (Throwable) {
×
1233
                                        //Ignore unparsable vmlDrawings. Later they will be moved from $unparsedVmlDrawings to $unparsedLoadedData
UNCOV
1234
                                        continue;
×
1235
                                    }
1236

1237
                                    // Locate VML drawings image relations
1238
                                    $drowingImages = [];
47✔
1239
                                    $VMLDrawingsRelations = dirname($relPath) . '/_rels/' . basename($relPath) . '.rels';
47✔
1240
                                    $vmlDrawingContents[$relName] = $this->getSecurityScannerOrThrow()->scan($this->getFromZipArchive($zip, $relPath));
47✔
1241
                                    if ($zip->locateName($VMLDrawingsRelations) !== false) {
47✔
1242
                                        $relsVMLDrawing = $this->loadZip($VMLDrawingsRelations, Namespaces::RELATIONSHIPS);
19✔
1243
                                        foreach ($relsVMLDrawing->Relationship as $elex) {
19✔
1244
                                            $ele = self::getAttributes($elex);
9✔
1245
                                            if ($ele['Type'] == Namespaces::IMAGE) {
9✔
1246
                                                $drowingImages[(string) $ele['Id']] = (string) $ele['Target'];
9✔
1247
                                            }
1248
                                        }
1249
                                    }
1250

1251
                                    $shapes = self::xpathNoFalse($vmlCommentsFile, '//v:shape');
47✔
1252
                                    foreach ($shapes as $shape) {
47✔
1253
                                        /** @var SimpleXMLElement $shape */
1254
                                        $vmlNamespaces = $shape->getNamespaces();
46✔
1255
                                        $shape->registerXPathNamespace('v', $vmlNamespaces['v'] ?? Namespaces::URN_VML);
46✔
1256
                                        $shape->registerXPathNamespace('x', $vmlNamespaces['x'] ?? Namespaces::URN_EXCEL);
46✔
1257
                                        $shape->registerXPathNamespace('o', $vmlNamespaces['o'] ?? Namespaces::URN_MSOFFICE);
46✔
1258

1259
                                        if (isset($shape['style'])) {
46✔
1260
                                            $style = (string) $shape['style'];
46✔
1261
                                            $fillColor = strtoupper(substr((string) $shape['fillcolor'], 1));
46✔
1262
                                            $column = null;
46✔
1263
                                            $row = null;
46✔
1264
                                            $textHAlign = null;
46✔
1265
                                            $fillImageRelId = null;
46✔
1266
                                            $fillImageTitle = '';
46✔
1267

1268
                                            $clientData = $shape->xpath('.//x:ClientData');
46✔
1269
                                            $textboxDirection = '';
46✔
1270
                                            $textboxPath = $shape->xpath('.//v:textbox');
46✔
1271
                                            $textbox = (string) ($textboxPath[0]['style'] ?? '');
46✔
1272
                                            if (Preg::isMatch('/rtl/i', $textbox)) {
46✔
1273
                                                $textboxDirection = Comment::TEXTBOX_DIRECTION_RTL;
1✔
1274
                                            } elseif (Preg::isMatch('/ltr/i', $textbox)) {
45✔
1275
                                                $textboxDirection = Comment::TEXTBOX_DIRECTION_LTR;
1✔
1276
                                            }
1277
                                            if (is_array($clientData) && !empty($clientData)) {
46✔
1278
                                                /** @var SimpleXMLElement */
1279
                                                $clientData = $clientData[0];
44✔
1280

1281
                                                if (isset($clientData['ObjectType']) && (string) $clientData['ObjectType'] == 'Note') {
44✔
1282
                                                    $clientData->registerXPathNamespace('x', $vmlNamespaces['x'] ?? Namespaces::URN_EXCEL);
42✔
1283
                                                    $temp = $clientData->xpath('.//x:Row');
42✔
1284
                                                    if (is_array($temp)) {
42✔
1285
                                                        $row = $temp[0];
42✔
1286
                                                    }
1287

1288
                                                    $temp = $clientData->xpath('.//x:Column');
42✔
1289
                                                    if (is_array($temp)) {
42✔
1290
                                                        $column = $temp[0];
42✔
1291
                                                    }
1292
                                                    $temp = $clientData->xpath('.//x:TextHAlign');
42✔
1293
                                                    if (!empty($temp)) {
42✔
1294
                                                        $textHAlign = strtolower($temp[0]);
12✔
1295
                                                    }
1296
                                                }
1297
                                            }
1298
                                            $rowx = (string) $row;
46✔
1299
                                            $colx = (string) $column;
46✔
1300
                                            if (is_numeric($rowx) && is_numeric($colx) && $textHAlign !== null) {
46✔
1301
                                                $docSheet->getComment([1 + (int) $colx, 1 + (int) $rowx], false)->setAlignment((string) $textHAlign);
12✔
1302
                                            }
1303
                                            if (is_numeric($rowx) && is_numeric($colx) && $textboxDirection !== '') {
46✔
1304
                                                $docSheet->getComment([1 + (int) $colx, 1 + (int) $rowx], false)->setTextboxDirection($textboxDirection);
2✔
1305
                                            }
1306

1307
                                            $fillImageRelNode = $shape->xpath('.//v:fill/@o:relid');
46✔
1308
                                            if (is_array($fillImageRelNode) && !empty($fillImageRelNode)) {
46✔
1309
                                                /** @var SimpleXMLElement */
1310
                                                $fillImageRelNode = $fillImageRelNode[0];
5✔
1311

1312
                                                if (isset($fillImageRelNode['relid'])) {
5✔
1313
                                                    $fillImageRelId = (string) $fillImageRelNode['relid'];
5✔
1314
                                                }
1315
                                            }
1316

1317
                                            $fillImageTitleNode = $shape->xpath('.//v:fill/@o:title');
46✔
1318
                                            if (is_array($fillImageTitleNode) && !empty($fillImageTitleNode)) {
46✔
1319
                                                /** @var SimpleXMLElement */
1320
                                                $fillImageTitleNode = $fillImageTitleNode[0];
3✔
1321

1322
                                                if (isset($fillImageTitleNode['title'])) {
3✔
1323
                                                    $fillImageTitle = (string) $fillImageTitleNode['title'];
3✔
1324
                                                }
1325
                                            }
1326

1327
                                            if (($column !== null) && ($row !== null)) {
46✔
1328
                                                // Set comment properties
1329
                                                $comment = $docSheet->getComment([(int) $column + 1, (int) $row + 1]);
42✔
1330
                                                $comment->getFillColor()->setRGB($fillColor);
42✔
1331
                                                if (isset($fillImageRelId, $drowingImages[$fillImageRelId])) {
42✔
1332
                                                    $objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing();
5✔
1333
                                                    $objDrawing->setName($fillImageTitle);
5✔
1334
                                                    $imagePath = str_replace(['../', '/xl/'], 'xl/', $drowingImages[$fillImageRelId]);
5✔
1335
                                                    $objDrawing->setPath(
5✔
1336
                                                        'zip://' . File::realpath($filename) . '#' . $imagePath,
5✔
1337
                                                        true,
5✔
1338
                                                        $zip
5✔
1339
                                                    );
5✔
1340
                                                    $comment->setBackgroundImage($objDrawing);
5✔
1341
                                                }
1342

1343
                                                // Parse style
1344
                                                $styleArray = explode(';', str_replace(' ', '', $style));
42✔
1345
                                                foreach ($styleArray as $stylePair) {
42✔
1346
                                                    $stylePair = explode(':', $stylePair);
42✔
1347

1348
                                                    if ($stylePair[0] == 'margin-left') {
42✔
1349
                                                        $comment->setMarginLeft($stylePair[1]);
38✔
1350
                                                    }
1351
                                                    if ($stylePair[0] == 'margin-top') {
42✔
1352
                                                        $comment->setMarginTop($stylePair[1]);
38✔
1353
                                                    }
1354
                                                    if ($stylePair[0] == 'width') {
42✔
1355
                                                        $comment->setWidth($stylePair[1]);
38✔
1356
                                                    }
1357
                                                    if ($stylePair[0] == 'height') {
42✔
1358
                                                        $comment->setHeight($stylePair[1]);
38✔
1359
                                                    }
1360
                                                    if ($stylePair[0] == 'visibility') {
42✔
1361
                                                        $comment->setVisible($stylePair[1] == 'visible');
35✔
1362
                                                    }
1363
                                                }
1364

1365
                                                unset($unparsedVmlDrawings[$relName]);
42✔
1366
                                            }
1367
                                        }
1368
                                    }
1369
                                }
1370

1371
                                // unparsed vmlDrawing
1372
                                if ($unparsedVmlDrawings) {
739✔
1373
                                    foreach ($unparsedVmlDrawings as $rId => $relPath) {
7✔
1374
                                        /** @var mixed[][][] $unparsedLoadedData */
1375
                                        $rId = substr($rId, 3); // rIdXXX
7✔
1376
                                        /** @var mixed[][] */
1377
                                        $unparsedVmlDrawing = &$unparsedLoadedData['sheets'][$docSheet->getCodeName()]['vmlDrawings'];
7✔
1378
                                        $unparsedVmlDrawing[$rId] = [];
7✔
1379
                                        $unparsedVmlDrawing[$rId]['filePath'] = self::dirAdd("$dir/$fileWorksheet", $relPath);
7✔
1380
                                        $unparsedVmlDrawing[$rId]['relFilePath'] = $relPath;
7✔
1381
                                        $unparsedVmlDrawing[$rId]['content'] = $this->getSecurityScannerOrThrow()->scan($this->getFromZipArchive($zip, $unparsedVmlDrawing[$rId]['filePath']));
7✔
1382
                                        unset($unparsedVmlDrawing);
7✔
1383
                                    }
1384
                                }
1385

1386
                                // Header/footer images
1387
                                if ($xmlSheetNS && $xmlSheetNS->legacyDrawingHF) {
739✔
1388
                                    $vmlHfRid = '';
3✔
1389
                                    $vmlHfRidAttr = $xmlSheetNS->legacyDrawingHF->attributes(Namespaces::SCHEMA_OFFICE_DOCUMENT);
3✔
1390
                                    if ($vmlHfRidAttr !== null && isset($vmlHfRidAttr['id'])) {
3✔
1391
                                        $vmlHfRid = (string) $vmlHfRidAttr['id'][0];
3✔
1392
                                    }
1393
                                    if ($zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels') !== false) {
3✔
1394
                                        $relsWorksheet = $this->loadZipNoNamespace(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels', Namespaces::RELATIONSHIPS);
3✔
1395
                                        $vmlRelationship = '';
3✔
1396

1397
                                        foreach ($relsWorksheet->Relationship as $ele) {
3✔
1398
                                            if ((string) $ele['Type'] == Namespaces::VML && (string) $ele['Id'] === $vmlHfRid) {
3✔
1399
                                                $vmlRelationship = self::dirAdd("$dir/$fileWorksheet", $ele['Target']);
3✔
1400

1401
                                                break;
3✔
1402
                                            }
1403
                                        }
1404

1405
                                        if ($vmlRelationship != '') {
3✔
1406
                                            // Fetch linked images
1407
                                            $relsVML = $this->loadZipNoNamespace(dirname($vmlRelationship) . '/_rels/' . basename($vmlRelationship) . '.rels', Namespaces::RELATIONSHIPS);
3✔
1408
                                            $drawings = [];
3✔
1409
                                            if (isset($relsVML->Relationship)) {
3✔
1410
                                                foreach ($relsVML->Relationship as $ele) {
3✔
1411
                                                    if ($ele['Type'] == Namespaces::IMAGE) {
3✔
1412
                                                        $drawings[(string) $ele['Id']] = self::dirAdd($vmlRelationship, $ele['Target']);
3✔
1413
                                                    }
1414
                                                }
1415
                                            }
1416
                                            // Fetch VML document
1417
                                            $vmlDrawing = $this->loadZipNoNamespace($vmlRelationship, '');
3✔
1418
                                            $vmlDrawing->registerXPathNamespace('v', Namespaces::URN_VML);
3✔
1419

1420
                                            $hfImages = [];
3✔
1421

1422
                                            $shapes = self::xpathNoFalse($vmlDrawing, '//v:shape');
3✔
1423
                                            foreach ($shapes as $idx => $shape) {
3✔
1424
                                                /** @var SimpleXMLElement $shape */
1425
                                                $shape->registerXPathNamespace('v', Namespaces::URN_VML);
3✔
1426
                                                $imageData = $shape->xpath('//v:imagedata');
3✔
1427

1428
                                                if (empty($imageData)) {
3✔
UNCOV
1429
                                                    continue;
×
1430
                                                }
1431

1432
                                                $imageData = $imageData[$idx];
3✔
1433

1434
                                                $imageData = self::getAttributes($imageData, Namespaces::URN_MSOFFICE);
3✔
1435
                                                /** @var array{width: int, height: int, margin-left?: int, margin-top: int} */
1436
                                                $style = self::toCSSArray((string) $shape['style']);
3✔
1437

1438
                                                if (array_key_exists((string) $imageData['relid'], $drawings)) {
3✔
1439
                                                    $shapeId = (string) $shape['id'];
3✔
1440
                                                    $hfImages[$shapeId] = new HeaderFooterDrawing();
3✔
1441
                                                    if (isset($imageData['title'])) {
3✔
1442
                                                        $hfImages[$shapeId]->setName((string) $imageData['title']);
3✔
1443
                                                    }
1444

1445
                                                    $hfImages[$shapeId]->setPath('zip://' . File::realpath($filename) . '#' . $drawings[(string) $imageData['relid']], false, $zip);
3✔
1446
                                                    $hfImages[$shapeId]->setResizeProportional(false);
3✔
1447
                                                    $hfImages[$shapeId]->setWidth($style['width']);
3✔
1448
                                                    $hfImages[$shapeId]->setHeight($style['height']);
3✔
1449
                                                    if (isset($style['margin-left'])) {
3✔
1450
                                                        $hfImages[$shapeId]->setOffsetX($style['margin-left']);
3✔
1451
                                                    }
1452
                                                    $hfImages[$shapeId]->setOffsetY($style['margin-top']);
3✔
1453
                                                    $hfImages[$shapeId]->setResizeProportional(true);
3✔
1454
                                                }
1455
                                            }
1456

1457
                                            $docSheet->getHeaderFooter()->setImages($hfImages);
3✔
1458
                                        }
1459
                                    }
1460
                                }
1461
                            }
1462

1463
                            // TODO: Autoshapes from twoCellAnchors!
1464
                            $drawingFilename = dirname("$dir/$fileWorksheet")
742✔
1465
                                . '/_rels/'
742✔
1466
                                . basename($fileWorksheet)
742✔
1467
                                . '.rels';
742✔
1468
                            if (str_starts_with($drawingFilename, 'xl//xl/')) {
742✔
UNCOV
1469
                                $drawingFilename = substr($drawingFilename, 4);
×
1470
                            }
1471
                            if (str_starts_with($drawingFilename, '/xl//xl/')) {
742✔
UNCOV
1472
                                $drawingFilename = substr($drawingFilename, 5);
×
1473
                            }
1474
                            if ($zip->locateName($drawingFilename) !== false) {
742✔
1475
                                $relsWorksheet = $this->loadZip($drawingFilename, Namespaces::RELATIONSHIPS);
612✔
1476
                                $drawings = [];
612✔
1477
                                foreach ($relsWorksheet->Relationship as $elex) {
612✔
1478
                                    $ele = self::getAttributes($elex);
431✔
1479
                                    if ((string) $ele['Type'] === "$xmlNamespaceBase/drawing") {
431✔
1480
                                        $eleTarget = (string) $ele['Target'];
154✔
1481
                                        if (str_starts_with($eleTarget, '/xl/')) {
154✔
1482
                                            $drawings[(string) $ele['Id']] = substr($eleTarget, 1);
4✔
1483
                                        } else {
1484
                                            $drawings[(string) $ele['Id']] = self::dirAdd("$dir/$fileWorksheet", $ele['Target']);
151✔
1485
                                        }
1486
                                    }
1487
                                }
1488

1489
                                if ($xmlSheetNS->drawing && !$this->readDataOnly) {
612✔
1490
                                    $unparsedDrawings = [];
153✔
1491
                                    $fileDrawing = null;
153✔
1492
                                    foreach ($xmlSheetNS->drawing as $drawing) {
153✔
1493
                                        $drawingRelId = self::getArrayItemString(self::getAttributes($drawing, $xmlNamespaceBase), 'id');
153✔
1494
                                        $fileDrawing = $drawings[$drawingRelId];
153✔
1495
                                        $drawingFilename = dirname($fileDrawing) . '/_rels/' . basename($fileDrawing) . '.rels';
153✔
1496
                                        $relsDrawing = $this->loadZip($drawingFilename, Namespaces::RELATIONSHIPS);
153✔
1497

1498
                                        $images = [];
153✔
1499
                                        $hyperlinks = [];
153✔
1500
                                        if ($relsDrawing && $relsDrawing->Relationship) {
153✔
1501
                                            foreach ($relsDrawing->Relationship as $elex) {
130✔
1502
                                                $ele = self::getAttributes($elex);
130✔
1503
                                                $eleType = (string) $ele['Type'];
130✔
1504
                                                if ($eleType === Namespaces::HYPERLINK) {
130✔
1505
                                                    $hyperlinks[(string) $ele['Id']] = (string) $ele['Target'];
5✔
1506
                                                }
1507
                                                if ($eleType === "$xmlNamespaceBase/image") {
130✔
1508
                                                    $eleTarget = (string) $ele['Target'];
79✔
1509
                                                    if (str_starts_with($eleTarget, '/xl/')) {
79✔
1510
                                                        $eleTarget = substr($eleTarget, 1);
1✔
1511
                                                        $images[(string) $ele['Id']] = $eleTarget;
1✔
1512
                                                    } else {
1513
                                                        $images[(string) $ele['Id']] = self::dirAdd($fileDrawing, $eleTarget);
78✔
1514
                                                    }
1515
                                                } elseif ($eleType === "$xmlNamespaceBase/chart") {
77✔
1516
                                                    if ($this->includeCharts) {
71✔
1517
                                                        $eleTarget = (string) $ele['Target'];
70✔
1518
                                                        if (str_starts_with($eleTarget, '/xl/')) {
70✔
1519
                                                            $index = substr($eleTarget, 1);
3✔
1520
                                                        } else {
1521
                                                            $index = self::dirAdd($fileDrawing, $eleTarget);
68✔
1522
                                                        }
1523
                                                        $charts[$index] = [
70✔
1524
                                                            'id' => (string) $ele['Id'],
70✔
1525
                                                            'sheet' => $docSheet->getTitle(),
70✔
1526
                                                        ];
70✔
1527
                                                    }
1528
                                                }
1529
                                            }
1530
                                        }
1531

1532
                                        $xmlDrawing = $this->loadZipNoNamespace($fileDrawing, '');
153✔
1533
                                        $xmlDrawingChildren = $xmlDrawing->children(Namespaces::SPREADSHEET_DRAWING);
153✔
1534

1535
                                        // Store drawing XML for pass-through if enabled
1536
                                        if ($this->enableDrawingPassThrough) {
153✔
1537
                                            $unparsedDrawings[$drawingRelId] = $xmlDrawing->asXML();
11✔
1538
                                            // Mark that pass-through is enabled for this sheet
1539
                                            $sheetCodeName = $docSheet->getCodeName();
11✔
1540
                                            if (!isset($unparsedLoadedData['sheets']) || !is_array($unparsedLoadedData['sheets'])) {
11✔
1541
                                                $unparsedLoadedData['sheets'] = [];
11✔
1542
                                            }
1543
                                            if (!isset($unparsedLoadedData['sheets'][$sheetCodeName]) || !is_array($unparsedLoadedData['sheets'][$sheetCodeName])) {
11✔
1544
                                                $unparsedLoadedData['sheets'][$sheetCodeName] = [];
11✔
1545
                                            }
1546
                                            /** @var array<string, mixed> $sheetUnparsedData */
1547
                                            $sheetUnparsedData = &$unparsedLoadedData['sheets'][$sheetCodeName];
11✔
1548
                                            $sheetUnparsedData['drawingPassThroughEnabled'] = true;
11✔
1549
                                            // Store original drawing relationships for pass-through
1550
                                            if ($relsDrawing) {
11✔
1551
                                                $sheetUnparsedData['drawingRelationships'] = $relsDrawing->asXML();
11✔
1552
                                            }
1553
                                            // Store original media files paths and source file for pass-through
1554
                                            $sheetUnparsedData['drawingMediaFiles'] = $images;
11✔
1555
                                            $sheetUnparsedData['drawingSourceFile'] = File::realpath($filename);
11✔
1556
                                        }
1557

1558
                                        if ($xmlDrawingChildren->oneCellAnchor) {
153✔
1559
                                            foreach ($xmlDrawingChildren->oneCellAnchor as $oneCellAnchor) {
25✔
1560
                                                $oneCellAnchor = self::testSimpleXml($oneCellAnchor);
25✔
1561
                                                if ($oneCellAnchor->pic->blipFill) {
25✔
1562
                                                    $objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing();
17✔
1563
                                                    $blip = $oneCellAnchor->pic->blipFill->children(Namespaces::DRAWINGML)->blip;
17✔
1564
                                                    if (isset($blip, $blip->alphaModFix)) {
17✔
1565
                                                        $temp = (string) $blip->alphaModFix->attributes()->amt;
1✔
1566
                                                        if (is_numeric($temp)) {
1✔
1567
                                                            $objDrawing->setOpacity((int) $temp);
1✔
1568
                                                        }
1569
                                                    }
1570
                                                    $xfrm = $oneCellAnchor->pic->spPr->children(Namespaces::DRAWINGML)->xfrm;
17✔
1571
                                                    $outerShdw = $oneCellAnchor->pic->spPr->children(Namespaces::DRAWINGML)->effectLst->outerShdw;
17✔
1572

1573
                                                    $objDrawing->setName(self::getArrayItemString(self::getAttributes($oneCellAnchor->pic->nvPicPr->cNvPr), 'name'));
17✔
1574
                                                    $objDrawing->setDescription(self::getArrayItemString(self::getAttributes($oneCellAnchor->pic->nvPicPr->cNvPr), 'descr'));
17✔
1575
                                                    $embedImageKey = self::getArrayItemString(
17✔
1576
                                                        self::getAttributes($blip, $xmlNamespaceBase),
17✔
1577
                                                        'embed'
17✔
1578
                                                    );
17✔
1579
                                                    if (isset($images[$embedImageKey])) {
17✔
1580
                                                        $objDrawing->setPath(
17✔
1581
                                                            'zip://' . File::realpath($filename) . '#'
17✔
1582
                                                            . $images[$embedImageKey],
17✔
1583
                                                            false,
17✔
1584
                                                            $zip
17✔
1585
                                                        );
17✔
1586
                                                    } else {
UNCOV
1587
                                                        $linkImageKey = self::getArrayItemString(
×
UNCOV
1588
                                                            $blip->attributes('http://schemas.openxmlformats.org/officeDocument/2006/relationships'),
×
UNCOV
1589
                                                            'link'
×
UNCOV
1590
                                                        );
×
UNCOV
1591
                                                        if (isset($images[$linkImageKey])) {
×
UNCOV
1592
                                                            $url = str_replace('xl/drawings/', '', $images[$linkImageKey]);
×
UNCOV
1593
                                                            $objDrawing->setPath($url, false, allowExternal: $this->allowExternalImages);
×
1594
                                                        }
UNCOV
1595
                                                        if ($objDrawing->getPath() === '') {
×
UNCOV
1596
                                                            continue;
×
1597
                                                        }
1598
                                                    }
1599
                                                    $objDrawing->setCoordinates(Coordinate::stringFromColumnIndex(((int) $oneCellAnchor->from->col) + 1) . ($oneCellAnchor->from->row + 1));
17✔
1600

1601
                                                    $objDrawing->setOffsetX((int) Drawing::EMUToPixels($oneCellAnchor->from->colOff));
17✔
1602
                                                    $objDrawing->setOffsetY(Drawing::EMUToPixels($oneCellAnchor->from->rowOff));
17✔
1603
                                                    $objDrawing->setResizeProportional(false);
17✔
1604
                                                    $objDrawing->setWidth(Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($oneCellAnchor->ext), 'cx')));
17✔
1605
                                                    $objDrawing->setHeight(Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($oneCellAnchor->ext), 'cy')));
17✔
1606
                                                    if ($xfrm) {
17✔
1607
                                                        $objDrawing->setRotation((int) Drawing::angleToDegrees(self::getArrayItemIntOrSxml(self::getAttributes($xfrm), 'rot')));
17✔
1608
                                                        $objDrawing->setFlipVertical((bool) self::getArrayItem(self::getAttributes($xfrm), 'flipV'));
17✔
1609
                                                        $objDrawing->setFlipHorizontal((bool) self::getArrayItem(self::getAttributes($xfrm), 'flipH'));
17✔
1610
                                                    }
1611
                                                    if ($outerShdw) {
17✔
1612
                                                        $shadow = $objDrawing->getShadow();
3✔
1613
                                                        $shadow->setVisible(true);
3✔
1614
                                                        $shadow->setBlurRadius(Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($outerShdw), 'blurRad')));
3✔
1615
                                                        $shadow->setDistance(Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($outerShdw), 'dist')));
3✔
1616
                                                        $shadow->setDirection(Drawing::angleToDegrees(self::getArrayItemIntOrSxml(self::getAttributes($outerShdw), 'dir')));
3✔
1617
                                                        $shadow->setAlignment(self::getArrayItemString(self::getAttributes($outerShdw), 'algn'));
3✔
1618
                                                        $clr = $outerShdw->srgbClr ?? $outerShdw->prstClr;
3✔
1619
                                                        $shadow->getColor()->setRGB(self::getArrayItemString(self::getAttributes($clr), 'val'));
3✔
1620
                                                        if ($clr->alpha) {
3✔
1621
                                                            $alpha = StringHelper::convertToString(self::getArrayItem(self::getAttributes($clr->alpha), 'val'));
3✔
1622
                                                            if (is_numeric($alpha)) {
3✔
1623
                                                                $alpha = (int) ($alpha / 1000);
3✔
1624
                                                                $shadow->setAlpha($alpha);
3✔
1625
                                                            }
1626
                                                        }
1627
                                                    }
1628

1629
                                                    $this->readHyperLinkDrawing($objDrawing, $oneCellAnchor, $hyperlinks);
17✔
1630

1631
                                                    $objDrawing->setWorksheet($docSheet);
17✔
1632
                                                } elseif ($this->includeCharts && $oneCellAnchor->graphicFrame) {
8✔
1633
                                                    // Exported XLSX from Google Sheets positions charts with a oneCellAnchor
1634
                                                    $coordinates = Coordinate::stringFromColumnIndex(((int) $oneCellAnchor->from->col) + 1) . ($oneCellAnchor->from->row + 1);
4✔
1635
                                                    $offsetX = Drawing::EMUToPixels($oneCellAnchor->from->colOff);
4✔
1636
                                                    $offsetY = Drawing::EMUToPixels($oneCellAnchor->from->rowOff);
4✔
1637
                                                    $width = Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($oneCellAnchor->ext), 'cx'));
4✔
1638
                                                    $height = Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($oneCellAnchor->ext), 'cy'));
4✔
1639

1640
                                                    $graphic = $oneCellAnchor->graphicFrame->children(Namespaces::DRAWINGML)->graphic;
4✔
1641
                                                    $chartRef = $graphic->graphicData->children(Namespaces::CHART)->chart;
4✔
1642
                                                    $thisChart = (string) self::getAttributes($chartRef, $xmlNamespaceBase);
4✔
1643

1644
                                                    $chartDetails[$docSheet->getTitle() . '!' . $thisChart] = [
4✔
1645
                                                        'fromCoordinate' => $coordinates,
4✔
1646
                                                        'fromOffsetX' => $offsetX,
4✔
1647
                                                        'fromOffsetY' => $offsetY,
4✔
1648
                                                        'width' => $width,
4✔
1649
                                                        'height' => $height,
4✔
1650
                                                        'worksheetTitle' => $docSheet->getTitle(),
4✔
1651
                                                        'oneCellAnchor' => true,
4✔
1652
                                                    ];
4✔
1653
                                                }
1654
                                            }
1655
                                        }
1656
                                        if ($xmlDrawingChildren->twoCellAnchor) {
153✔
1657
                                            foreach ($xmlDrawingChildren->twoCellAnchor as $twoCellAnchor) {
112✔
1658
                                                $twoCellAnchor = self::testSimpleXml($twoCellAnchor);
112✔
1659
                                                if ($twoCellAnchor->pic->blipFill) {
112✔
1660
                                                    $objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing();
62✔
1661
                                                    $blip = $twoCellAnchor->pic->blipFill->children(Namespaces::DRAWINGML)->blip;
62✔
1662
                                                    if (isset($blip, $blip->alphaModFix)) {
62✔
1663
                                                        $temp = (string) $blip->alphaModFix->attributes()->amt;
3✔
1664
                                                        if (is_numeric($temp)) {
3✔
1665
                                                            $objDrawing->setOpacity((int) $temp);
3✔
1666
                                                        }
1667
                                                    }
1668
                                                    if (isset($twoCellAnchor->pic->blipFill->children(Namespaces::DRAWINGML)->srcRect)) {
62✔
1669
                                                        $objDrawing->setSrcRect($twoCellAnchor->pic->blipFill->children(Namespaces::DRAWINGML)->srcRect->attributes());
11✔
1670
                                                    }
1671
                                                    $xfrm = $twoCellAnchor->pic->spPr->children(Namespaces::DRAWINGML)->xfrm;
62✔
1672
                                                    $outerShdw = $twoCellAnchor->pic->spPr->children(Namespaces::DRAWINGML)->effectLst->outerShdw;
62✔
1673
                                                    $editAs = $twoCellAnchor->attributes();
62✔
1674
                                                    if (isset($editAs, $editAs['editAs'])) {
62✔
1675
                                                        $objDrawing->setEditAs($editAs['editAs']);
55✔
1676
                                                    }
1677
                                                    $objDrawing->setName((string) self::getArrayItemString(self::getAttributes($twoCellAnchor->pic->nvPicPr->cNvPr), 'name'));
62✔
1678
                                                    $objDrawing->setDescription(self::getArrayItemString(self::getAttributes($twoCellAnchor->pic->nvPicPr->cNvPr), 'descr'));
62✔
1679
                                                    $embedImageKey = self::getArrayItemString(
62✔
1680
                                                        self::getAttributes($blip, $xmlNamespaceBase),
62✔
1681
                                                        'embed'
62✔
1682
                                                    );
62✔
1683
                                                    if (isset($images[$embedImageKey])) {
62✔
1684
                                                        $objDrawing->setPath(
55✔
1685
                                                            'zip://' . File::realpath($filename) . '#'
55✔
1686
                                                            . $images[$embedImageKey],
55✔
1687
                                                            false,
55✔
1688
                                                            $zip
55✔
1689
                                                        );
55✔
1690
                                                    } else {
1691
                                                        $linkImageKey = self::getArrayItemString(
7✔
1692
                                                            $blip->attributes('http://schemas.openxmlformats.org/officeDocument/2006/relationships'),
7✔
1693
                                                            'link'
7✔
1694
                                                        );
7✔
1695
                                                        if (isset($images[$linkImageKey])) {
7✔
1696
                                                            $url = str_replace('xl/drawings/', '', $images[$linkImageKey]);
7✔
1697
                                                            $objDrawing->setPath($url, false, allowExternal: $this->allowExternalImages);
7✔
1698
                                                        }
1699
                                                        if ($objDrawing->getPath() === '') {
6✔
1700
                                                            continue;
4✔
1701
                                                        }
1702
                                                    }
1703
                                                    $objDrawing->setCoordinates(Coordinate::stringFromColumnIndex(((int) $twoCellAnchor->from->col) + 1) . ($twoCellAnchor->from->row + 1));
57✔
1704

1705
                                                    $objDrawing->setOffsetX(Drawing::EMUToPixels($twoCellAnchor->from->colOff));
57✔
1706
                                                    $objDrawing->setOffsetY(Drawing::EMUToPixels($twoCellAnchor->from->rowOff));
57✔
1707

1708
                                                    $objDrawing->setCoordinates2(Coordinate::stringFromColumnIndex(((int) $twoCellAnchor->to->col) + 1) . ($twoCellAnchor->to->row + 1));
57✔
1709

1710
                                                    $objDrawing->setOffsetX2(Drawing::EMUToPixels($twoCellAnchor->to->colOff));
57✔
1711
                                                    $objDrawing->setOffsetY2(Drawing::EMUToPixels($twoCellAnchor->to->rowOff));
57✔
1712

1713
                                                    $objDrawing->setResizeProportional(false);
57✔
1714

1715
                                                    if ($xfrm) {
57✔
1716
                                                        $objDrawing->setWidth(Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($xfrm->ext), 'cx')));
57✔
1717
                                                        $objDrawing->setHeight(Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($xfrm->ext), 'cy')));
57✔
1718
                                                        $objDrawing->setRotation(Drawing::angleToDegrees(self::getArrayItemIntOrSxml(self::getAttributes($xfrm), 'rot')));
57✔
1719
                                                        $objDrawing->setFlipVertical((bool) self::getArrayItem(self::getAttributes($xfrm), 'flipV'));
57✔
1720
                                                        $objDrawing->setFlipHorizontal((bool) self::getArrayItem(self::getAttributes($xfrm), 'flipH'));
57✔
1721
                                                    }
1722
                                                    if ($outerShdw) {
57✔
1723
                                                        $shadow = $objDrawing->getShadow();
1✔
1724
                                                        $shadow->setVisible(true);
1✔
1725
                                                        $shadow->setBlurRadius(Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($outerShdw), 'blurRad')));
1✔
1726
                                                        $shadow->setDistance(Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($outerShdw), 'dist')));
1✔
1727
                                                        $shadow->setDirection(Drawing::angleToDegrees(self::getArrayItemIntOrSxml(self::getAttributes($outerShdw), 'dir')));
1✔
1728
                                                        $shadow->setAlignment(self::getArrayItemString(self::getAttributes($outerShdw), 'algn'));
1✔
1729
                                                        $clr = $outerShdw->srgbClr ?? $outerShdw->prstClr;
1✔
1730
                                                        $shadow->getColor()->setRGB(self::getArrayItemString(self::getAttributes($clr), 'val'));
1✔
1731
                                                        if ($clr->alpha) {
1✔
1732
                                                            $alpha = StringHelper::convertToString(self::getArrayItem(self::getAttributes($clr->alpha), 'val'));
1✔
1733
                                                            if (is_numeric($alpha)) {
1✔
1734
                                                                $alpha = (int) ($alpha / 1000);
1✔
1735
                                                                $shadow->setAlpha($alpha);
1✔
1736
                                                            }
1737
                                                        }
1738
                                                    }
1739

1740
                                                    $this->readHyperLinkDrawing($objDrawing, $twoCellAnchor, $hyperlinks);
57✔
1741

1742
                                                    $objDrawing->setWorksheet($docSheet);
57✔
1743
                                                } elseif (($this->includeCharts) && ($twoCellAnchor->graphicFrame)) {
85✔
1744
                                                    $fromCoordinate = Coordinate::stringFromColumnIndex(((int) $twoCellAnchor->from->col) + 1) . ($twoCellAnchor->from->row + 1);
66✔
1745
                                                    $fromOffsetX = Drawing::EMUToPixels($twoCellAnchor->from->colOff);
66✔
1746
                                                    $fromOffsetY = Drawing::EMUToPixels($twoCellAnchor->from->rowOff);
66✔
1747
                                                    $toCoordinate = Coordinate::stringFromColumnIndex(((int) $twoCellAnchor->to->col) + 1) . ($twoCellAnchor->to->row + 1);
66✔
1748
                                                    $toOffsetX = Drawing::EMUToPixels($twoCellAnchor->to->colOff);
66✔
1749
                                                    $toOffsetY = Drawing::EMUToPixels($twoCellAnchor->to->rowOff);
66✔
1750
                                                    $graphic = $twoCellAnchor->graphicFrame->children(Namespaces::DRAWINGML)->graphic;
66✔
1751
                                                    $chartRef = $graphic->graphicData->children(Namespaces::CHART)->chart;
66✔
1752
                                                    $thisChart = (string) self::getAttributes($chartRef, $xmlNamespaceBase);
66✔
1753

1754
                                                    $chartDetails[$docSheet->getTitle() . '!' . $thisChart] = [
66✔
1755
                                                        'fromCoordinate' => $fromCoordinate,
66✔
1756
                                                        'fromOffsetX' => $fromOffsetX,
66✔
1757
                                                        'fromOffsetY' => $fromOffsetY,
66✔
1758
                                                        'toCoordinate' => $toCoordinate,
66✔
1759
                                                        'toOffsetX' => $toOffsetX,
66✔
1760
                                                        'toOffsetY' => $toOffsetY,
66✔
1761
                                                        'worksheetTitle' => $docSheet->getTitle(),
66✔
1762
                                                    ];
66✔
1763
                                                }
1764
                                            }
1765
                                        }
1766
                                        if ($xmlDrawingChildren->absoluteAnchor) {
152✔
1767
                                            foreach ($xmlDrawingChildren->absoluteAnchor as $absoluteAnchor) {
1✔
1768
                                                if (($this->includeCharts) && ($absoluteAnchor->graphicFrame)) {
1✔
1769
                                                    $graphic = $absoluteAnchor->graphicFrame->children(Namespaces::DRAWINGML)->graphic;
1✔
1770
                                                    $chartRef = $graphic->graphicData->children(Namespaces::CHART)->chart;
1✔
1771
                                                    $thisChart = (string) self::getAttributes($chartRef, $xmlNamespaceBase);
1✔
1772
                                                    $width = Drawing::EMUToPixels((int) self::getArrayItemString(self::getAttributes($absoluteAnchor->ext), 'cx')[0]);
1✔
1773
                                                    $height = Drawing::EMUToPixels((int) self::getArrayItemString(self::getAttributes($absoluteAnchor->ext), 'cy')[0]);
1✔
1774

1775
                                                    $chartDetails[$docSheet->getTitle() . '!' . $thisChart] = [
1✔
1776
                                                        'fromCoordinate' => 'A1',
1✔
1777
                                                        'fromOffsetX' => 0,
1✔
1778
                                                        'fromOffsetY' => 0,
1✔
1779
                                                        'width' => $width,
1✔
1780
                                                        'height' => $height,
1✔
1781
                                                        'worksheetTitle' => $docSheet->getTitle(),
1✔
1782
                                                    ];
1✔
1783
                                                }
1784
                                            }
1785
                                        }
1786
                                        if (empty($relsDrawing) && $xmlDrawing->count() == 0) {
152✔
1787
                                            // Save Drawing without rels and children as unparsed
1788
                                            $unparsedDrawings[$drawingRelId] = $xmlDrawing->asXML();
28✔
1789
                                        }
1790
                                    }
1791

1792
                                    // store original rId of drawing files
1793
                                    /** @var mixed[][][][] $unparsedLoadedData */
1794
                                    $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['drawingOriginalIds'] = [];
152✔
1795
                                    foreach ($relsWorksheet->Relationship as $elex) {
152✔
1796
                                        $ele = self::getAttributes($elex);
152✔
1797
                                        if ((string) $ele['Type'] === "$xmlNamespaceBase/drawing") {
152✔
1798
                                            $drawingRelId = (string) $ele['Id'];
152✔
1799
                                            $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['drawingOriginalIds'][(string) $ele['Target']] = $drawingRelId;
152✔
1800
                                            if (isset($unparsedDrawings[$drawingRelId])) {
152✔
1801
                                                $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['Drawings'][$drawingRelId] = $unparsedDrawings[$drawingRelId];
39✔
1802
                                            }
1803
                                        }
1804
                                    }
1805
                                    if ($xmlSheet->legacyDrawing && !$this->readDataOnly) {
152✔
1806
                                        foreach ($xmlSheet->legacyDrawing as $drawing) {
22✔
1807
                                            $drawingRelId = self::getArrayItemString(self::getAttributes($drawing, $xmlNamespaceBase), 'id');
22✔
1808
                                            if (isset($vmlDrawingContents[$drawingRelId])) {
22✔
1809
                                                if (self::onlyNoteVml($vmlDrawingContents[$drawingRelId]) === false) {
22✔
1810
                                                    $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['legacyDrawing'] = $vmlDrawingContents[$drawingRelId];
5✔
1811
                                                }
1812
                                            }
1813
                                        }
1814
                                    }
1815

1816
                                    // unparsed drawing AlternateContent
1817
                                    $xmlAltDrawing = $this->loadZip((string) $fileDrawing, Namespaces::COMPATIBILITY);
152✔
1818

1819
                                    if ($xmlAltDrawing->AlternateContent) {
152✔
1820
                                        foreach ($xmlAltDrawing->AlternateContent as $alternateContent) {
4✔
1821
                                            $alternateContent = self::testSimpleXml($alternateContent);
4✔
1822
                                            /** @var mixed[][][][][] $unparsedLoadedData */
1823
                                            $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['drawingAlternateContents'][] = $alternateContent->asXML();
4✔
1824
                                        }
1825
                                    }
1826
                                }
1827
                            }
1828

1829
                            /** @var mixed[][][][] $unparsedLoadedData */
1830
                            $this->readFormControlProperties($excel, $dir, $fileWorksheet, $docSheet, $unparsedLoadedData);
741✔
1831
                            $this->readPrinterSettings($excel, $dir, $fileWorksheet, $docSheet, $unparsedLoadedData);
741✔
1832

1833
                            // Loop through definedNames
1834
                            if ($xmlWorkbook->definedNames) {
741✔
1835
                                foreach ($xmlWorkbook->definedNames->definedName as $definedName) {
403✔
1836
                                    // Extract range
1837
                                    $extractedRange = (string) $definedName;
108✔
1838
                                    if (($spos = strpos($extractedRange, '!')) !== false) {
108✔
1839
                                        $extractedRange = substr($extractedRange, 0, $spos) . str_replace('$', '', substr($extractedRange, $spos));
89✔
1840
                                    } else {
1841
                                        $extractedRange = str_replace('$', '', $extractedRange);
34✔
1842
                                    }
1843

1844
                                    // Valid range?
1845
                                    if ($extractedRange == '') {
108✔
UNCOV
1846
                                        continue;
×
1847
                                    }
1848

1849
                                    // Some definedNames are only applicable if we are on the same sheet...
1850
                                    if ((string) $definedName['localSheetId'] != '' && (string) $definedName['localSheetId'] == $oldSheetId) {
108✔
1851
                                        // Switch on type
1852
                                        switch ((string) $definedName['name']) {
50✔
1853
                                            case '_xlnm._FilterDatabase':
50✔
1854
                                                if ((string) $definedName['hidden'] !== '1') {
20✔
UNCOV
1855
                                                    $extractedRange = explode(',', $extractedRange);
×
UNCOV
1856
                                                    foreach ($extractedRange as $range) {
×
UNCOV
1857
                                                        $autoFilterRange = $range;
×
UNCOV
1858
                                                        if (str_contains($autoFilterRange, ':')) {
×
UNCOV
1859
                                                            $docSheet->getAutoFilter()->setRange($autoFilterRange);
×
1860
                                                        }
1861
                                                    }
1862
                                                }
1863

1864
                                                break;
20✔
1865
                                            case '_xlnm.Print_Titles':
30✔
1866
                                                // Split $extractedRange
1867
                                                $extractedRange = explode(',', $extractedRange);
3✔
1868

1869
                                                // Set print titles
1870
                                                foreach ($extractedRange as $range) {
3✔
1871
                                                    $matches = [];
3✔
1872
                                                    $range = str_replace('$', '', $range);
3✔
1873

1874
                                                    // check for repeating columns, e g. 'A:A' or 'A:D'
1875
                                                    if (Preg::isMatch('/!?([A-Z]+)\:([A-Z]+)$/', $range, $matches)) {
3✔
UNCOV
1876
                                                        $docSheet->getPageSetup()->setColumnsToRepeatAtLeft([$matches[1], $matches[2]]);
×
1877
                                                    } elseif (Preg::isMatch('/!?(\d+)\:(\d+)$/', $range, $matches)) {
3✔
1878
                                                        // check for repeating rows, e.g. '1:1' or '1:5'
1879
                                                        $docSheet->getPageSetup()->setRowsToRepeatAtTop([(int) $matches[1], (int) $matches[2]]);
3✔
1880
                                                    }
1881
                                                }
1882

1883
                                                break;
3✔
1884
                                            case '_xlnm.Print_Area':
29✔
1885
                                                $rangeSets = Preg::split("/('?(?:.*?)'?(?:![A-Z0-9]+:[A-Z0-9]+)),?/", $extractedRange, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE) ?: [];
11✔
1886
                                                $newRangeSets = [];
11✔
1887
                                                foreach ($rangeSets as $rangeSet) {
11✔
1888
                                                    [, $rangeSet] = Worksheet::extractSheetTitle($rangeSet, true);
11✔
1889
                                                    if (empty($rangeSet)) {
11✔
UNCOV
1890
                                                        continue;
×
1891
                                                    }
1892
                                                    if (!str_contains($rangeSet, ':')) {
11✔
UNCOV
1893
                                                        $rangeSet = $rangeSet . ':' . $rangeSet;
×
1894
                                                    }
1895
                                                    $newRangeSets[] = str_replace('$', '', $rangeSet);
11✔
1896
                                                }
1897
                                                if (count($newRangeSets) > 0) {
11✔
1898
                                                    $docSheet->getPageSetup()->setPrintArea(implode(',', $newRangeSets));
11✔
1899
                                                }
1900

1901
                                                break;
11✔
1902
                                            default:
1903
                                                break;
19✔
1904
                                        }
1905
                                    }
1906
                                }
1907
                            }
1908

1909
                            // Next sheet id
1910
                            ++$sheetId;
741✔
1911
                        }
1912

1913
                        // Loop through definedNames
1914
                        if ($xmlWorkbook->definedNames) {
745✔
1915
                            foreach ($xmlWorkbook->definedNames->definedName as $definedName) {
403✔
1916
                                // Extract range
1917
                                $extractedRange = (string) $definedName;
108✔
1918

1919
                                // Valid range?
1920
                                if ($extractedRange == '') {
108✔
UNCOV
1921
                                    continue;
×
1922
                                }
1923

1924
                                // Some definedNames are only applicable if we are on the same sheet...
1925
                                if ((string) $definedName['localSheetId'] != '') {
108✔
1926
                                    // Local defined name
1927
                                    // Switch on type
1928
                                    switch ((string) $definedName['name']) {
50✔
1929
                                        case '_xlnm._FilterDatabase':
50✔
1930
                                        case '_xlnm.Print_Titles':
30✔
1931
                                        case '_xlnm.Print_Area':
29✔
1932
                                            break;
33✔
1933
                                        default:
1934
                                            if ($mapSheetId[(int) $definedName['localSheetId']] !== null) {
19✔
1935
                                                $range = Worksheet::extractSheetTitle($extractedRange, true);
19✔
1936
                                                $scope = $excel->getSheet($mapSheetId[(int) $definedName['localSheetId']]);
19✔
1937
                                                if (str_contains((string) $definedName, '!')) {
19✔
1938
                                                    $range[0] = str_replace("''", "'", $range[0]);
19✔
1939
                                                    $range[0] = str_replace("'", '', $range[0]);
19✔
1940
                                                    if ($worksheet = $excel->getSheetByName($range[0])) {
19✔
1941
                                                        $excel->addDefinedName(DefinedName::createInstance((string) $definedName['name'], $worksheet, $extractedRange, true, $scope));
19✔
1942
                                                    } else {
1943
                                                        $excel->addDefinedName(DefinedName::createInstance((string) $definedName['name'], $scope, $extractedRange, true, $scope));
14✔
1944
                                                    }
1945
                                                } else {
UNCOV
1946
                                                    $excel->addDefinedName(DefinedName::createInstance((string) $definedName['name'], $scope, $extractedRange, true));
×
1947
                                                }
1948
                                            }
1949

1950
                                            break;
19✔
1951
                                    }
1952
                                } elseif (!isset($definedName['localSheetId'])) {
78✔
1953
                                    // "Global" definedNames
1954
                                    $locatedSheet = null;
78✔
1955
                                    if (str_contains((string) $definedName, '!')) {
78✔
1956
                                        // Modify range, and extract the first worksheet reference
1957
                                        // Need to split on a comma or a space if not in quotes, and extract the first part.
1958
                                        $definedNameValueParts = Preg::split("/[ ,](?=([^']*'[^']*')*[^']*$)/miuU", $extractedRange);
58✔
1959
                                        // Extract sheet name
1960
                                        [$extractedSheetName] = Worksheet::extractSheetTitle((string) $definedNameValueParts[0], true, true);
58✔
1961

1962
                                        // Locate sheet
1963
                                        $locatedSheet = $excel->getSheetByName("$extractedSheetName");
58✔
1964
                                    }
1965

1966
                                    if ($locatedSheet === null && !DefinedName::testIfFormula($extractedRange)) {
78✔
1967
                                        $extractedRange = '#REF!';
2✔
1968
                                    }
1969
                                    $excel->addDefinedName(DefinedName::createInstance((string) $definedName['name'], $locatedSheet, $extractedRange, false));
78✔
1970
                                }
1971
                            }
1972
                        }
1973
                    }
1974
                    if ($this->createBlankSheetIfNoneRead && !$sheetCreated) {
745✔
1975
                        $excel->createSheet();
2✔
1976
                    }
1977

1978
                    (new WorkbookView($excel))->viewSettings($xmlWorkbook, $mainNS, $mapSheetId, $this->readDataOnly);
745✔
1979

1980
                    break;
743✔
1981
            }
1982
        }
1983

1984
        if (!$this->readDataOnly) {
743✔
1985
            $contentTypes = $this->loadZip('[Content_Types].xml');
740✔
1986

1987
            // Default content types
1988
            foreach ($contentTypes->Default as $contentType) {
740✔
1989
                switch ($contentType['ContentType']) {
738✔
1990
                    case 'application/vnd.openxmlformats-officedocument.spreadsheetml.printerSettings':
738✔
1991
                        $unparsedLoadedData['default_content_types'][(string) $contentType['Extension']] = (string) $contentType['ContentType'];
299✔
1992

1993
                        break;
299✔
1994
                }
1995
            }
1996

1997
            // Override content types
1998
            foreach ($contentTypes->Override as $contentType) {
740✔
1999
                switch ($contentType['ContentType']) {
739✔
2000
                    case 'application/vnd.openxmlformats-officedocument.drawingml.chart+xml':
739✔
2001
                        if ($this->includeCharts) {
72✔
2002
                            $chartEntryRef = ltrim((string) $contentType['PartName'], '/');
70✔
2003
                            $chartElements = $this->loadZip($chartEntryRef);
70✔
2004
                            $chartReader = new Chart($chartNS, $drawingNS);
70✔
2005
                            $objChart = $chartReader->readChart($chartElements, basename($chartEntryRef, '.xml'));
70✔
2006
                            if (isset($charts[$chartEntryRef])) {
70✔
2007
                                $chartPositionRef = $charts[$chartEntryRef]['sheet'] . '!' . $charts[$chartEntryRef]['id'];
70✔
2008
                                if (isset($chartDetails[$chartPositionRef]) && $excel->getSheetByName($charts[$chartEntryRef]['sheet']) !== null) {
70✔
2009
                                    $excel->getSheetByName($charts[$chartEntryRef]['sheet'])->addChart($objChart);
70✔
2010
                                    $objChart->setWorksheet($excel->getSheetByName($charts[$chartEntryRef]['sheet']));
70✔
2011
                                    // For oneCellAnchor or absoluteAnchor positioned charts,
2012
                                    //     toCoordinate is not in the data. Does it need to be calculated?
2013
                                    if (array_key_exists('toCoordinate', $chartDetails[$chartPositionRef])) {
70✔
2014
                                        // twoCellAnchor
2015
                                        $objChart->setTopLeftPosition($chartDetails[$chartPositionRef]['fromCoordinate'], $chartDetails[$chartPositionRef]['fromOffsetX'], $chartDetails[$chartPositionRef]['fromOffsetY']);
66✔
2016
                                        $objChart->setBottomRightPosition($chartDetails[$chartPositionRef]['toCoordinate'], $chartDetails[$chartPositionRef]['toOffsetX'], $chartDetails[$chartPositionRef]['toOffsetY']);
66✔
2017
                                    } else {
2018
                                        // oneCellAnchor or absoluteAnchor (e.g. Chart sheet)
2019
                                        $objChart->setTopLeftPosition($chartDetails[$chartPositionRef]['fromCoordinate'], $chartDetails[$chartPositionRef]['fromOffsetX'], $chartDetails[$chartPositionRef]['fromOffsetY']);
5✔
2020
                                        $objChart->setBottomRightPosition('', $chartDetails[$chartPositionRef]['width'], $chartDetails[$chartPositionRef]['height']);
5✔
2021
                                        if (array_key_exists('oneCellAnchor', $chartDetails[$chartPositionRef])) {
5✔
2022
                                            $objChart->setOneCellAnchor($chartDetails[$chartPositionRef]['oneCellAnchor']);
4✔
2023
                                        }
2024
                                    }
2025
                                }
2026
                            }
2027
                        }
2028

2029
                        break;
72✔
2030

2031
                        // unparsed
2032
                    case 'application/vnd.ms-excel.controlproperties+xml':
739✔
2033
                        $unparsedLoadedData['override_content_types'][(string) $contentType['PartName']] = (string) $contentType['ContentType'];
4✔
2034

2035
                        break;
4✔
2036
                }
2037
            }
2038
        }
2039

2040
        /** @var array<array<array<array<string>|string>>> $unparsedLoadedData */
2041
        $excel->setUnparsedLoadedData($unparsedLoadedData);
743✔
2042

2043
        $zip->close();
743✔
2044

2045
        return $excel;
743✔
2046
    }
2047

2048
    private function parseRichText(?SimpleXMLElement $is): RichText
93✔
2049
    {
2050
        $value = new RichText();
93✔
2051

2052
        if (isset($is->t)) {
93✔
2053
            $value->createText(StringHelper::controlCharacterOOXML2PHP((string) $is->t));
24✔
2054
        } elseif ($is !== null) {
71✔
2055
            if (is_object($is->r)) {
71✔
2056
                foreach ($is->r as $run) {
71✔
2057
                    if (!isset($run->rPr)) {
68✔
2058
                        $value->createText(StringHelper::controlCharacterOOXML2PHP((string) $run->t));
41✔
2059
                    } else {
2060
                        $objText = $value->createTextRun(StringHelper::controlCharacterOOXML2PHP((string) $run->t));
64✔
2061
                        $objFont = $objText->getFont() ?? new StyleFont();
64✔
2062

2063
                        if (isset($run->rPr->rFont)) {
64✔
2064
                            $attr = $run->rPr->rFont->attributes();
64✔
2065
                            if (isset($attr['val'])) {
64✔
2066
                                $objFont->setName((string) $attr['val']);
64✔
2067
                            }
2068
                        }
2069
                        if (isset($run->rPr->sz)) {
64✔
2070
                            $attr = $run->rPr->sz->attributes();
64✔
2071
                            if (isset($attr['val'])) {
64✔
2072
                                $objFont->setSize((float) $attr['val']);
64✔
2073
                            }
2074
                        }
2075
                        if (isset($run->rPr->color)) {
64✔
2076
                            $objFont->setColor(new Color($this->styleReader->readColor($run->rPr->color)));
54✔
2077
                        }
2078
                        if (isset($run->rPr->b)) {
64✔
2079
                            $attr = $run->rPr->b->attributes();
49✔
2080
                            if (
2081
                                (isset($attr['val']) && self::boolean((string) $attr['val']))
49✔
2082
                                || (!isset($attr['val']))
49✔
2083
                            ) {
2084
                                $objFont->setBold(true);
45✔
2085
                            }
2086
                        }
2087
                        if (isset($run->rPr->i)) {
64✔
2088
                            $attr = $run->rPr->i->attributes();
18✔
2089
                            if (
2090
                                (isset($attr['val']) && self::boolean((string) $attr['val']))
18✔
2091
                                || (!isset($attr['val']))
18✔
2092
                            ) {
2093
                                $objFont->setItalic(true);
9✔
2094
                            }
2095
                        }
2096
                        if (isset($run->rPr->vertAlign)) {
64✔
UNCOV
2097
                            $attr = $run->rPr->vertAlign->attributes();
×
UNCOV
2098
                            if (isset($attr['val'])) {
×
2099
                                $vertAlign = strtolower((string) $attr['val']);
×
UNCOV
2100
                                if ($vertAlign == 'superscript') {
×
UNCOV
2101
                                    $objFont->setSuperscript(true);
×
2102
                                }
UNCOV
2103
                                if ($vertAlign == 'subscript') {
×
2104
                                    $objFont->setSubscript(true);
×
2105
                                }
2106
                            }
2107
                        }
2108
                        if (isset($run->rPr->u)) {
64✔
2109
                            $attr = $run->rPr->u->attributes();
14✔
2110
                            if (!isset($attr['val'])) {
14✔
2111
                                $objFont->setUnderline(StyleFont::UNDERLINE_SINGLE);
1✔
2112
                            } else {
2113
                                $objFont->setUnderline((string) $attr['val']);
13✔
2114
                            }
2115
                        }
2116
                        if (isset($run->rPr->strike)) {
64✔
2117
                            $attr = $run->rPr->strike->attributes();
13✔
2118
                            if (
2119
                                (isset($attr['val']) && self::boolean((string) $attr['val']))
13✔
2120
                                || (!isset($attr['val']))
13✔
2121
                            ) {
UNCOV
2122
                                $objFont->setStrikethrough(true);
×
2123
                            }
2124
                        }
2125
                    }
2126
                }
2127
            }
2128
        }
2129

2130
        return $value;
93✔
2131
    }
2132

2133
    private function readRibbon(Spreadsheet $excel, string $customUITarget, ZipArchive $zip): void
2✔
2134
    {
2135
        $baseDir = dirname($customUITarget);
2✔
2136
        $nameCustomUI = basename($customUITarget);
2✔
2137
        // get the xml file (ribbon)
2138
        $localRibbon = $this->getFromZipArchive($zip, $customUITarget);
2✔
2139
        $customUIImagesNames = [];
2✔
2140
        $customUIImagesBinaries = [];
2✔
2141
        // something like customUI/_rels/customUI.xml.rels
2142
        $pathRels = $baseDir . '/_rels/' . $nameCustomUI . '.rels';
2✔
2143
        $dataRels = $this->getFromZipArchive($zip, $pathRels);
2✔
2144
        if ($dataRels) {
2✔
2145
            // exists and not empty if the ribbon have some pictures (other than internal MSO)
UNCOV
2146
            $UIRels = simplexml_load_string(
×
UNCOV
2147
                $this->getSecurityScannerOrThrow()
×
UNCOV
2148
                    ->scan($dataRels),
×
UNCOV
2149
                SimpleXMLElement::class,
×
UNCOV
2150
                $this->parseHuge ? LIBXML_PARSEHUGE : 0
×
UNCOV
2151
            );
×
UNCOV
2152
            if (false !== $UIRels) {
×
2153
                // we need to save id and target to avoid parsing customUI.xml and "guess" if it's a pseudo callback who load the image
UNCOV
2154
                foreach ($UIRels->Relationship as $ele) {
×
UNCOV
2155
                    if ((string) $ele['Type'] === Namespaces::SCHEMA_OFFICE_DOCUMENT . '/image') {
×
2156
                        // an image ?
2157
                        $customUIImagesNames[(string) $ele['Id']] = (string) $ele['Target'];
×
2158
                        $customUIImagesBinaries[(string) $ele['Target']] = $this->getFromZipArchive($zip, $baseDir . '/' . (string) $ele['Target']);
×
2159
                    }
2160
                }
2161
            }
2162
        }
2163
        if ($localRibbon) {
2✔
2164
            $excel->setRibbonXMLData($customUITarget, $localRibbon);
2✔
2165
            if (count($customUIImagesNames) > 0 && count($customUIImagesBinaries) > 0) {
2✔
UNCOV
2166
                $excel->setRibbonBinObjects($customUIImagesNames, $customUIImagesBinaries);
×
2167
            } else {
2168
                $excel->setRibbonBinObjects(null, null);
2✔
2169
            }
2170
        } else {
UNCOV
2171
            $excel->setRibbonXMLData(null, null);
×
UNCOV
2172
            $excel->setRibbonBinObjects(null, null);
×
2173
        }
2174
    }
2175

2176
    /** @param null|bool|mixed[]|SimpleXMLElement $array */
2177
    private static function getArrayItem(null|array|bool|SimpleXMLElement $array, int|string $key = 0): mixed
770✔
2178
    {
2179
        return ($array === null || is_bool($array)) ? null : ($array[$key] ?? null);
770✔
2180
    }
2181

2182
    /** @param null|bool|mixed[]|SimpleXMLElement $array */
2183
    private static function getArrayItemString(null|array|bool|SimpleXMLElement $array, int|string $key = 0): string
759✔
2184
    {
2185
        $retVal = self::getArrayItem($array, $key);
759✔
2186

2187
        return StringHelper::convertToString($retVal, false);
759✔
2188
    }
2189

2190
    /** @param null|bool|mixed[]|SimpleXMLElement $array */
2191
    private static function getArrayItemIntOrSxml(null|array|bool|SimpleXMLElement $array, int|string $key = 0): int|SimpleXMLElement
75✔
2192
    {
2193
        $retVal = self::getArrayItem($array, $key);
75✔
2194

2195
        return (is_int($retVal) || $retVal instanceof SimpleXMLElement) ? $retVal : 0;
75✔
2196
    }
2197

2198
    private static function dirAdd(null|SimpleXMLElement|string $base, null|SimpleXMLElement|string $add): string
400✔
2199
    {
2200
        $base = (string) $base;
400✔
2201
        $add = (string) $add;
400✔
2202

2203
        return Preg::replace('~[^/]+/\.\./~', '', dirname($base) . "/$add");
400✔
2204
    }
2205

2206
    /** @return mixed[] */
2207
    private static function toCSSArray(string $style): array
3✔
2208
    {
2209
        $style = self::stripWhiteSpaceFromStyleString($style);
3✔
2210

2211
        $temp = explode(';', $style);
3✔
2212
        $style = [];
3✔
2213
        foreach ($temp as $item) {
3✔
2214
            $item = explode(':', $item);
3✔
2215

2216
            if (str_contains($item[1], 'px')) {
3✔
2217
                $item[1] = str_replace('px', '', $item[1]);
2✔
2218
            }
2219
            if (str_contains($item[1], 'pt')) {
3✔
2220
                $item[1] = str_replace('pt', '', $item[1]);
2✔
2221
                $item[1] = (string) Font::fontSizeToPixels((int) $item[1]);
2✔
2222
            }
2223
            if (str_contains($item[1], 'in')) {
3✔
UNCOV
2224
                $item[1] = str_replace('in', '', $item[1]);
×
UNCOV
2225
                $item[1] = (string) Font::inchSizeToPixels((int) $item[1]);
×
2226
            }
2227
            if (str_contains($item[1], 'cm')) {
3✔
UNCOV
2228
                $item[1] = str_replace('cm', '', $item[1]);
×
UNCOV
2229
                $item[1] = (string) Font::centimeterSizeToPixels((int) $item[1]);
×
2230
            }
2231

2232
            $style[$item[0]] = $item[1];
3✔
2233
        }
2234

2235
        return $style;
3✔
2236
    }
2237

2238
    public static function stripWhiteSpaceFromStyleString(string $string): string
6✔
2239
    {
2240
        return trim(str_replace(["\r", "\n", ' '], '', $string), ';');
6✔
2241
    }
2242

2243
    private static function boolean(string $value): bool
105✔
2244
    {
2245
        if (is_numeric($value)) {
105✔
2246
            return (bool) $value;
72✔
2247
        }
2248

2249
        return $value === 'true' || $value === 'TRUE';
46✔
2250
    }
2251

2252
    /** @param string[] $hyperlinks */
2253
    private function readHyperLinkDrawing(\PhpOffice\PhpSpreadsheet\Worksheet\Drawing $objDrawing, SimpleXMLElement $cellAnchor, array $hyperlinks): void
72✔
2254
    {
2255
        $hlinkClick = $cellAnchor->pic->nvPicPr->cNvPr->children(Namespaces::DRAWINGML)->hlinkClick;
72✔
2256

2257
        if ($hlinkClick->count() === 0) {
72✔
2258
            return;
68✔
2259
        }
2260

2261
        $hlinkId = (string) self::getAttributes($hlinkClick, Namespaces::SCHEMA_OFFICE_DOCUMENT)['id'];
4✔
2262
        $hyperlink = new Hyperlink(
4✔
2263
            Preg::replace('/^#/', 'sheet://', $hyperlinks[$hlinkId]),
4✔
2264
            self::getArrayItemString(
4✔
2265
                self::getAttributes(
4✔
2266
                    $cellAnchor->pic->nvPicPr->cNvPr
4✔
2267
                ),
4✔
2268
                'name'
4✔
2269
            )
4✔
2270
        );
4✔
2271
        $objDrawing->setHyperlink($hyperlink);
4✔
2272
    }
2273

2274
    private function readProtection(Spreadsheet $excel, SimpleXMLElement $xmlWorkbook): void
746✔
2275
    {
2276
        if (!$xmlWorkbook->workbookProtection) {
746✔
2277
            return;
713✔
2278
        }
2279

2280
        $security = $excel->getSecurity();
38✔
2281
        $security->setLockRevision(
38✔
2282
            self::getLockValue($xmlWorkbook->workbookProtection, 'lockRevision')
38✔
2283
        );
38✔
2284
        $security->setLockStructure(
38✔
2285
            self::getLockValue($xmlWorkbook->workbookProtection, 'lockStructure')
38✔
2286
        );
38✔
2287
        $security->setLockWindows(
38✔
2288
            self::getLockValue($xmlWorkbook->workbookProtection, 'lockWindows')
38✔
2289
        );
38✔
2290

2291
        if ($xmlWorkbook->workbookProtection['revisionsPassword']) {
38✔
2292
            $security->setRevisionsPassword(
1✔
2293
                (string) $xmlWorkbook->workbookProtection['revisionsPassword'],
1✔
2294
                true
1✔
2295
            );
1✔
2296
        }
2297
        if ($xmlWorkbook->workbookProtection['revisionsAlgorithmName']) {
38✔
2298
            $security->setRevisionsAlgorithmName(
1✔
2299
                (string) $xmlWorkbook->workbookProtection['revisionsAlgorithmName']
1✔
2300
            );
1✔
2301
        }
2302
        if ($xmlWorkbook->workbookProtection['revisionsSaltValue']) {
38✔
2303
            $security->setRevisionsSaltValue(
1✔
2304
                (string) $xmlWorkbook->workbookProtection['revisionsSaltValue'],
1✔
2305
                false
1✔
2306
            );
1✔
2307
        }
2308
        if ($xmlWorkbook->workbookProtection['revisionsSpinCount']) {
38✔
2309
            $security->setRevisionsSpinCount(
1✔
2310
                (int) $xmlWorkbook->workbookProtection['revisionsSpinCount']
1✔
2311
            );
1✔
2312
        }
2313
        if ($xmlWorkbook->workbookProtection['revisionsHashValue']) {
38✔
2314
            if ($security->advancedRevisionsPassword()) {
1✔
2315
                $security->setRevisionsPassword(
1✔
2316
                    (string) $xmlWorkbook->workbookProtection['revisionsHashValue'],
1✔
2317
                    true
1✔
2318
                );
1✔
2319
            }
2320
        }
2321

2322
        if ($xmlWorkbook->workbookProtection['workbookPassword']) {
38✔
2323
            $security->setWorkbookPassword(
2✔
2324
                (string) $xmlWorkbook->workbookProtection['workbookPassword'],
2✔
2325
                true
2✔
2326
            );
2✔
2327
        }
2328

2329
        if ($xmlWorkbook->workbookProtection['workbookAlgorithmName']) {
38✔
2330
            $security->setWorkbookAlgorithmName(
2✔
2331
                (string) $xmlWorkbook->workbookProtection['workbookAlgorithmName']
2✔
2332
            );
2✔
2333
        }
2334
        if ($xmlWorkbook->workbookProtection['workbookSaltValue']) {
38✔
2335
            $security->setWorkbookSaltValue(
2✔
2336
                (string) $xmlWorkbook->workbookProtection['workbookSaltValue'],
2✔
2337
                false
2✔
2338
            );
2✔
2339
        }
2340
        if ($xmlWorkbook->workbookProtection['workbookSpinCount']) {
38✔
2341
            $security->setWorkbookSpinCount(
2✔
2342
                (int) $xmlWorkbook->workbookProtection['workbookSpinCount']
2✔
2343
            );
2✔
2344
        }
2345
        if ($xmlWorkbook->workbookProtection['workbookHashValue']) {
38✔
2346
            if ($security->advancedPassword()) {
2✔
2347
                $security->setWorkbookPassword(
2✔
2348
                    (string) $xmlWorkbook->workbookProtection['workbookHashValue'],
2✔
2349
                    true
2✔
2350
                );
2✔
2351
            }
2352
        }
2353
    }
2354

2355
    private static function getLockValue(SimpleXMLElement $protection, string $key): ?bool
38✔
2356
    {
2357
        $returnValue = null;
38✔
2358
        $protectKey = $protection[$key];
38✔
2359
        if (!empty($protectKey)) {
38✔
2360
            $protectKey = (string) $protectKey;
12✔
2361
            $returnValue = $protectKey !== 'false' && (bool) $protectKey;
12✔
2362
        }
2363

2364
        return $returnValue;
38✔
2365
    }
2366

2367
    /** @param mixed[][][][] $unparsedLoadedData */
2368
    private function readFormControlProperties(Spreadsheet $excel, string $dir, string $fileWorksheet, Worksheet $docSheet, array &$unparsedLoadedData): void
741✔
2369
    {
2370
        $zip = $this->zip;
741✔
2371
        if ($zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels') === false) {
741✔
2372
            return;
358✔
2373
        }
2374

2375
        $filename = dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels';
611✔
2376
        $relsWorksheet = $this->loadZipNoNamespace($filename, Namespaces::RELATIONSHIPS);
611✔
2377
        $ctrlProps = [];
611✔
2378
        foreach ($relsWorksheet->Relationship as $ele) {
611✔
2379
            if ((string) $ele['Type'] === Namespaces::SCHEMA_OFFICE_DOCUMENT . '/ctrlProp') {
425✔
2380
                $ctrlProps[(string) $ele['Id']] = $ele;
4✔
2381
            }
2382
        }
2383

2384
        /** @var mixed[][] */
2385
        $unparsedCtrlProps = &$unparsedLoadedData['sheets'][$docSheet->getCodeName()]['ctrlProps'];
611✔
2386
        foreach ($ctrlProps as $rId => $ctrlProp) {
611✔
2387
            $rId = substr($rId, 3); // rIdXXX
4✔
2388
            $unparsedCtrlProps[$rId] = [];
4✔
2389
            $unparsedCtrlProps[$rId]['filePath'] = self::dirAdd("$dir/$fileWorksheet", $ctrlProp['Target']);
4✔
2390
            $unparsedCtrlProps[$rId]['relFilePath'] = (string) $ctrlProp['Target'];
4✔
2391
            $unparsedCtrlProps[$rId]['content'] = $this->getSecurityScannerOrThrow()->scan($this->getFromZipArchive($zip, $unparsedCtrlProps[$rId]['filePath']));
4✔
2392
        }
2393
        unset($unparsedCtrlProps);
611✔
2394
    }
2395

2396
    /** @param mixed[][][][] $unparsedLoadedData */
2397
    private function readPrinterSettings(Spreadsheet $excel, string $dir, string $fileWorksheet, Worksheet $docSheet, array &$unparsedLoadedData): void
741✔
2398
    {
2399
        if ($this->readDataOnly) {
741✔
2400
            return;
3✔
2401
        }
2402
        $zip = $this->zip;
738✔
2403
        if ($zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels') === false) {
738✔
2404
            return;
357✔
2405
        }
2406

2407
        $filename = dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels';
608✔
2408
        $relsWorksheet = $this->loadZipNoNamespace($filename, Namespaces::RELATIONSHIPS);
608✔
2409
        $sheetPrinterSettings = [];
608✔
2410
        foreach ($relsWorksheet->Relationship as $ele) {
608✔
2411
            if ((string) $ele['Type'] === Namespaces::SCHEMA_OFFICE_DOCUMENT . '/printerSettings') {
423✔
2412
                $sheetPrinterSettings[(string) $ele['Id']] = $ele;
289✔
2413
            }
2414
        }
2415

2416
        /** @var mixed[][] */
2417
        $unparsedPrinterSettings = &$unparsedLoadedData['sheets'][$docSheet->getCodeName()]['printerSettings'];
608✔
2418
        foreach ($sheetPrinterSettings as $rId => $printerSettings) {
608✔
2419
            $rId = substr($rId, 3); // rIdXXX
289✔
2420
            if (!str_ends_with($rId, 'ps')) {
289✔
2421
                $rId = $rId . 'ps'; // rIdXXX, add 'ps' suffix to avoid identical resource identifier collision with unparsed vmlDrawing
289✔
2422
            }
2423
            $unparsedPrinterSettings[$rId] = [];
289✔
2424
            $target = (string) str_replace('/xl/', '../', (string) $printerSettings['Target']);
289✔
2425
            $unparsedPrinterSettings[$rId]['filePath'] = self::dirAdd("$dir/$fileWorksheet", $target);
289✔
2426
            $unparsedPrinterSettings[$rId]['relFilePath'] = $target;
289✔
2427
            $unparsedPrinterSettings[$rId]['content'] = $this->getSecurityScannerOrThrow()->scan($this->getFromZipArchive($zip, $unparsedPrinterSettings[$rId]['filePath']));
289✔
2428
        }
2429
        unset($unparsedPrinterSettings);
608✔
2430
    }
2431

2432
    /** @return array{string, string} */
2433
    private function getWorkbookBaseName(): array
757✔
2434
    {
2435
        $workbookBasename = '';
757✔
2436
        $xmlNamespaceBase = '';
757✔
2437

2438
        // check if it is an OOXML archive
2439
        $rels = $this->loadZip(self::INITIAL_FILE);
757✔
2440
        foreach ($rels->children(Namespaces::RELATIONSHIPS)->Relationship as $rel) {
757✔
2441
            $rel = self::getAttributes($rel);
757✔
2442
            $type = (string) $rel['Type'];
757✔
2443
            switch ($type) {
2444
                case Namespaces::OFFICE_DOCUMENT:
750✔
2445
                case Namespaces::PURL_OFFICE_DOCUMENT:
735✔
2446
                    $basename = basename((string) $rel['Target']);
757✔
2447
                    $xmlNamespaceBase = dirname($type);
757✔
2448
                    if (Preg::isMatch('/workbook.*\.xml/', $basename)) {
757✔
2449
                        $workbookBasename = $basename;
757✔
2450
                    }
2451

2452
                    break;
757✔
2453
            }
2454
        }
2455

2456
        return [$workbookBasename, $xmlNamespaceBase];
757✔
2457
    }
2458

2459
    private function readSheetProtection(Worksheet $docSheet, SimpleXMLElement $xmlSheet): void
732✔
2460
    {
2461
        if ($this->readDataOnly || !$xmlSheet->sheetProtection) {
732✔
2462
            return;
674✔
2463
        }
2464

2465
        $algorithmName = (string) $xmlSheet->sheetProtection['algorithmName'];
69✔
2466
        $protection = $docSheet->getProtection();
69✔
2467
        $protection->setAlgorithm($algorithmName);
69✔
2468

2469
        if ($algorithmName) {
69✔
2470
            $protection->setPassword((string) $xmlSheet->sheetProtection['hashValue'], true);
2✔
2471
            $protection->setSalt((string) $xmlSheet->sheetProtection['saltValue']);
2✔
2472
            $protection->setSpinCount((int) $xmlSheet->sheetProtection['spinCount']);
2✔
2473
        } else {
2474
            $protection->setPassword((string) $xmlSheet->sheetProtection['password'], true);
68✔
2475
        }
2476

2477
        if ($xmlSheet->protectedRanges->protectedRange) {
69✔
2478
            foreach ($xmlSheet->protectedRanges->protectedRange as $protectedRange) {
4✔
2479
                $docSheet->protectCells((string) $protectedRange['sqref'], (string) $protectedRange['password'], true, (string) $protectedRange['name'], (string) $protectedRange['securityDescriptor']);
4✔
2480
            }
2481
        }
2482
    }
2483

2484
    private function readAutoFilter(
739✔
2485
        SimpleXMLElement $xmlSheet,
2486
        Worksheet $docSheet
2487
    ): void {
2488
        if ($xmlSheet && $xmlSheet->autoFilter) {
739✔
2489
            (new AutoFilter($docSheet, $xmlSheet))->load();
18✔
2490
        }
2491
    }
2492

2493
    private function readBackgroundImage(
739✔
2494
        SimpleXMLElement $xmlSheet,
2495
        Worksheet $docSheet,
2496
        string $relsName
2497
    ): void {
2498
        if ($xmlSheet && $xmlSheet->picture) {
739✔
2499
            $id = (string) self::getArrayItemString(self::getAttributes($xmlSheet->picture, Namespaces::SCHEMA_OFFICE_DOCUMENT), 'id');
1✔
2500
            $rels = $this->loadZip($relsName);
1✔
2501
            foreach ($rels->Relationship as $rel) {
1✔
2502
                $attrs = $rel->attributes() ?? [];
1✔
2503
                $rid = (string) ($attrs['Id'] ?? '');
1✔
2504
                $target = (string) ($attrs['Target'] ?? '');
1✔
2505
                if ($rid === $id && str_starts_with($target, '..')) {
1✔
2506
                    $target = 'xl' . substr($target, 2);
1✔
2507
                    $content = $this->getFromZipArchive($this->zip, $target);
1✔
2508
                    $docSheet->setBackgroundImage($content);
1✔
2509
                }
2510
            }
2511
        }
2512
    }
2513

2514
    /**
2515
     * @param TableDxfsStyle[] $tableStyles
2516
     * @param Style[] $dxfs
2517
     */
2518
    private function readTables(
742✔
2519
        SimpleXMLElement $xmlSheet,
2520
        Worksheet $docSheet,
2521
        string $dir,
2522
        string $fileWorksheet,
2523
        ZipArchive $zip,
2524
        string $namespaceTable,
2525
        array $tableStyles,
2526
        array $dxfs
2527
    ): void {
2528
        if ($xmlSheet && $xmlSheet->tableParts) {
742✔
2529
            /** @var array{count: scalar} */
2530
            $attributes = $xmlSheet->tableParts->attributes() ?? ['count' => 0];
37✔
2531
            if (((int) $attributes['count']) > 0) {
37✔
2532
                $this->readTablesInTablesFile($xmlSheet, $dir, $fileWorksheet, $zip, $docSheet, $namespaceTable, $tableStyles, $dxfs);
33✔
2533
            }
2534
        }
2535
    }
2536

2537
    /**
2538
     * @param TableDxfsStyle[] $tableStyles
2539
     * @param Style[] $dxfs
2540
     */
2541
    private function readTablesInTablesFile(
33✔
2542
        SimpleXMLElement $xmlSheet,
2543
        string $dir,
2544
        string $fileWorksheet,
2545
        ZipArchive $zip,
2546
        Worksheet $docSheet,
2547
        string $namespaceTable,
2548
        array $tableStyles,
2549
        array $dxfs
2550
    ): void {
2551
        foreach ($xmlSheet->tableParts->tablePart as $tablePart) {
33✔
2552
            $relation = self::getAttributes($tablePart, Namespaces::SCHEMA_OFFICE_DOCUMENT);
33✔
2553
            $tablePartRel = (string) $relation['id'];
33✔
2554
            $relationsFileName = dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels';
33✔
2555

2556
            if ($zip->locateName($relationsFileName) !== false) {
33✔
2557
                $relsTableReferences = $this->loadZip($relationsFileName, Namespaces::RELATIONSHIPS);
33✔
2558
                foreach ($relsTableReferences->Relationship as $relationship) {
33✔
2559
                    $relationshipAttributes = self::getAttributes($relationship, '');
33✔
2560

2561
                    if ((string) $relationshipAttributes['Id'] === $tablePartRel) {
33✔
2562
                        $relationshipFileName = (string) $relationshipAttributes['Target'];
33✔
2563
                        $relationshipFilePath = dirname("$dir/$fileWorksheet") . '/' . $relationshipFileName;
33✔
2564
                        $relationshipFilePath = File::realpath($relationshipFilePath);
33✔
2565

2566
                        if ($this->fileExistsInArchive($this->zip, $relationshipFilePath)) {
33✔
2567
                            $tableXml = $this->loadZip($relationshipFilePath, $namespaceTable);
33✔
2568
                            (new TableReader($docSheet, $tableXml))->load($tableStyles, $dxfs);
33✔
2569
                        }
2570
                    }
2571
                }
2572
            }
2573
        }
2574
    }
2575

2576
    /** @return mixed[] */
2577
    private static function extractStyles(?SimpleXMLElement $sxml, string $node1, string $node2): array
753✔
2578
    {
2579
        $array = [];
753✔
2580
        if ($sxml && $sxml->{$node1}->{$node2}) {
753✔
2581
            /** @var SimpleXMLElement */
2582
            $temp = $sxml->{$node1}->{$node2};
753✔
2583
            foreach ($temp as $node) {
753✔
2584
                $array[] = $node;
753✔
2585
            }
2586
        }
2587

2588
        return $array;
753✔
2589
    }
2590

2591
    /** @return string[] */
2592
    private static function extractPalette(?SimpleXMLElement $sxml): array
753✔
2593
    {
2594
        $array = [];
753✔
2595
        if ($sxml && $sxml->colors->indexedColors) {
753✔
2596
            foreach ($sxml->colors->indexedColors->rgbColor as $node) {
16✔
2597
                $attr = $node->attributes();
16✔
2598
                if (isset($attr['rgb'])) {
16✔
2599
                    $array[] = (string) $attr['rgb'];
16✔
2600
                }
2601
            }
2602
        }
2603

2604
        return $array;
753✔
2605
    }
2606

2607
    private function processIgnoredErrors(SimpleXMLElement $xml, Worksheet $sheet): void
4✔
2608
    {
2609
        $cellCollection = $sheet->getCellCollection();
4✔
2610
        $attributes = self::getAttributes($xml);
4✔
2611
        $sqref = (string) ($attributes['sqref'] ?? '');
4✔
2612
        $numberStoredAsText = (string) ($attributes['numberStoredAsText'] ?? '');
4✔
2613
        $formula = (string) ($attributes['formula'] ?? '');
4✔
2614
        $formulaRange = (string) ($attributes['formulaRange'] ?? '');
4✔
2615
        $twoDigitTextYear = (string) ($attributes['twoDigitTextYear'] ?? '');
4✔
2616
        $evalError = (string) ($attributes['evalError'] ?? '');
4✔
2617
        if (!empty($sqref)) {
4✔
2618
            $explodedSqref = explode(' ', $sqref);
4✔
2619
            $pattern1 = '/^([A-Z]{1,3})([0-9]{1,7})(:([A-Z]{1,3})([0-9]{1,7}))?$/';
4✔
2620
            foreach ($explodedSqref as $sqref1) {
4✔
2621
                if (Preg::isMatch($pattern1, $sqref1, $matches)) {
4✔
2622
                    $firstRow = $matches[2];
4✔
2623
                    $firstCol = $matches[1];
4✔
2624
                    if ($matches[3] !== null) {
4✔
2625
                        $lastCol = (string) $matches[4];
3✔
2626
                        $lastRow = (string) $matches[5];
3✔
2627
                    } else {
2628
                        $lastCol = $firstCol;
3✔
2629
                        $lastRow = $firstRow;
3✔
2630
                    }
2631
                    StringHelper::stringIncrement($lastCol);
4✔
2632
                    for ($row = $firstRow; $row <= $lastRow; ++$row) {
4✔
2633
                        for ($col = $firstCol; $col !== $lastCol; StringHelper::stringIncrement($col)) {
4✔
2634
                            if (!$cellCollection->has2("$col$row")) {
4✔
2635
                                continue;
1✔
2636
                            }
2637
                            if ($numberStoredAsText === '1') {
4✔
2638
                                $sheet->getCell("$col$row")->getIgnoredErrors()->setNumberStoredAsText(true);
4✔
2639
                            }
2640
                            if ($formula === '1') {
4✔
2641
                                $sheet->getCell("$col$row")->getIgnoredErrors()->setFormula(true);
1✔
2642
                            }
2643
                            if ($formulaRange === '1') {
4✔
2644
                                $sheet->getCell("$col$row")->getIgnoredErrors()->setFormulaRange(true);
1✔
2645
                            }
2646
                            if ($twoDigitTextYear === '1') {
4✔
2647
                                $sheet->getCell("$col$row")->getIgnoredErrors()->setTwoDigitTextYear(true);
1✔
2648
                            }
2649
                            if ($evalError === '1') {
4✔
2650
                                $sheet->getCell("$col$row")->getIgnoredErrors()->setEvalError(true);
1✔
2651
                            }
2652
                        }
2653
                    }
2654
                }
2655
            }
2656
        }
2657
    }
2658

2659
    private static function storeFormulaAttributes(SimpleXMLElement $f, Worksheet $docSheet, string $r): void
375✔
2660
    {
2661
        $formulaAttributes = [];
375✔
2662
        $attributes = $f->attributes();
375✔
2663
        if (isset($attributes['t'])) {
375✔
2664
            $formulaAttributes['t'] = (string) $attributes['t'];
249✔
2665
        }
2666
        if (isset($attributes['ref'])) {
375✔
2667
            $formulaAttributes['ref'] = (string) $attributes['ref'];
249✔
2668
        }
2669
        if (!empty($formulaAttributes)) {
375✔
2670
            $docSheet->getCell($r)->setFormulaAttributes($formulaAttributes);
249✔
2671
        }
2672
    }
2673

2674
    private static function onlyNoteVml(string $data): bool
22✔
2675
    {
2676
        $data = str_replace('<br>', '<br/>', $data);
22✔
2677

2678
        try {
2679
            $sxml = @simplexml_load_string($data);
22✔
UNCOV
2680
        } catch (Throwable) {
×
UNCOV
2681
            $sxml = false;
×
2682
        }
2683

2684
        if ($sxml === false) {
22✔
2685
            return false;
1✔
2686
        }
2687
        $shapes = $sxml->children(Namespaces::URN_VML);
21✔
2688
        foreach ($shapes->shape as $shape) {
21✔
2689
            $clientData = $shape->children(Namespaces::URN_EXCEL);
21✔
2690
            if (!isset($clientData->ClientData)) {
21✔
UNCOV
2691
                return false;
×
2692
            }
2693
            $attrs = $clientData->ClientData->attributes();
21✔
2694
            if (!isset($attrs['ObjectType'])) {
21✔
UNCOV
2695
                return false;
×
2696
            }
2697
            $objectType = (string) $attrs['ObjectType'];
21✔
2698
            if ($objectType !== 'Note') {
21✔
2699
                return false;
4✔
2700
            }
2701
        }
2702

2703
        return true;
18✔
2704
    }
2705
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc