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

PHPOffice / PhpSpreadsheet / 21750822390

06 Feb 2026 12:37PM UTC coverage: 96.269% (-0.004%) from 96.273%
21750822390

Pull #4801

github

web-flow
Merge d52f556f2 into 95b826a3c
Pull Request #4801: Add support for image CSS size in millimetres

2 of 6 new or added lines in 1 file covered. (33.33%)

5 existing lines in 1 file now uncovered.

46495 of 48297 relevant lines covered (96.27%)

387.82 hits per line

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

95.93
/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
     */
73
    public function setParseHuge(bool $parseHuge): void
×
74
    {
75
        $this->parseHuge = $parseHuge;
×
76
    }
77

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

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

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

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

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

107
        return $result;
24✔
108
    }
109

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

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

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

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

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

146
        return self::testSimpleXml($rels);
787✔
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
754✔
152
    {
153
        $contents = $this->getFromZipArchive($this->zip, $filename);
754✔
154
        $rels = simplexml_load_string(
754✔
155
            $this->getSecurityScannerOrThrow()->scan($contents),
754✔
156
            SimpleXMLElement::class,
754✔
157
            $this->parseHuge ? LIBXML_PARSEHUGE : 0,
754✔
158
            ($ns === '' ? $ns : '')
754✔
159
        );
754✔
160

161
        return self::testSimpleXml($rels);
749✔
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
24✔
323
    {
324
        $value = isset($c->v) ? (string) $c->v : null;
24✔
325
        if ($value == '0') {
24✔
326
            return false;
17✔
327
        } elseif ($value == '1') {
20✔
328
            return true;
20✔
329
        }
330

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
567✔
340
    {
341
        return isset($c, $c->v) ? (string) $c->v : null;
567✔
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✔
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
740✔
382
    {
383
        // Root-relative paths
384
        if (str_contains($fileName, '//')) {
740✔
385
            $fileName = substr($fileName, strpos($fileName, '//') + 1);
1✔
386
        }
387
        $fileName = File::realpath($fileName);
740✔
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);
740✔
394
        if ($contents === false) {
740✔
395
            $contents = $archive->locateName(substr($fileName, 1), ZipArchive::FL_NOCASE);
4✔
396
        }
397

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

401
    private function getFromZipArchive(ZipArchive $archive, string $fileName = ''): string
787✔
402
    {
403
        // Root-relative paths
404
        if (str_contains($fileName, '//')) {
787✔
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);
787✔
410
        $fileName = File::realpath($fileName);
787✔
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);
787✔
416

417
        // Apache POI fixes
418
        if ($contents === false) {
787✔
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) {
787✔
424
            $contents = $archive->getFromName(str_replace('/', '\\', $fileName), 0, ZipArchive::FL_NOCASE);
53✔
425
        }
426

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

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

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

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

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

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

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

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

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

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

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

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

536
                    break;
735✔
537
            }
538
        }
539

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

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

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

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

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

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

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

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

590
                                break;
756✔
591
                            case Namespaces::CHARTSHEET:
750✔
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:
750✔
599
                                $macros = $ele['Target'];
3✔
600

601
                                break;
3✔
602
                        }
603
                    }
604

605
                    if ($macros !== null) {
756✔
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='"
756✔
617
                        . "$xmlNamespaceBase/styles"
756✔
618
                        . "']";
756✔
619
                    /** @var ?SimpleXMLElement */
620
                    $xpath = self::getArrayItem(self::xpathNoFalse($relsWorkbook, $relType));
756✔
621

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

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

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

655
                            if ($xf['numFmtId']) {
753✔
656
                                if (isset($numFmts)) {
751✔
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
751✔
670
                                    && (int) $xf['numFmtId'] < 164
751✔
671
                                    && NumberFormat::builtInFormatCode((int) $xf['numFmtId']) !== ''
751✔
672
                                ) {
673
                                    $numFmt = NumberFormat::builtInFormatCode((int) $xf['numFmtId']);
728✔
674
                                }
675
                            }
676
                            $quotePrefix = (bool) (string) ($xf['quotePrefix'] ?? '');
753✔
677

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

689
                            // add style to cellXf collection
690
                            $objStyle = new Style();
753✔
691
                            $this->styleReader
753✔
692
                                ->readStyle($objStyle, $style);
753✔
693
                            if (isset($xfTag->extLst)) {
753✔
694
                                foreach ($xfTag->extLst->ext as $extTag) {
2✔
695
                                    $attributes = $extTag->attributes();
2✔
696
                                    if (isset($attributes['uri'])) {
2✔
697
                                        if ((string) $attributes['uri'] === Namespaces::STYLE_CHECKBOX_URI) {
2✔
698
                                            $objStyle->setCheckBox(true);
2✔
699
                                        }
700
                                    }
701
                                }
702
                            }
703
                            foreach ($this->styleReader->getFontCharsets() as $fontName => $charset) {
753✔
704
                                $excel->addFontCharset($fontName, $charset);
34✔
705
                            }
706
                            if ($addingFirstCellXf) {
753✔
707
                                $excel->removeCellXfByIndex(0); // remove the default style
753✔
708
                                $addingFirstCellXf = false;
753✔
709
                            }
710
                            $excel->addCellXf($objStyle);
753✔
711
                        }
712

713
                        foreach ($cellXfTags as $xfTag) {
753✔
714
                            /** @var SimpleXMLElement $xfTag */
715
                            $xf = self::getAttributes($xfTag);
752✔
716
                            $numFmt = NumberFormat::FORMAT_GENERAL;
752✔
717
                            if ($numFmts && $xf['numFmtId']) {
752✔
718
                                /** @var ?SimpleXMLElement */
719
                                $tmpNumFmt = self::getArrayItem($numFmts->xpath("sml:numFmt[@numFmtId=$xf[numFmtId]]"));
263✔
720
                                if (isset($tmpNumFmt['formatCode'])) {
263✔
721
                                    $numFmt = (string) $tmpNumFmt['formatCode'];
42✔
722
                                } elseif ((int) $xf['numFmtId'] < 165) {
261✔
723
                                    $numFmt = NumberFormat::builtInFormatCode((int) $xf['numFmtId']);
261✔
724
                                }
725
                            }
726

727
                            $quotePrefix = (bool) (string) ($xf['quotePrefix'] ?? '');
752✔
728

729
                            $cellStyle = (object) [
752✔
730
                                'numFmt' => $numFmt,
752✔
731
                                'font' => $fonts[(int) ($xf['fontId'])],
752✔
732
                                'fill' => $fills[((int) $xf['fillId'])],
752✔
733
                                'border' => $borders[(int) ($xf['borderId'])],
752✔
734
                                'alignment' => $xfTag->alignment,
752✔
735
                                'protection' => $xfTag->protection,
752✔
736
                                'quotePrefix' => $quotePrefix,
752✔
737
                            ];
752✔
738
                            $cellStyles[] = $cellStyle;
752✔
739

740
                            // add style to cellStyleXf collection
741
                            $objStyle = new Style();
752✔
742
                            $this->styleReader->readStyle($objStyle, $cellStyle);
752✔
743
                            if ($addingFirstCellStyleXf) {
752✔
744
                                $excel->removeCellStyleXfByIndex(0); // remove the default style
752✔
745
                                $addingFirstCellStyleXf = false;
752✔
746
                            }
747
                            $excel->addCellStyleXf($objStyle);
752✔
748
                        }
749
                    }
750
                    $this->styleReader->setStyleXml($xmlStyles);
756✔
751
                    $this->styleReader->setNamespace($mainNS);
756✔
752
                    $this->styleReader->setStyleBaseData($theme, $styles, $cellStyles);
756✔
753
                    $dxfs = $this->styleReader->dxfs($this->readDataOnly);
756✔
754
                    $tableStyles = $this->styleReader->tableStyles($this->readDataOnly);
756✔
755
                    $styles = $this->styleReader->styles();
756✔
756

757
                    // Read content after setting the styles
758
                    $sharedStrings = [];
756✔
759
                    $relType = "rel:Relationship[@Type='"
756✔
760
                        //. Namespaces::SHARED_STRINGS
756✔
761
                        . "$xmlNamespaceBase/sharedStrings"
756✔
762
                        . "']";
756✔
763
                    /** @var ?SimpleXMLElement */
764
                    $xpath = self::getArrayItem($relsWorkbook->xpath($relType));
756✔
765

766
                    if ($xpath) {
756✔
767
                        $sharedStringsTarget = (string) $xpath['Target'];
689✔
768
                        $sharedStringsTarget = str_starts_with($sharedStringsTarget, '/') ? substr($sharedStringsTarget, 1) : "$dir/$sharedStringsTarget";
689✔
769
                        $xmlStrings = $this->loadZip($sharedStringsTarget, $mainNS);
689✔
770
                        if (isset($xmlStrings->si)) {
687✔
771
                            foreach ($xmlStrings->si as $val) {
543✔
772
                                if (isset($val->t)) {
543✔
773
                                    $sharedStrings[] = StringHelper::controlCharacterOOXML2PHP((string) $val->t);
539✔
774
                                } elseif (isset($val->r)) {
45✔
775
                                    $sharedStrings[] = $this->parseRichText($val);
45✔
776
                                } else {
777
                                    $sharedStrings[] = '';
1✔
778
                                }
779
                            }
780
                        }
781
                    }
782

783
                    $xmlWorkbook = $this->loadZipNoNamespace($relTarget, $mainNS);
754✔
784
                    $xmlWorkbookNS = $this->loadZip($relTarget, $mainNS);
749✔
785

786
                    // Set base date
787
                    $excel->setExcelCalendar(Date::CALENDAR_WINDOWS_1900);
749✔
788
                    if ($xmlWorkbookNS->workbookPr) {
749✔
789
                        Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900);
740✔
790
                        $attrs1904 = self::getAttributes($xmlWorkbookNS->workbookPr);
740✔
791
                        if (isset($attrs1904['date1904'])) {
740✔
792
                            if (self::boolean((string) $attrs1904['date1904'])) {
28✔
793
                                Date::setExcelCalendar(Date::CALENDAR_MAC_1904);
4✔
794
                                $excel->setExcelCalendar(Date::CALENDAR_MAC_1904);
4✔
795
                            }
796
                        }
797
                    }
798

799
                    // Set protection
800
                    $this->readProtection($excel, $xmlWorkbook);
749✔
801

802
                    $sheetId = 0; // keep track of new sheet id in final workbook
749✔
803
                    $oldSheetId = -1; // keep track of old sheet id in final workbook
749✔
804
                    $countSkippedSheets = 0; // keep track of number of skipped sheets
749✔
805
                    $mapSheetId = []; // mapping of sheet ids from old to new
749✔
806

807
                    $charts = $chartDetails = [];
749✔
808

809
                    // Add richData (contains relation of in-cell images)
810
                    $richData = [];
749✔
811
                    $relationsFileName = $dir . '/richData/_rels/richValueRel.xml.rels';
749✔
812
                    if ($zip->locateName($relationsFileName)) {
749✔
813
                        $relsWorksheet = $this->loadZip($relationsFileName, Namespaces::RELATIONSHIPS);
6✔
814
                        foreach ($relsWorksheet->Relationship as $elex) {
6✔
815
                            $ele = self::getAttributes($elex);
6✔
816
                            if ($ele['Type'] == Namespaces::IMAGE) {
6✔
817
                                $richData['image'][(string) $ele['Id']] = (string) $ele['Target'];
6✔
818
                            }
819
                        }
820
                    }
821

822
                    $sheetCreated = false;
749✔
823
                    if ($xmlWorkbookNS->sheets) {
749✔
824
                        foreach ($xmlWorkbookNS->sheets->sheet as $eleSheet) {
749✔
825
                            $eleSheetAttr = self::getAttributes($eleSheet);
749✔
826
                            ++$oldSheetId;
749✔
827

828
                            // Check if sheet should be skipped
829
                            if (is_array($this->loadSheetsOnly) && !in_array((string) $eleSheetAttr['name'], $this->loadSheetsOnly)) {
749✔
830
                                ++$countSkippedSheets;
9✔
831
                                $mapSheetId[$oldSheetId] = null;
9✔
832

833
                                continue;
9✔
834
                            }
835

836
                            $sheetReferenceId = self::getArrayItemString(self::getAttributes($eleSheet, $xmlNamespaceBase), 'id');
745✔
837
                            if (isset($worksheets[$sheetReferenceId]) === false) {
745✔
838
                                ++$countSkippedSheets;
1✔
839
                                $mapSheetId[$oldSheetId] = null;
1✔
840

841
                                continue;
1✔
842
                            }
843
                            // Map old sheet id in original workbook to new sheet id.
844
                            // They will differ if loadSheetsOnly() is being used
845
                            $mapSheetId[$oldSheetId] = $oldSheetId - $countSkippedSheets;
745✔
846

847
                            // Load sheet
848
                            $docSheet = $excel->createSheet();
745✔
849
                            $sheetCreated = true;
745✔
850
                            //    Use false for $updateFormulaCellReferences to prevent adjustment of worksheet
851
                            //        references in formula cells... during the load, all formulae should be correct,
852
                            //        and we're simply bringing the worksheet name in line with the formula, not the
853
                            //        reverse
854
                            $docSheet->setTitle((string) $eleSheetAttr['name'], false, false);
745✔
855

856
                            $fileWorksheet = (string) $worksheets[$sheetReferenceId];
745✔
857
                            // issue 3665 adds test for /.
858
                            // This broke XlsxRootZipFilesTest,
859
                            //  but Excel reports an error with that file.
860
                            //  Testing dir for . avoids this problem.
861
                            //  It might be better just to drop the test.
862
                            if ($fileWorksheet[0] == '/' && $dir !== '.') {
745✔
863
                                $fileWorksheet = substr($fileWorksheet, strlen($dir) + 2);
12✔
864
                            }
865
                            $xmlSheet = $this->loadZipNoNamespace("$dir/$fileWorksheet", $mainNS);
745✔
866
                            $xmlSheetNS = $this->loadZip("$dir/$fileWorksheet", $mainNS);
745✔
867

868
                            // Shared Formula table is unique to each Worksheet, so we need to reset it here
869
                            $this->sharedFormulae = [];
745✔
870

871
                            if (isset($eleSheetAttr['state']) && (string) $eleSheetAttr['state'] != '') {
745✔
872
                                $docSheet->setSheetState((string) $eleSheetAttr['state']);
54✔
873
                            }
874
                            if ($xmlSheetNS) {
745✔
875
                                $xmlSheetMain = $xmlSheetNS->children($mainNS);
745✔
876
                                // Setting Conditional Styles adjusts selected cells, so we need to execute this
877
                                //    before reading the sheet view data to get the actual selected cells
878
                                if (!$this->readDataOnly && ($xmlSheet->conditionalFormatting)) {
745✔
879
                                    (new ConditionalStyles($docSheet, $xmlSheet, $dxfs, $this->styleReader))->load();
224✔
880
                                }
881
                                if (!$this->readDataOnly && $xmlSheet->extLst) {
745✔
882
                                    (new ConditionalStyles($docSheet, $xmlSheet, $dxfs, $this->styleReader))->loadFromExt();
202✔
883
                                }
884
                                if (isset($xmlSheetMain->sheetViews, $xmlSheetMain->sheetViews->sheetView)) {
745✔
885
                                    $sheetViews = new SheetViews($xmlSheetMain->sheetViews->sheetView, $docSheet);
742✔
886
                                    $sheetViews->load();
742✔
887
                                }
888

889
                                $sheetViewOptions = new SheetViewOptions($docSheet, $xmlSheetNS);
745✔
890
                                $sheetViewOptions->load($this->readDataOnly, $this->styleReader);
745✔
891

892
                                (new ColumnAndRowAttributes($docSheet, $xmlSheetNS))
745✔
893
                                    ->load($this->getReadFilter(), $this->readDataOnly, $this->ignoreRowsWithNoCells);
745✔
894
                            }
895

896
                            $holdSelectedCells = $docSheet->getSelectedCells();
745✔
897
                            if ($xmlSheetNS && $xmlSheetNS->sheetData && $xmlSheetNS->sheetData->row) {
745✔
898
                                $cIndex = 1; // Cell Start from 1
695✔
899
                                foreach ($xmlSheetNS->sheetData->row as $row) {
695✔
900
                                    $rowIndex = 1;
695✔
901
                                    foreach ($row->c as $c) {
695✔
902
                                        $cAttr = self::getAttributes($c);
694✔
903
                                        $r = (string) $cAttr['r'];
694✔
904
                                        if ($r == '') {
694✔
905
                                            $r = Coordinate::stringFromColumnIndex($rowIndex) . $cIndex;
2✔
906
                                        }
907
                                        $cellDataType = (string) $cAttr['t'];
694✔
908
                                        $originalCellDataTypeNumeric = $cellDataType === '';
694✔
909
                                        $value = null;
694✔
910
                                        $calculatedValue = null;
694✔
911

912
                                        // Read cell?
913
                                        $coordinates = Coordinate::coordinateFromString($r);
694✔
914

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

925
                                            continue;
4✔
926
                                        }
927

928
                                        // Read cell!
929
                                        $useFormula = isset($c->f)
694✔
930
                                            && ((string) $c->f !== '' || (isset($c->f->attributes()['t']) && strtolower((string) $c->f->attributes()['t']) === 'shared'));
694✔
931
                                        switch ($cellDataType) {
932
                                            case DataType::TYPE_STRING:
20✔
933
                                                if ((string) $c->v != '') {
538✔
934
                                                    $value = $sharedStrings[(int) ($c->v)];
538✔
935

936
                                                    if ($value instanceof RichText) {
538✔
937
                                                        $value = clone $value;
37✔
938
                                                    }
939
                                                } else {
940
                                                    $value = '';
17✔
941
                                                }
942

943
                                                break;
538✔
944
                                            case DataType::TYPE_BOOL:
16✔
945
                                                if (!$useFormula) {
24✔
946
                                                    if (isset($c->v)) {
18✔
947
                                                        $value = self::castToBoolean($c);
18✔
948
                                                    } else {
949
                                                        $value = null;
1✔
950
                                                        $cellDataType = DataType::TYPE_NULL;
1✔
951
                                                    }
952
                                                } else {
953
                                                    // Formula
954
                                                    $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToBoolean');
6✔
955
                                                    self::storeFormulaAttributes($c->f, $docSheet, $r);
6✔
956
                                                }
957

958
                                                break;
24✔
959
                                            case DataType::TYPE_STRING2:
16✔
960
                                                if ($useFormula) {
227✔
961
                                                    $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToString');
225✔
962
                                                    self::storeFormulaAttributes($c->f, $docSheet, $r);
225✔
963
                                                } else {
964
                                                    $value = self::castToString($c);
3✔
965
                                                }
966

967
                                                break;
227✔
968
                                            case DataType::TYPE_INLINE:
16✔
969
                                                if ($useFormula) {
18✔
970
                                                    $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToError');
×
971
                                                    self::storeFormulaAttributes($c->f, $docSheet, $r);
×
972
                                                } else {
973
                                                    $value = $this->parseRichText($c->is);
18✔
974
                                                }
975

976
                                                break;
18✔
977
                                            case DataType::TYPE_ERROR:
16✔
978
                                                if (isset($cAttr->vm, $richData['image']['rId' . $cAttr->vm]) && !$useFormula) {
195✔
979
                                                    $imagePath = $dir . '/' . str_replace('../', '', $richData['image']['rId' . $cAttr->vm]);
6✔
980
                                                    $objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing();
6✔
981
                                                    $objDrawing->setPath(
6✔
982
                                                        'zip://' . File::realpath($filename) . '#' . $imagePath,
6✔
983
                                                        false,
6✔
984
                                                        $zip
6✔
985
                                                    );
6✔
986

987
                                                    $objDrawing->setCoordinates($r);
6✔
988
                                                    $objDrawing->setResizeProportional(false);
6✔
989
                                                    $objDrawing->setInCell(true);
6✔
990
                                                    $objDrawing->setWorksheet($docSheet);
6✔
991

992
                                                    $value = $objDrawing;
6✔
993
                                                    $cellDataType = DataType::TYPE_DRAWING_IN_CELL;
6✔
994
                                                    $c->t = DataType::TYPE_ERROR;
6✔
995

996
                                                    break;
6✔
997
                                                }
998

999
                                                if (!$useFormula) {
189✔
1000
                                                    $value = self::castToError($c);
×
1001
                                                } else {
1002
                                                    // Formula
1003
                                                    $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToError');
189✔
1004
                                                    $eattr = $c->attributes();
189✔
1005
                                                    if (isset($eattr['vm'])) {
189✔
1006
                                                        if ($calculatedValue === ExcelError::VALUE()) {
1✔
1007
                                                            $calculatedValue = ExcelError::SPILL();
1✔
1008
                                                        }
1009
                                                    }
1010
                                                }
1011

1012
                                                break;
189✔
1013
                                            default:
1014
                                                if (!$useFormula) {
554✔
1015
                                                    $value = self::castToString($c);
544✔
1016
                                                    if (is_numeric($value)) {
544✔
1017
                                                        $value += 0;
508✔
1018
                                                        $cellDataType = DataType::TYPE_NUMERIC;
508✔
1019
                                                    }
1020
                                                } else {
1021
                                                    // Formula
1022
                                                    $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToString');
350✔
1023
                                                    if (is_numeric($calculatedValue)) {
350✔
1024
                                                        $calculatedValue += 0;
347✔
1025
                                                    }
1026
                                                    self::storeFormulaAttributes($c->f, $docSheet, $r);
350✔
1027
                                                }
1028

1029
                                                break;
554✔
1030
                                        }
1031

1032
                                        // read empty cells or the cells are not empty
1033
                                        if ($this->readEmptyCells || ($value !== null && $value !== '')) {
694✔
1034
                                            // Rich text?
1035
                                            if ($value instanceof RichText && $this->readDataOnly) {
694✔
1036
                                                $value = $value->getPlainText();
1✔
1037
                                            }
1038

1039
                                            $cell = $docSheet->getCell($r);
694✔
1040
                                            // Assign value
1041
                                            if ($cellDataType != '') {
694✔
1042
                                                // it is possible, that datatype is numeric but with an empty string, which result in an error
1043
                                                if ($cellDataType === DataType::TYPE_NUMERIC && ($value === '' || $value === null)) {
682✔
1044
                                                    $cellDataType = DataType::TYPE_NULL;
1✔
1045
                                                }
1046
                                                if ($cellDataType !== DataType::TYPE_NULL) {
682✔
1047
                                                    $cell->setValueExplicit($value, $cellDataType);
682✔
1048
                                                }
1049
                                            } else {
1050
                                                $cell->setValue($value);
308✔
1051
                                            }
1052
                                            if ($calculatedValue !== null) {
694✔
1053
                                                $cell->setCalculatedValue($calculatedValue, $originalCellDataTypeNumeric);
366✔
1054
                                            }
1055

1056
                                            // Style information?
1057
                                            if (!$this->readDataOnly) {
694✔
1058
                                                $cAttrS = (int) ($cAttr['s'] ?? 0);
691✔
1059
                                                // no style index means 0, it seems
1060
                                                $cAttrS = isset($styles[$cAttrS]) ? $cAttrS : 0;
691✔
1061
                                                $cell->setXfIndex($cAttrS);
691✔
1062
                                                // issue 3495
1063
                                                if ($cellDataType === DataType::TYPE_FORMULA && $styles[$cAttrS]->quotePrefix === true) { //* @phpstan-ignore-line
691✔
1064
                                                    $holdSelected = $docSheet->getSelectedCells();
2✔
1065
                                                    $cell->getStyle()->setQuotePrefix(false);
2✔
1066
                                                    $docSheet->setSelectedCells($holdSelected);
2✔
1067
                                                }
1068
                                            }
1069
                                        }
1070
                                        ++$rowIndex;
694✔
1071
                                    }
1072
                                    ++$cIndex;
695✔
1073
                                }
1074
                            }
1075
                            $docSheet->setSelectedCells($holdSelectedCells);
745✔
1076
                            if (!$this->readDataOnly && $xmlSheetNS && $xmlSheetNS->ignoredErrors) {
745✔
1077
                                foreach ($xmlSheetNS->ignoredErrors->ignoredError as $ignoredError) {
4✔
1078
                                    $this->processIgnoredErrors($ignoredError, $docSheet);
4✔
1079
                                }
1080
                            }
1081

1082
                            if (!$this->readDataOnly && $xmlSheetNS && $xmlSheetNS->sheetProtection) {
745✔
1083
                                $protAttr = $xmlSheetNS->sheetProtection->attributes() ?? [];
70✔
1084
                                foreach ($protAttr as $key => $value) {
70✔
1085
                                    $method = 'set' . ucfirst($key);
70✔
1086
                                    $docSheet->getProtection()->$method(self::boolean((string) $value));
70✔
1087
                                }
1088
                            }
1089

1090
                            if ($xmlSheet) {
745✔
1091
                                $this->readSheetProtection($docSheet, $xmlSheet);
735✔
1092
                            }
1093

1094
                            if ($this->readDataOnly === false) {
745✔
1095
                                $this->readAutoFilter($xmlSheetNS, $docSheet);
742✔
1096
                                $this->readBackgroundImage($xmlSheetNS, $docSheet, dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels');
742✔
1097
                            }
1098

1099
                            $this->readTables($xmlSheetNS, $docSheet, $dir, $fileWorksheet, $zip, $mainNS, $tableStyles, $dxfs);
745✔
1100

1101
                            if ($xmlSheetNS && $xmlSheetNS->mergeCells && $xmlSheetNS->mergeCells->mergeCell && !$this->readDataOnly) {
745✔
1102
                                foreach ($xmlSheetNS->mergeCells->mergeCell as $mergeCellx) {
70✔
1103
                                    $mergeCell = $mergeCellx->attributes();
70✔
1104
                                    $mergeRef = (string) ($mergeCell['ref'] ?? '');
70✔
1105
                                    if (str_contains($mergeRef, ':')) {
70✔
1106
                                        $docSheet->mergeCells($mergeRef, Worksheet::MERGE_CELL_CONTENT_HIDE);
70✔
1107
                                    }
1108
                                }
1109
                            }
1110

1111
                            if ($xmlSheet && !$this->readDataOnly) {
745✔
1112
                                $unparsedLoadedData = (new PageSetup($docSheet, $xmlSheet))->load($unparsedLoadedData);
732✔
1113
                            }
1114

1115
                            if (isset($xmlSheet->extLst->ext)) {
745✔
1116
                                foreach ($xmlSheet->extLst->ext as $extlst) {
202✔
1117
                                    $extAttrs = $extlst->attributes() ?? [];
202✔
1118
                                    $extUri = (string) ($extAttrs['uri'] ?? '');
202✔
1119
                                    if ($extUri !== '{CCE6A557-97BC-4b89-ADB6-D9C93CAAB3DF}') {
202✔
1120
                                        continue;
196✔
1121
                                    }
1122
                                    // Create dataValidations node if does not exists, maybe is better inside the foreach ?
1123
                                    if (!$xmlSheet->dataValidations) {
6✔
1124
                                        $xmlSheet->addChild('dataValidations');
1✔
1125
                                    }
1126

1127
                                    foreach ($extlst->children(Namespaces::DATA_VALIDATIONS1)->dataValidations->dataValidation as $item) {
6✔
1128
                                        $item = self::testSimpleXml($item);
6✔
1129
                                        $node = self::testSimpleXml($xmlSheet->dataValidations)->addChild('dataValidation');
6✔
1130
                                        foreach ($item->attributes() ?? [] as $attr) {
6✔
1131
                                            $node->addAttribute($attr->getName(), $attr);
6✔
1132
                                        }
1133
                                        $node->addAttribute('sqref', $item->children(Namespaces::DATA_VALIDATIONS2)->sqref);
6✔
1134
                                        if (isset($item->formula1)) {
6✔
1135
                                            $childNode = $node->addChild('formula1');
6✔
1136
                                            if ($childNode !== null) { // null should never happen
6✔
1137
                                                // see https://github.com/phpstan/phpstan/issues/8236
1138
                                                // resolved with Phpstan 2.1.23
1139
                                                $childNode[0] = (string) $item->formula1->children(Namespaces::DATA_VALIDATIONS2)->f;
6✔
1140
                                            }
1141
                                        }
1142
                                    }
1143
                                }
1144
                            }
1145

1146
                            if ($xmlSheet && $xmlSheet->dataValidations && !$this->readDataOnly) {
745✔
1147
                                (new DataValidations($docSheet, $xmlSheet))->load();
24✔
1148
                            }
1149

1150
                            // unparsed sheet AlternateContent
1151
                            if ($xmlSheet && !$this->readDataOnly) {
745✔
1152
                                $mc = $xmlSheet->children(Namespaces::COMPATIBILITY);
732✔
1153
                                if ($mc->AlternateContent) {
732✔
1154
                                    foreach ($mc->AlternateContent as $alternateContent) {
4✔
1155
                                        $alternateContent = self::testSimpleXml($alternateContent);
4✔
1156
                                        /** @var mixed[][][][] $unparsedLoadedData */
1157
                                        $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['AlternateContents'][] = $alternateContent->asXML();
4✔
1158
                                    }
1159
                                }
1160
                            }
1161

1162
                            // Add hyperlinks
1163
                            if (!$this->readDataOnly) {
745✔
1164
                                $hyperlinkReader = new Hyperlinks($docSheet);
742✔
1165
                                // Locate hyperlink relations
1166
                                $relationsFileName = dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels';
742✔
1167
                                if ($zip->locateName($relationsFileName) !== false) {
742✔
1168
                                    $relsWorksheet = $this->loadZip($relationsFileName, Namespaces::RELATIONSHIPS);
613✔
1169
                                    $hyperlinkReader->readHyperlinks($relsWorksheet);
613✔
1170
                                }
1171

1172
                                // Loop through hyperlinks
1173
                                if ($xmlSheetNS && $xmlSheetNS->children($mainNS)->hyperlinks) {
742✔
1174
                                    $hyperlinkReader->setHyperlinks($xmlSheetNS->children($mainNS)->hyperlinks);
20✔
1175
                                }
1176
                            }
1177

1178
                            // Add comments
1179
                            $comments = [];
745✔
1180
                            $vmlComments = [];
745✔
1181
                            if (!$this->readDataOnly) {
745✔
1182
                                // Locate comment relations
1183
                                $commentRelations = dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels';
742✔
1184
                                if ($zip->locateName($commentRelations) !== false) {
742✔
1185
                                    $relsWorksheet = $this->loadZip($commentRelations, Namespaces::RELATIONSHIPS);
613✔
1186
                                    foreach ($relsWorksheet->Relationship as $elex) {
613✔
1187
                                        $ele = self::getAttributes($elex);
429✔
1188
                                        if ($ele['Type'] == Namespaces::COMMENTS) {
429✔
1189
                                            $comments[(string) $ele['Id']] = (string) $ele['Target'];
43✔
1190
                                        }
1191
                                        if ($ele['Type'] == Namespaces::VML) {
429✔
1192
                                            $vmlComments[(string) $ele['Id']] = (string) $ele['Target'];
47✔
1193
                                        }
1194
                                    }
1195
                                }
1196

1197
                                // Loop through comments
1198
                                foreach ($comments as $relName => $relPath) {
742✔
1199
                                    // Load comments file
1200
                                    $relPath = File::realpath(dirname("$dir/$fileWorksheet") . '/' . $relPath);
43✔
1201
                                    // okay to ignore namespace - using xpath
1202
                                    $commentsFile = $this->loadZip($relPath, '');
43✔
1203

1204
                                    // Utility variables
1205
                                    $authors = [];
43✔
1206
                                    $commentsFile->registerXpathNamespace('com', $mainNS);
43✔
1207
                                    $authorPath = self::xpathNoFalse($commentsFile, 'com:authors/com:author');
43✔
1208
                                    foreach ($authorPath as $author) {
43✔
1209
                                        /** @var SimpleXMLElement $author */
1210
                                        $authors[] = (string) $author;
43✔
1211
                                    }
1212

1213
                                    // Loop through contents
1214
                                    $contentPath = self::xpathNoFalse($commentsFile, 'com:commentList/com:comment');
43✔
1215
                                    foreach ($contentPath as $comment) {
43✔
1216
                                        /** @var SimpleXMLElement $comment */
1217
                                        $commentx = $comment->attributes();
43✔
1218
                                        /** @var array{ref: scalar, authorId?: scalar}  $commentx */
1219
                                        $commentModel = $docSheet->getComment((string) $commentx['ref']);
43✔
1220
                                        if (isset($commentx['authorId'])) {
43✔
1221
                                            $commentModel->setAuthor($authors[(int) $commentx['authorId']]);
43✔
1222
                                        }
1223
                                        /** @var SimpleXMLElement */
1224
                                        $temp = $comment->children($mainNS);
43✔
1225
                                        $commentModel->setText($this->parseRichText($temp->text));
43✔
1226
                                    }
1227
                                }
1228

1229
                                // later we will remove from it real vmlComments
1230
                                $unparsedVmlDrawings = $vmlComments;
742✔
1231
                                $vmlDrawingContents = [];
742✔
1232

1233
                                // Loop through VML comments
1234
                                foreach ($vmlComments as $relName => $relPath) {
742✔
1235
                                    // Load VML comments file
1236
                                    $relPath = File::realpath(dirname("$dir/$fileWorksheet") . '/' . $relPath);
47✔
1237

1238
                                    try {
1239
                                        // no namespace okay - processed with Xpath
1240
                                        $vmlCommentsFile = $this->loadZip($relPath, '', true);
47✔
1241
                                        $vmlCommentsFile->registerXPathNamespace('v', Namespaces::URN_VML);
47✔
1242
                                    } catch (Throwable) {
×
1243
                                        //Ignore unparsable vmlDrawings. Later they will be moved from $unparsedVmlDrawings to $unparsedLoadedData
1244
                                        continue;
×
1245
                                    }
1246

1247
                                    // Locate VML drawings image relations
1248
                                    $drowingImages = [];
47✔
1249
                                    $VMLDrawingsRelations = dirname($relPath) . '/_rels/' . basename($relPath) . '.rels';
47✔
1250
                                    $vmlDrawingContents[$relName] = $this->getSecurityScannerOrThrow()->scan($this->getFromZipArchive($zip, $relPath));
47✔
1251
                                    if ($zip->locateName($VMLDrawingsRelations) !== false) {
47✔
1252
                                        $relsVMLDrawing = $this->loadZip($VMLDrawingsRelations, Namespaces::RELATIONSHIPS);
19✔
1253
                                        foreach ($relsVMLDrawing->Relationship as $elex) {
19✔
1254
                                            $ele = self::getAttributes($elex);
9✔
1255
                                            if ($ele['Type'] == Namespaces::IMAGE) {
9✔
1256
                                                $drowingImages[(string) $ele['Id']] = (string) $ele['Target'];
9✔
1257
                                            }
1258
                                        }
1259
                                    }
1260

1261
                                    $shapes = self::xpathNoFalse($vmlCommentsFile, '//v:shape');
47✔
1262
                                    foreach ($shapes as $shape) {
47✔
1263
                                        /** @var SimpleXMLElement $shape */
1264
                                        $vmlNamespaces = $shape->getNamespaces();
46✔
1265
                                        $shape->registerXPathNamespace('v', $vmlNamespaces['v'] ?? Namespaces::URN_VML);
46✔
1266
                                        $shape->registerXPathNamespace('x', $vmlNamespaces['x'] ?? Namespaces::URN_EXCEL);
46✔
1267
                                        $shape->registerXPathNamespace('o', $vmlNamespaces['o'] ?? Namespaces::URN_MSOFFICE);
46✔
1268

1269
                                        if (isset($shape['style'])) {
46✔
1270
                                            $style = (string) $shape['style'];
46✔
1271
                                            $fillColor = strtoupper(substr((string) $shape['fillcolor'], 1));
46✔
1272
                                            $column = null;
46✔
1273
                                            $row = null;
46✔
1274
                                            $textHAlign = null;
46✔
1275
                                            $fillImageRelId = null;
46✔
1276
                                            $fillImageTitle = '';
46✔
1277

1278
                                            $clientData = $shape->xpath('.//x:ClientData');
46✔
1279
                                            $textboxDirection = '';
46✔
1280
                                            $textboxPath = $shape->xpath('.//v:textbox');
46✔
1281
                                            $textbox = (string) ($textboxPath[0]['style'] ?? '');
46✔
1282
                                            if (Preg::isMatch('/rtl/i', $textbox)) {
46✔
1283
                                                $textboxDirection = Comment::TEXTBOX_DIRECTION_RTL;
1✔
1284
                                            } elseif (Preg::isMatch('/ltr/i', $textbox)) {
45✔
1285
                                                $textboxDirection = Comment::TEXTBOX_DIRECTION_LTR;
1✔
1286
                                            }
1287
                                            if (is_array($clientData) && !empty($clientData)) {
46✔
1288
                                                /** @var SimpleXMLElement */
1289
                                                $clientData = $clientData[0];
44✔
1290

1291
                                                if (isset($clientData['ObjectType']) && (string) $clientData['ObjectType'] == 'Note') {
44✔
1292
                                                    $clientData->registerXPathNamespace('x', $vmlNamespaces['x'] ?? Namespaces::URN_EXCEL);
42✔
1293
                                                    $temp = $clientData->xpath('.//x:Row');
42✔
1294
                                                    if (is_array($temp)) {
42✔
1295
                                                        $row = $temp[0];
42✔
1296
                                                    }
1297

1298
                                                    $temp = $clientData->xpath('.//x:Column');
42✔
1299
                                                    if (is_array($temp)) {
42✔
1300
                                                        $column = $temp[0];
42✔
1301
                                                    }
1302
                                                    $temp = $clientData->xpath('.//x:TextHAlign');
42✔
1303
                                                    if (!empty($temp)) {
42✔
1304
                                                        $textHAlign = strtolower($temp[0]);
12✔
1305
                                                    }
1306
                                                }
1307
                                            }
1308
                                            $rowx = (string) $row;
46✔
1309
                                            $colx = (string) $column;
46✔
1310
                                            if (is_numeric($rowx) && is_numeric($colx) && $textHAlign !== null) {
46✔
1311
                                                $docSheet->getComment([1 + (int) $colx, 1 + (int) $rowx], false)->setAlignment((string) $textHAlign);
12✔
1312
                                            }
1313
                                            if (is_numeric($rowx) && is_numeric($colx) && $textboxDirection !== '') {
46✔
1314
                                                $docSheet->getComment([1 + (int) $colx, 1 + (int) $rowx], false)->setTextboxDirection($textboxDirection);
2✔
1315
                                            }
1316

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

1322
                                                if (isset($fillImageRelNode['relid'])) {
5✔
1323
                                                    $fillImageRelId = (string) $fillImageRelNode['relid'];
5✔
1324
                                                }
1325
                                            }
1326

1327
                                            $fillImageTitleNode = $shape->xpath('.//v:fill/@o:title');
46✔
1328
                                            if (is_array($fillImageTitleNode) && !empty($fillImageTitleNode)) {
46✔
1329
                                                /** @var SimpleXMLElement */
1330
                                                $fillImageTitleNode = $fillImageTitleNode[0];
3✔
1331

1332
                                                if (isset($fillImageTitleNode['title'])) {
3✔
1333
                                                    $fillImageTitle = (string) $fillImageTitleNode['title'];
3✔
1334
                                                }
1335
                                            }
1336

1337
                                            if (($column !== null) && ($row !== null)) {
46✔
1338
                                                // Set comment properties
1339
                                                $comment = $docSheet->getComment([(int) $column + 1, (int) $row + 1]);
42✔
1340
                                                $comment->getFillColor()->setRGB($fillColor);
42✔
1341
                                                if (isset($fillImageRelId, $drowingImages[$fillImageRelId])) {
42✔
1342
                                                    $objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing();
5✔
1343
                                                    $objDrawing->setName($fillImageTitle);
5✔
1344
                                                    $imagePath = str_replace(['../', '/xl/'], 'xl/', $drowingImages[$fillImageRelId]);
5✔
1345
                                                    $objDrawing->setPath(
5✔
1346
                                                        'zip://' . File::realpath($filename) . '#' . $imagePath,
5✔
1347
                                                        true,
5✔
1348
                                                        $zip
5✔
1349
                                                    );
5✔
1350
                                                    $comment->setBackgroundImage($objDrawing);
5✔
1351
                                                }
1352

1353
                                                // Parse style
1354
                                                $styleArray = explode(';', str_replace(' ', '', $style));
42✔
1355
                                                foreach ($styleArray as $stylePair) {
42✔
1356
                                                    $stylePair = explode(':', $stylePair);
42✔
1357

1358
                                                    if ($stylePair[0] == 'margin-left') {
42✔
1359
                                                        $comment->setMarginLeft($stylePair[1]);
38✔
1360
                                                    }
1361
                                                    if ($stylePair[0] == 'margin-top') {
42✔
1362
                                                        $comment->setMarginTop($stylePair[1]);
38✔
1363
                                                    }
1364
                                                    if ($stylePair[0] == 'width') {
42✔
1365
                                                        $comment->setWidth($stylePair[1]);
38✔
1366
                                                    }
1367
                                                    if ($stylePair[0] == 'height') {
42✔
1368
                                                        $comment->setHeight($stylePair[1]);
38✔
1369
                                                    }
1370
                                                    if ($stylePair[0] == 'visibility') {
42✔
1371
                                                        $comment->setVisible($stylePair[1] == 'visible');
35✔
1372
                                                    }
1373
                                                }
1374

1375
                                                unset($unparsedVmlDrawings[$relName]);
42✔
1376
                                            }
1377
                                        }
1378
                                    }
1379
                                }
1380

1381
                                // unparsed vmlDrawing
1382
                                if ($unparsedVmlDrawings) {
742✔
1383
                                    foreach ($unparsedVmlDrawings as $rId => $relPath) {
7✔
1384
                                        /** @var mixed[][][] $unparsedLoadedData */
1385
                                        $rId = substr($rId, 3); // rIdXXX
7✔
1386
                                        /** @var mixed[][] */
1387
                                        $unparsedVmlDrawing = &$unparsedLoadedData['sheets'][$docSheet->getCodeName()]['vmlDrawings'];
7✔
1388
                                        $unparsedVmlDrawing[$rId] = [];
7✔
1389
                                        $unparsedVmlDrawing[$rId]['filePath'] = self::dirAdd("$dir/$fileWorksheet", $relPath);
7✔
1390
                                        $unparsedVmlDrawing[$rId]['relFilePath'] = $relPath;
7✔
1391
                                        $unparsedVmlDrawing[$rId]['content'] = $this->getSecurityScannerOrThrow()->scan($this->getFromZipArchive($zip, $unparsedVmlDrawing[$rId]['filePath']));
7✔
1392
                                        unset($unparsedVmlDrawing);
7✔
1393
                                    }
1394
                                }
1395

1396
                                // Header/footer images
1397
                                if ($xmlSheetNS && $xmlSheetNS->legacyDrawingHF) {
742✔
1398
                                    $vmlHfRid = '';
3✔
1399
                                    $vmlHfRidAttr = $xmlSheetNS->legacyDrawingHF->attributes(Namespaces::SCHEMA_OFFICE_DOCUMENT);
3✔
1400
                                    if ($vmlHfRidAttr !== null && isset($vmlHfRidAttr['id'])) {
3✔
1401
                                        $vmlHfRid = (string) $vmlHfRidAttr['id'][0];
3✔
1402
                                    }
1403
                                    if ($zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels') !== false) {
3✔
1404
                                        $relsWorksheet = $this->loadZipNoNamespace(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels', Namespaces::RELATIONSHIPS);
3✔
1405
                                        $vmlRelationship = '';
3✔
1406

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

1411
                                                break;
3✔
1412
                                            }
1413
                                        }
1414

1415
                                        if ($vmlRelationship != '') {
3✔
1416
                                            // Fetch linked images
1417
                                            $relsVML = $this->loadZipNoNamespace(dirname($vmlRelationship) . '/_rels/' . basename($vmlRelationship) . '.rels', Namespaces::RELATIONSHIPS);
3✔
1418
                                            $drawings = [];
3✔
1419
                                            if (isset($relsVML->Relationship)) {
3✔
1420
                                                foreach ($relsVML->Relationship as $ele) {
3✔
1421
                                                    if ($ele['Type'] == Namespaces::IMAGE) {
3✔
1422
                                                        $drawings[(string) $ele['Id']] = self::dirAdd($vmlRelationship, $ele['Target']);
3✔
1423
                                                    }
1424
                                                }
1425
                                            }
1426
                                            // Fetch VML document
1427
                                            $vmlDrawing = $this->loadZipNoNamespace($vmlRelationship, '');
3✔
1428
                                            $vmlDrawing->registerXPathNamespace('v', Namespaces::URN_VML);
3✔
1429

1430
                                            $hfImages = [];
3✔
1431

1432
                                            $shapes = self::xpathNoFalse($vmlDrawing, '//v:shape');
3✔
1433
                                            foreach ($shapes as $idx => $shape) {
3✔
1434
                                                /** @var SimpleXMLElement $shape */
1435
                                                $shape->registerXPathNamespace('v', Namespaces::URN_VML);
3✔
1436
                                                $imageData = $shape->xpath('//v:imagedata');
3✔
1437

1438
                                                if (empty($imageData)) {
3✔
1439
                                                    continue;
×
1440
                                                }
1441

1442
                                                $imageData = $imageData[$idx];
3✔
1443

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

1448
                                                if (array_key_exists((string) $imageData['relid'], $drawings)) {
3✔
1449
                                                    $shapeId = (string) $shape['id'];
3✔
1450
                                                    $hfImages[$shapeId] = new HeaderFooterDrawing();
3✔
1451
                                                    if (isset($imageData['title'])) {
3✔
1452
                                                        $hfImages[$shapeId]->setName((string) $imageData['title']);
3✔
1453
                                                    }
1454

1455
                                                    $hfImages[$shapeId]->setPath('zip://' . File::realpath($filename) . '#' . $drawings[(string) $imageData['relid']], false, $zip);
3✔
1456
                                                    $hfImages[$shapeId]->setResizeProportional(false);
3✔
1457
                                                    $hfImages[$shapeId]->setWidth($style['width']);
3✔
1458
                                                    $hfImages[$shapeId]->setHeight($style['height']);
3✔
1459
                                                    if (isset($style['margin-left'])) {
3✔
1460
                                                        $hfImages[$shapeId]->setOffsetX($style['margin-left']);
3✔
1461
                                                    }
1462
                                                    $hfImages[$shapeId]->setOffsetY($style['margin-top']);
3✔
1463
                                                    $hfImages[$shapeId]->setResizeProportional(true);
3✔
1464
                                                }
1465
                                            }
1466

1467
                                            $docSheet->getHeaderFooter()->setImages($hfImages);
3✔
1468
                                        }
1469
                                    }
1470
                                }
1471
                            }
1472

1473
                            // TODO: Autoshapes from twoCellAnchors!
1474
                            $drawingFilename = dirname("$dir/$fileWorksheet")
745✔
1475
                                . '/_rels/'
745✔
1476
                                . basename($fileWorksheet)
745✔
1477
                                . '.rels';
745✔
1478
                            if (str_starts_with($drawingFilename, 'xl//xl/')) {
745✔
1479
                                $drawingFilename = substr($drawingFilename, 4);
×
1480
                            }
1481
                            if (str_starts_with($drawingFilename, '/xl//xl/')) {
745✔
1482
                                $drawingFilename = substr($drawingFilename, 5);
×
1483
                            }
1484
                            if ($zip->locateName($drawingFilename) !== false) {
745✔
1485
                                $relsWorksheet = $this->loadZip($drawingFilename, Namespaces::RELATIONSHIPS);
616✔
1486
                                $drawings = [];
616✔
1487
                                foreach ($relsWorksheet->Relationship as $elex) {
616✔
1488
                                    $ele = self::getAttributes($elex);
431✔
1489
                                    if ((string) $ele['Type'] === "$xmlNamespaceBase/drawing") {
431✔
1490
                                        $eleTarget = (string) $ele['Target'];
154✔
1491
                                        if (str_starts_with($eleTarget, '/xl/')) {
154✔
1492
                                            $drawings[(string) $ele['Id']] = substr($eleTarget, 1);
4✔
1493
                                        } else {
1494
                                            $drawings[(string) $ele['Id']] = self::dirAdd("$dir/$fileWorksheet", $ele['Target']);
151✔
1495
                                        }
1496
                                    }
1497
                                }
1498

1499
                                if ($xmlSheetNS->drawing && !$this->readDataOnly) {
616✔
1500
                                    $unparsedDrawings = [];
153✔
1501
                                    $fileDrawing = null;
153✔
1502
                                    foreach ($xmlSheetNS->drawing as $drawing) {
153✔
1503
                                        $drawingRelId = self::getArrayItemString(self::getAttributes($drawing, $xmlNamespaceBase), 'id');
153✔
1504
                                        $fileDrawing = $drawings[$drawingRelId];
153✔
1505
                                        $drawingFilename = dirname($fileDrawing) . '/_rels/' . basename($fileDrawing) . '.rels';
153✔
1506
                                        $relsDrawing = $this->loadZip($drawingFilename, Namespaces::RELATIONSHIPS);
153✔
1507

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

1542
                                        $xmlDrawing = $this->loadZipNoNamespace($fileDrawing, '');
153✔
1543
                                        $xmlDrawingChildren = $xmlDrawing->children(Namespaces::SPREADSHEET_DRAWING);
153✔
1544

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

1568
                                        if ($xmlDrawingChildren->oneCellAnchor) {
153✔
1569
                                            foreach ($xmlDrawingChildren->oneCellAnchor as $oneCellAnchor) {
25✔
1570
                                                $oneCellAnchor = self::testSimpleXml($oneCellAnchor);
25✔
1571
                                                if ($oneCellAnchor->pic->blipFill) {
25✔
1572
                                                    $objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing();
17✔
1573
                                                    $blip = $oneCellAnchor->pic->blipFill->children(Namespaces::DRAWINGML)->blip;
17✔
1574
                                                    if (isset($blip, $blip->alphaModFix)) {
17✔
1575
                                                        $temp = (string) $blip->alphaModFix->attributes()->amt;
1✔
1576
                                                        if (is_numeric($temp)) {
1✔
1577
                                                            $objDrawing->setOpacity((int) $temp);
1✔
1578
                                                        }
1579
                                                    }
1580
                                                    $xfrm = $oneCellAnchor->pic->spPr->children(Namespaces::DRAWINGML)->xfrm;
17✔
1581
                                                    $outerShdw = $oneCellAnchor->pic->spPr->children(Namespaces::DRAWINGML)->effectLst->outerShdw;
17✔
1582

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

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

1639
                                                    $this->readHyperLinkDrawing($objDrawing, $oneCellAnchor, $hyperlinks);
17✔
1640

1641
                                                    $objDrawing->setWorksheet($docSheet);
17✔
1642
                                                } elseif ($this->includeCharts && $oneCellAnchor->graphicFrame) {
8✔
1643
                                                    // Exported XLSX from Google Sheets positions charts with a oneCellAnchor
1644
                                                    $coordinates = Coordinate::stringFromColumnIndex(((int) $oneCellAnchor->from->col) + 1) . ($oneCellAnchor->from->row + 1);
4✔
1645
                                                    $offsetX = Drawing::EMUToPixels($oneCellAnchor->from->colOff);
4✔
1646
                                                    $offsetY = Drawing::EMUToPixels($oneCellAnchor->from->rowOff);
4✔
1647
                                                    $width = Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($oneCellAnchor->ext), 'cx'));
4✔
1648
                                                    $height = Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($oneCellAnchor->ext), 'cy'));
4✔
1649

1650
                                                    $graphic = $oneCellAnchor->graphicFrame->children(Namespaces::DRAWINGML)->graphic;
4✔
1651
                                                    $chartRef = $graphic->graphicData->children(Namespaces::CHART)->chart;
4✔
1652
                                                    $thisChart = (string) self::getAttributes($chartRef, $xmlNamespaceBase);
4✔
1653

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

1715
                                                    $objDrawing->setOffsetX(Drawing::EMUToPixels($twoCellAnchor->from->colOff));
57✔
1716
                                                    $objDrawing->setOffsetY(Drawing::EMUToPixels($twoCellAnchor->from->rowOff));
57✔
1717

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

1720
                                                    $objDrawing->setOffsetX2(Drawing::EMUToPixels($twoCellAnchor->to->colOff));
57✔
1721
                                                    $objDrawing->setOffsetY2(Drawing::EMUToPixels($twoCellAnchor->to->rowOff));
57✔
1722

1723
                                                    $objDrawing->setResizeProportional(false);
57✔
1724

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

1750
                                                    $this->readHyperLinkDrawing($objDrawing, $twoCellAnchor, $hyperlinks);
57✔
1751

1752
                                                    $objDrawing->setWorksheet($docSheet);
57✔
1753
                                                } elseif (($this->includeCharts) && ($twoCellAnchor->graphicFrame)) {
85✔
1754
                                                    $fromCoordinate = Coordinate::stringFromColumnIndex(((int) $twoCellAnchor->from->col) + 1) . ($twoCellAnchor->from->row + 1);
66✔
1755
                                                    $fromOffsetX = Drawing::EMUToPixels($twoCellAnchor->from->colOff);
66✔
1756
                                                    $fromOffsetY = Drawing::EMUToPixels($twoCellAnchor->from->rowOff);
66✔
1757
                                                    $toCoordinate = Coordinate::stringFromColumnIndex(((int) $twoCellAnchor->to->col) + 1) . ($twoCellAnchor->to->row + 1);
66✔
1758
                                                    $toOffsetX = Drawing::EMUToPixels($twoCellAnchor->to->colOff);
66✔
1759
                                                    $toOffsetY = Drawing::EMUToPixels($twoCellAnchor->to->rowOff);
66✔
1760
                                                    $graphic = $twoCellAnchor->graphicFrame->children(Namespaces::DRAWINGML)->graphic;
66✔
1761
                                                    $chartRef = $graphic->graphicData->children(Namespaces::CHART)->chart;
66✔
1762
                                                    $thisChart = (string) self::getAttributes($chartRef, $xmlNamespaceBase);
66✔
1763

1764
                                                    $chartDetails[$docSheet->getTitle() . '!' . $thisChart] = [
66✔
1765
                                                        'fromCoordinate' => $fromCoordinate,
66✔
1766
                                                        'fromOffsetX' => $fromOffsetX,
66✔
1767
                                                        'fromOffsetY' => $fromOffsetY,
66✔
1768
                                                        'toCoordinate' => $toCoordinate,
66✔
1769
                                                        'toOffsetX' => $toOffsetX,
66✔
1770
                                                        'toOffsetY' => $toOffsetY,
66✔
1771
                                                        'worksheetTitle' => $docSheet->getTitle(),
66✔
1772
                                                    ];
66✔
1773
                                                }
1774
                                            }
1775
                                        }
1776
                                        if ($xmlDrawingChildren->absoluteAnchor) {
152✔
1777
                                            foreach ($xmlDrawingChildren->absoluteAnchor as $absoluteAnchor) {
1✔
1778
                                                if (($this->includeCharts) && ($absoluteAnchor->graphicFrame)) {
1✔
1779
                                                    $graphic = $absoluteAnchor->graphicFrame->children(Namespaces::DRAWINGML)->graphic;
1✔
1780
                                                    $chartRef = $graphic->graphicData->children(Namespaces::CHART)->chart;
1✔
1781
                                                    $thisChart = (string) self::getAttributes($chartRef, $xmlNamespaceBase);
1✔
1782
                                                    $width = Drawing::EMUToPixels((int) self::getArrayItemString(self::getAttributes($absoluteAnchor->ext), 'cx')[0]);
1✔
1783
                                                    $height = Drawing::EMUToPixels((int) self::getArrayItemString(self::getAttributes($absoluteAnchor->ext), 'cy')[0]);
1✔
1784

1785
                                                    $chartDetails[$docSheet->getTitle() . '!' . $thisChart] = [
1✔
1786
                                                        'fromCoordinate' => 'A1',
1✔
1787
                                                        'fromOffsetX' => 0,
1✔
1788
                                                        'fromOffsetY' => 0,
1✔
1789
                                                        'width' => $width,
1✔
1790
                                                        'height' => $height,
1✔
1791
                                                        'worksheetTitle' => $docSheet->getTitle(),
1✔
1792
                                                    ];
1✔
1793
                                                }
1794
                                            }
1795
                                        }
1796
                                        if (empty($relsDrawing) && $xmlDrawing->count() == 0) {
152✔
1797
                                            // Save Drawing without rels and children as unparsed
1798
                                            $unparsedDrawings[$drawingRelId] = $xmlDrawing->asXML();
28✔
1799
                                        }
1800
                                    }
1801

1802
                                    // store original rId of drawing files
1803
                                    /** @var mixed[][][][] $unparsedLoadedData */
1804
                                    $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['drawingOriginalIds'] = [];
152✔
1805
                                    foreach ($relsWorksheet->Relationship as $elex) {
152✔
1806
                                        $ele = self::getAttributes($elex);
152✔
1807
                                        if ((string) $ele['Type'] === "$xmlNamespaceBase/drawing") {
152✔
1808
                                            $drawingRelId = (string) $ele['Id'];
152✔
1809
                                            $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['drawingOriginalIds'][(string) $ele['Target']] = $drawingRelId;
152✔
1810
                                            if (isset($unparsedDrawings[$drawingRelId])) {
152✔
1811
                                                $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['Drawings'][$drawingRelId] = $unparsedDrawings[$drawingRelId];
39✔
1812
                                            }
1813
                                        }
1814
                                    }
1815
                                    if ($xmlSheet->legacyDrawing && !$this->readDataOnly) {
152✔
1816
                                        foreach ($xmlSheet->legacyDrawing as $drawing) {
22✔
1817
                                            $drawingRelId = self::getArrayItemString(self::getAttributes($drawing, $xmlNamespaceBase), 'id');
22✔
1818
                                            if (isset($vmlDrawingContents[$drawingRelId])) {
22✔
1819
                                                if (self::onlyNoteVml($vmlDrawingContents[$drawingRelId]) === false) {
22✔
1820
                                                    $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['legacyDrawing'] = $vmlDrawingContents[$drawingRelId];
5✔
1821
                                                }
1822
                                            }
1823
                                        }
1824
                                    }
1825

1826
                                    // unparsed drawing AlternateContent
1827
                                    $xmlAltDrawing = $this->loadZip((string) $fileDrawing, Namespaces::COMPATIBILITY);
152✔
1828

1829
                                    if ($xmlAltDrawing->AlternateContent) {
152✔
1830
                                        foreach ($xmlAltDrawing->AlternateContent as $alternateContent) {
4✔
1831
                                            $alternateContent = self::testSimpleXml($alternateContent);
4✔
1832
                                            /** @var mixed[][][][][] $unparsedLoadedData */
1833
                                            $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['drawingAlternateContents'][] = $alternateContent->asXML();
4✔
1834
                                        }
1835
                                    }
1836
                                }
1837
                            }
1838

1839
                            /** @var mixed[][][][] $unparsedLoadedData */
1840
                            $this->readFormControlProperties($excel, $dir, $fileWorksheet, $docSheet, $unparsedLoadedData);
744✔
1841
                            $this->readPrinterSettings($excel, $dir, $fileWorksheet, $docSheet, $unparsedLoadedData);
744✔
1842

1843
                            // Loop through definedNames
1844
                            if ($xmlWorkbook->definedNames) {
744✔
1845
                                foreach ($xmlWorkbook->definedNames->definedName as $definedName) {
407✔
1846
                                    // Extract range
1847
                                    $extractedRange = (string) $definedName;
108✔
1848
                                    if (($spos = strpos($extractedRange, '!')) !== false) {
108✔
1849
                                        $extractedRange = substr($extractedRange, 0, $spos) . str_replace('$', '', substr($extractedRange, $spos));
89✔
1850
                                    } else {
1851
                                        $extractedRange = str_replace('$', '', $extractedRange);
34✔
1852
                                    }
1853

1854
                                    // Valid range?
1855
                                    if ($extractedRange == '') {
108✔
1856
                                        continue;
×
1857
                                    }
1858

1859
                                    // Some definedNames are only applicable if we are on the same sheet...
1860
                                    if ((string) $definedName['localSheetId'] != '' && (string) $definedName['localSheetId'] == $oldSheetId) {
108✔
1861
                                        // Switch on type
1862
                                        switch ((string) $definedName['name']) {
50✔
1863
                                            case '_xlnm._FilterDatabase':
50✔
1864
                                                if ((string) $definedName['hidden'] !== '1') {
20✔
1865
                                                    $extractedRange = explode(',', $extractedRange);
×
1866
                                                    foreach ($extractedRange as $range) {
×
1867
                                                        $autoFilterRange = $range;
×
1868
                                                        if (str_contains($autoFilterRange, ':')) {
×
1869
                                                            $docSheet->getAutoFilter()->setRange($autoFilterRange);
×
1870
                                                        }
1871
                                                    }
1872
                                                }
1873

1874
                                                break;
20✔
1875
                                            case '_xlnm.Print_Titles':
30✔
1876
                                                // Split $extractedRange
1877
                                                $extractedRange = explode(',', $extractedRange);
3✔
1878

1879
                                                // Set print titles
1880
                                                foreach ($extractedRange as $range) {
3✔
1881
                                                    $matches = [];
3✔
1882
                                                    $range = str_replace('$', '', $range);
3✔
1883

1884
                                                    // check for repeating columns, e g. 'A:A' or 'A:D'
1885
                                                    if (Preg::isMatch('/!?([A-Z]+)\:([A-Z]+)$/', $range, $matches)) {
3✔
1886
                                                        $docSheet->getPageSetup()->setColumnsToRepeatAtLeft([$matches[1], $matches[2]]);
×
1887
                                                    } elseif (Preg::isMatch('/!?(\d+)\:(\d+)$/', $range, $matches)) {
3✔
1888
                                                        // check for repeating rows, e.g. '1:1' or '1:5'
1889
                                                        $docSheet->getPageSetup()->setRowsToRepeatAtTop([(int) $matches[1], (int) $matches[2]]);
3✔
1890
                                                    }
1891
                                                }
1892

1893
                                                break;
3✔
1894
                                            case '_xlnm.Print_Area':
29✔
1895
                                                $rangeSets = Preg::split("/('?(?:.*?)'?(?:![A-Z0-9]+:[A-Z0-9]+)),?/", $extractedRange, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE) ?: [];
11✔
1896
                                                $newRangeSets = [];
11✔
1897
                                                foreach ($rangeSets as $rangeSet) {
11✔
1898
                                                    [, $rangeSet] = Worksheet::extractSheetTitle($rangeSet, true);
11✔
1899
                                                    if (empty($rangeSet)) {
11✔
1900
                                                        continue;
×
1901
                                                    }
1902
                                                    if (!str_contains($rangeSet, ':')) {
11✔
1903
                                                        $rangeSet = $rangeSet . ':' . $rangeSet;
×
1904
                                                    }
1905
                                                    $newRangeSets[] = str_replace('$', '', $rangeSet);
11✔
1906
                                                }
1907
                                                if (count($newRangeSets) > 0) {
11✔
1908
                                                    $docSheet->getPageSetup()->setPrintArea(implode(',', $newRangeSets));
11✔
1909
                                                }
1910

1911
                                                break;
11✔
1912
                                            default:
1913
                                                break;
19✔
1914
                                        }
1915
                                    }
1916
                                }
1917
                            }
1918

1919
                            // Next sheet id
1920
                            ++$sheetId;
744✔
1921
                        }
1922

1923
                        // Loop through definedNames
1924
                        if ($xmlWorkbook->definedNames) {
748✔
1925
                            foreach ($xmlWorkbook->definedNames->definedName as $definedName) {
407✔
1926
                                // Extract range
1927
                                $extractedRange = (string) $definedName;
108✔
1928

1929
                                // Valid range?
1930
                                if ($extractedRange == '') {
108✔
1931
                                    continue;
×
1932
                                }
1933

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

1960
                                            break;
19✔
1961
                                    }
1962
                                } elseif (!isset($definedName['localSheetId'])) {
78✔
1963
                                    // "Global" definedNames
1964
                                    $locatedSheet = null;
78✔
1965
                                    if (str_contains((string) $definedName, '!')) {
78✔
1966
                                        // Modify range, and extract the first worksheet reference
1967
                                        // Need to split on a comma or a space if not in quotes, and extract the first part.
1968
                                        $definedNameValueParts = Preg::split("/[ ,](?=([^']*'[^']*')*[^']*$)/miuU", $extractedRange);
58✔
1969
                                        // Extract sheet name
1970
                                        [$extractedSheetName] = Worksheet::extractSheetTitle((string) $definedNameValueParts[0], true, true);
58✔
1971

1972
                                        // Locate sheet
1973
                                        $locatedSheet = $excel->getSheetByName("$extractedSheetName");
58✔
1974
                                    }
1975

1976
                                    if ($locatedSheet === null && !DefinedName::testIfFormula($extractedRange)) {
78✔
1977
                                        $extractedRange = '#REF!';
2✔
1978
                                    }
1979
                                    $excel->addDefinedName(DefinedName::createInstance((string) $definedName['name'], $locatedSheet, $extractedRange, false));
78✔
1980
                                }
1981
                            }
1982
                        }
1983
                    }
1984
                    if ($this->createBlankSheetIfNoneRead && !$sheetCreated) {
748✔
1985
                        $excel->createSheet();
2✔
1986
                    }
1987

1988
                    (new WorkbookView($excel))->viewSettings($xmlWorkbook, $mainNS, $mapSheetId, $this->readDataOnly);
748✔
1989

1990
                    break;
746✔
1991
            }
1992
        }
1993

1994
        if (!$this->readDataOnly) {
746✔
1995
            $contentTypes = $this->loadZip('[Content_Types].xml');
743✔
1996

1997
            // Default content types
1998
            foreach ($contentTypes->Default as $contentType) {
743✔
1999
                switch ($contentType['ContentType']) {
741✔
2000
                    case 'application/vnd.openxmlformats-officedocument.spreadsheetml.printerSettings':
741✔
2001
                        $unparsedLoadedData['default_content_types'][(string) $contentType['Extension']] = (string) $contentType['ContentType'];
299✔
2002

2003
                        break;
299✔
2004
                }
2005
            }
2006

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

2039
                        break;
72✔
2040

2041
                        // unparsed
2042
                    case 'application/vnd.ms-excel.controlproperties+xml':
742✔
2043
                        $unparsedLoadedData['override_content_types'][(string) $contentType['PartName']] = (string) $contentType['ContentType'];
4✔
2044

2045
                        break;
4✔
2046
                }
2047
            }
2048
        }
2049

2050
        /** @var array<array<array<array<string>|string>>> $unparsedLoadedData */
2051
        $excel->setUnparsedLoadedData($unparsedLoadedData);
746✔
2052

2053
        $zip->close();
746✔
2054

2055
        return $excel;
746✔
2056
    }
2057

2058
    private function parseRichText(?SimpleXMLElement $is): RichText
93✔
2059
    {
2060
        $value = new RichText();
93✔
2061

2062
        if (isset($is->t)) {
93✔
2063
            $value->createText(StringHelper::controlCharacterOOXML2PHP((string) $is->t));
24✔
2064
        } elseif ($is !== null) {
71✔
2065
            if (is_object($is->r)) {
71✔
2066
                foreach ($is->r as $run) {
71✔
2067
                    if (!isset($run->rPr)) {
68✔
2068
                        $value->createText(StringHelper::controlCharacterOOXML2PHP((string) $run->t));
41✔
2069
                    } else {
2070
                        $objText = $value->createTextRun(StringHelper::controlCharacterOOXML2PHP((string) $run->t));
64✔
2071
                        $objFont = $objText->getFont() ?? new StyleFont();
64✔
2072

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

2140
        return $value;
93✔
2141
    }
2142

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

2186
    /** @param null|bool|mixed[]|SimpleXMLElement $array */
2187
    private static function getArrayItem(null|array|bool|SimpleXMLElement $array, int|string $key = 0): mixed
773✔
2188
    {
2189
        return ($array === null || is_bool($array)) ? null : ($array[$key] ?? null);
773✔
2190
    }
2191

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

2197
        return StringHelper::convertToString($retVal, false);
762✔
2198
    }
2199

2200
    /** @param null|bool|mixed[]|SimpleXMLElement $array */
2201
    private static function getArrayItemIntOrSxml(null|array|bool|SimpleXMLElement $array, int|string $key = 0): int|SimpleXMLElement
75✔
2202
    {
2203
        $retVal = self::getArrayItem($array, $key);
75✔
2204

2205
        return (is_int($retVal) || $retVal instanceof SimpleXMLElement) ? $retVal : 0;
75✔
2206
    }
2207

2208
    private static function dirAdd(null|SimpleXMLElement|string $base, null|SimpleXMLElement|string $add): string
400✔
2209
    {
2210
        $base = (string) $base;
400✔
2211
        $add = (string) $add;
400✔
2212

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

2216
    /** @return mixed[] */
2217
    private static function toCSSArray(string $style): array
3✔
2218
    {
2219
        $style = self::stripWhiteSpaceFromStyleString($style);
3✔
2220

2221
        $temp = explode(';', $style);
3✔
2222
        $style = [];
3✔
2223
        foreach ($temp as $item) {
3✔
2224
            $item = explode(':', $item);
3✔
2225

2226
            if (str_contains($item[1], 'px')) {
3✔
2227
                $item[1] = str_replace('px', '', $item[1]);
2✔
2228
            }
2229
            if (str_contains($item[1], 'pt')) {
3✔
2230
                $item[1] = str_replace('pt', '', $item[1]);
2✔
2231
                $item[1] = Font::fontSizeToPixels((float) $item[1]);
2✔
2232
            }
2233
            if (str_contains($item[1], 'in')) {
3✔
2234
                $item[1] = str_replace('in', '', $item[1]);
×
NEW
2235
                $item[1] = (int) Font::inchSizeToPixels((float) $item[1]);
×
2236
            }
2237
            if (str_contains($item[1], 'cm')) {
3✔
2238
                $item[1] = str_replace('cm', '', $item[1]);
×
NEW
2239
                $item[1] = (int) Font::centimeterSizeToPixels((float) $item[1]);
×
2240
            }
2241
            if (str_contains($item[1], 'mm')) {
3✔
NEW
UNCOV
2242
                $item[1] = str_replace('mm', '', $item[1]);
×
NEW
UNCOV
2243
                $item[1] = (int) Font::centimeterSizeToPixels((float) $item[1] / 10);
×
2244
            }
2245

2246
            $style[$item[0]] = $item[1];
3✔
2247
        }
2248

2249
        return $style;
3✔
2250
    }
2251

2252
    public static function stripWhiteSpaceFromStyleString(string $string): string
6✔
2253
    {
2254
        return trim(str_replace(["\r", "\n", ' '], '', $string), ';');
6✔
2255
    }
2256

2257
    private static function boolean(string $value): bool
106✔
2258
    {
2259
        if (is_numeric($value)) {
106✔
2260
            return (bool) $value;
72✔
2261
        }
2262

2263
        return $value === 'true' || $value === 'TRUE';
47✔
2264
    }
2265

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

2271
        if ($hlinkClick->count() === 0) {
72✔
2272
            return;
68✔
2273
        }
2274

2275
        $hlinkId = (string) self::getAttributes($hlinkClick, Namespaces::SCHEMA_OFFICE_DOCUMENT)['id'];
4✔
2276
        $hyperlink = new Hyperlink(
4✔
2277
            Preg::replace('/^#/', 'sheet://', $hyperlinks[$hlinkId]),
4✔
2278
            self::getArrayItemString(
4✔
2279
                self::getAttributes(
4✔
2280
                    $cellAnchor->pic->nvPicPr->cNvPr
4✔
2281
                ),
4✔
2282
                'name'
4✔
2283
            )
4✔
2284
        );
4✔
2285
        $objDrawing->setHyperlink($hyperlink);
4✔
2286
    }
2287

2288
    private function readProtection(Spreadsheet $excel, SimpleXMLElement $xmlWorkbook): void
749✔
2289
    {
2290
        if (!$xmlWorkbook->workbookProtection) {
749✔
2291
            return;
715✔
2292
        }
2293

2294
        $security = $excel->getSecurity();
39✔
2295
        $security->setLockRevision(
39✔
2296
            self::getLockValue($xmlWorkbook->workbookProtection, 'lockRevision')
39✔
2297
        );
39✔
2298
        $security->setLockStructure(
39✔
2299
            self::getLockValue($xmlWorkbook->workbookProtection, 'lockStructure')
39✔
2300
        );
39✔
2301
        $security->setLockWindows(
39✔
2302
            self::getLockValue($xmlWorkbook->workbookProtection, 'lockWindows')
39✔
2303
        );
39✔
2304

2305
        if ($xmlWorkbook->workbookProtection['revisionsPassword']) {
39✔
2306
            $security->setRevisionsPassword(
1✔
2307
                (string) $xmlWorkbook->workbookProtection['revisionsPassword'],
1✔
2308
                true
1✔
2309
            );
1✔
2310
        }
2311
        if ($xmlWorkbook->workbookProtection['revisionsAlgorithmName']) {
39✔
2312
            $security->setRevisionsAlgorithmName(
1✔
2313
                (string) $xmlWorkbook->workbookProtection['revisionsAlgorithmName']
1✔
2314
            );
1✔
2315
        }
2316
        if ($xmlWorkbook->workbookProtection['revisionsSaltValue']) {
39✔
2317
            $security->setRevisionsSaltValue(
1✔
2318
                (string) $xmlWorkbook->workbookProtection['revisionsSaltValue'],
1✔
2319
                false
1✔
2320
            );
1✔
2321
        }
2322
        if ($xmlWorkbook->workbookProtection['revisionsSpinCount']) {
39✔
2323
            $security->setRevisionsSpinCount(
1✔
2324
                (int) $xmlWorkbook->workbookProtection['revisionsSpinCount']
1✔
2325
            );
1✔
2326
        }
2327
        if ($xmlWorkbook->workbookProtection['revisionsHashValue']) {
39✔
2328
            if ($security->advancedRevisionsPassword()) {
1✔
2329
                $security->setRevisionsPassword(
1✔
2330
                    (string) $xmlWorkbook->workbookProtection['revisionsHashValue'],
1✔
2331
                    true
1✔
2332
                );
1✔
2333
            }
2334
        }
2335

2336
        if ($xmlWorkbook->workbookProtection['workbookPassword']) {
39✔
2337
            $security->setWorkbookPassword(
2✔
2338
                (string) $xmlWorkbook->workbookProtection['workbookPassword'],
2✔
2339
                true
2✔
2340
            );
2✔
2341
        }
2342

2343
        if ($xmlWorkbook->workbookProtection['workbookAlgorithmName']) {
39✔
2344
            $security->setWorkbookAlgorithmName(
2✔
2345
                (string) $xmlWorkbook->workbookProtection['workbookAlgorithmName']
2✔
2346
            );
2✔
2347
        }
2348
        if ($xmlWorkbook->workbookProtection['workbookSaltValue']) {
39✔
2349
            $security->setWorkbookSaltValue(
2✔
2350
                (string) $xmlWorkbook->workbookProtection['workbookSaltValue'],
2✔
2351
                false
2✔
2352
            );
2✔
2353
        }
2354
        if ($xmlWorkbook->workbookProtection['workbookSpinCount']) {
39✔
2355
            $security->setWorkbookSpinCount(
2✔
2356
                (int) $xmlWorkbook->workbookProtection['workbookSpinCount']
2✔
2357
            );
2✔
2358
        }
2359
        if ($xmlWorkbook->workbookProtection['workbookHashValue']) {
39✔
2360
            if ($security->advancedPassword()) {
2✔
2361
                $security->setWorkbookPassword(
2✔
2362
                    (string) $xmlWorkbook->workbookProtection['workbookHashValue'],
2✔
2363
                    true
2✔
2364
                );
2✔
2365
            }
2366
        }
2367
    }
2368

2369
    private static function getLockValue(SimpleXMLElement $protection, string $key): ?bool
39✔
2370
    {
2371
        $returnValue = null;
39✔
2372
        $protectKey = $protection[$key];
39✔
2373
        if (!empty($protectKey)) {
39✔
2374
            $protectKey = (string) $protectKey;
12✔
2375
            $returnValue = $protectKey !== 'false' && (bool) $protectKey;
12✔
2376
        }
2377

2378
        return $returnValue;
39✔
2379
    }
2380

2381
    /** @param mixed[][][][] $unparsedLoadedData */
2382
    private function readFormControlProperties(Spreadsheet $excel, string $dir, string $fileWorksheet, Worksheet $docSheet, array &$unparsedLoadedData): void
744✔
2383
    {
2384
        $zip = $this->zip;
744✔
2385
        if ($zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels') === false) {
744✔
2386
            return;
357✔
2387
        }
2388

2389
        $filename = dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels';
615✔
2390
        $relsWorksheet = $this->loadZipNoNamespace($filename, Namespaces::RELATIONSHIPS);
615✔
2391
        $ctrlProps = [];
615✔
2392
        foreach ($relsWorksheet->Relationship as $ele) {
615✔
2393
            if ((string) $ele['Type'] === Namespaces::SCHEMA_OFFICE_DOCUMENT . '/ctrlProp') {
425✔
2394
                $ctrlProps[(string) $ele['Id']] = $ele;
4✔
2395
            }
2396
        }
2397

2398
        /** @var mixed[][] */
2399
        $unparsedCtrlProps = &$unparsedLoadedData['sheets'][$docSheet->getCodeName()]['ctrlProps'];
615✔
2400
        foreach ($ctrlProps as $rId => $ctrlProp) {
615✔
2401
            $rId = substr($rId, 3); // rIdXXX
4✔
2402
            $unparsedCtrlProps[$rId] = [];
4✔
2403
            $unparsedCtrlProps[$rId]['filePath'] = self::dirAdd("$dir/$fileWorksheet", $ctrlProp['Target']);
4✔
2404
            $unparsedCtrlProps[$rId]['relFilePath'] = (string) $ctrlProp['Target'];
4✔
2405
            $unparsedCtrlProps[$rId]['content'] = $this->getSecurityScannerOrThrow()->scan($this->getFromZipArchive($zip, $unparsedCtrlProps[$rId]['filePath']));
4✔
2406
        }
2407
        unset($unparsedCtrlProps);
615✔
2408
    }
2409

2410
    /** @param mixed[][][][] $unparsedLoadedData */
2411
    private function readPrinterSettings(Spreadsheet $excel, string $dir, string $fileWorksheet, Worksheet $docSheet, array &$unparsedLoadedData): void
744✔
2412
    {
2413
        if ($this->readDataOnly) {
744✔
2414
            return;
3✔
2415
        }
2416
        $zip = $this->zip;
741✔
2417
        if ($zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels') === false) {
741✔
2418
            return;
356✔
2419
        }
2420

2421
        $filename = dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels';
612✔
2422
        $relsWorksheet = $this->loadZipNoNamespace($filename, Namespaces::RELATIONSHIPS);
612✔
2423
        $sheetPrinterSettings = [];
612✔
2424
        foreach ($relsWorksheet->Relationship as $ele) {
612✔
2425
            if ((string) $ele['Type'] === Namespaces::SCHEMA_OFFICE_DOCUMENT . '/printerSettings') {
423✔
2426
                $sheetPrinterSettings[(string) $ele['Id']] = $ele;
289✔
2427
            }
2428
        }
2429

2430
        /** @var mixed[][] */
2431
        $unparsedPrinterSettings = &$unparsedLoadedData['sheets'][$docSheet->getCodeName()]['printerSettings'];
612✔
2432
        foreach ($sheetPrinterSettings as $rId => $printerSettings) {
612✔
2433
            $rId = substr($rId, 3); // rIdXXX
289✔
2434
            if (!str_ends_with($rId, 'ps')) {
289✔
2435
                $rId = $rId . 'ps'; // rIdXXX, add 'ps' suffix to avoid identical resource identifier collision with unparsed vmlDrawing
289✔
2436
            }
2437
            $unparsedPrinterSettings[$rId] = [];
289✔
2438
            $target = (string) str_replace('/xl/', '../', (string) $printerSettings['Target']);
289✔
2439
            $unparsedPrinterSettings[$rId]['filePath'] = self::dirAdd("$dir/$fileWorksheet", $target);
289✔
2440
            $unparsedPrinterSettings[$rId]['relFilePath'] = $target;
289✔
2441
            $unparsedPrinterSettings[$rId]['content'] = $this->getSecurityScannerOrThrow()->scan($this->getFromZipArchive($zip, $unparsedPrinterSettings[$rId]['filePath']));
289✔
2442
        }
2443
        unset($unparsedPrinterSettings);
612✔
2444
    }
2445

2446
    /** @return array{string, string} */
2447
    private function getWorkbookBaseName(): array
760✔
2448
    {
2449
        $workbookBasename = '';
760✔
2450
        $xmlNamespaceBase = '';
760✔
2451

2452
        // check if it is an OOXML archive
2453
        $rels = $this->loadZip(self::INITIAL_FILE);
760✔
2454
        foreach ($rels->children(Namespaces::RELATIONSHIPS)->Relationship as $rel) {
760✔
2455
            $rel = self::getAttributes($rel);
760✔
2456
            $type = (string) $rel['Type'];
760✔
2457
            switch ($type) {
2458
                case Namespaces::OFFICE_DOCUMENT:
753✔
2459
                case Namespaces::PURL_OFFICE_DOCUMENT:
738✔
2460
                    $basename = basename((string) $rel['Target']);
760✔
2461
                    $xmlNamespaceBase = dirname($type);
760✔
2462
                    if (Preg::isMatch('/workbook.*\.xml/', $basename)) {
760✔
2463
                        $workbookBasename = $basename;
760✔
2464
                    }
2465

2466
                    break;
760✔
2467
            }
2468
        }
2469

2470
        return [$workbookBasename, $xmlNamespaceBase];
760✔
2471
    }
2472

2473
    private function readSheetProtection(Worksheet $docSheet, SimpleXMLElement $xmlSheet): void
735✔
2474
    {
2475
        if ($this->readDataOnly || !$xmlSheet->sheetProtection) {
735✔
2476
            return;
677✔
2477
        }
2478

2479
        $algorithmName = (string) $xmlSheet->sheetProtection['algorithmName'];
69✔
2480
        $protection = $docSheet->getProtection();
69✔
2481
        $protection->setAlgorithm($algorithmName);
69✔
2482

2483
        if ($algorithmName) {
69✔
2484
            $protection->setPassword((string) $xmlSheet->sheetProtection['hashValue'], true);
2✔
2485
            $protection->setSalt((string) $xmlSheet->sheetProtection['saltValue']);
2✔
2486
            $protection->setSpinCount((int) $xmlSheet->sheetProtection['spinCount']);
2✔
2487
        } else {
2488
            $protection->setPassword((string) $xmlSheet->sheetProtection['password'], true);
68✔
2489
        }
2490

2491
        if ($xmlSheet->protectedRanges->protectedRange) {
69✔
2492
            foreach ($xmlSheet->protectedRanges->protectedRange as $protectedRange) {
4✔
2493
                $docSheet->protectCells((string) $protectedRange['sqref'], (string) $protectedRange['password'], true, (string) $protectedRange['name'], (string) $protectedRange['securityDescriptor']);
4✔
2494
            }
2495
        }
2496
    }
2497

2498
    private function readAutoFilter(
742✔
2499
        SimpleXMLElement $xmlSheet,
2500
        Worksheet $docSheet
2501
    ): void {
2502
        if ($xmlSheet && $xmlSheet->autoFilter) {
742✔
2503
            (new AutoFilter($docSheet, $xmlSheet))->load();
18✔
2504
        }
2505
    }
2506

2507
    private function readBackgroundImage(
742✔
2508
        SimpleXMLElement $xmlSheet,
2509
        Worksheet $docSheet,
2510
        string $relsName
2511
    ): void {
2512
        if ($xmlSheet && $xmlSheet->picture) {
742✔
2513
            $id = (string) self::getArrayItemString(self::getAttributes($xmlSheet->picture, Namespaces::SCHEMA_OFFICE_DOCUMENT), 'id');
1✔
2514
            $rels = $this->loadZip($relsName);
1✔
2515
            foreach ($rels->Relationship as $rel) {
1✔
2516
                $attrs = $rel->attributes() ?? [];
1✔
2517
                $rid = (string) ($attrs['Id'] ?? '');
1✔
2518
                $target = (string) ($attrs['Target'] ?? '');
1✔
2519
                if ($rid === $id && str_starts_with($target, '..')) {
1✔
2520
                    $target = 'xl' . substr($target, 2);
1✔
2521
                    $content = $this->getFromZipArchive($this->zip, $target);
1✔
2522
                    $docSheet->setBackgroundImage($content);
1✔
2523
                }
2524
            }
2525
        }
2526
    }
2527

2528
    /**
2529
     * @param TableDxfsStyle[] $tableStyles
2530
     * @param Style[] $dxfs
2531
     */
2532
    private function readTables(
745✔
2533
        SimpleXMLElement $xmlSheet,
2534
        Worksheet $docSheet,
2535
        string $dir,
2536
        string $fileWorksheet,
2537
        ZipArchive $zip,
2538
        string $namespaceTable,
2539
        array $tableStyles,
2540
        array $dxfs
2541
    ): void {
2542
        if ($xmlSheet && $xmlSheet->tableParts) {
745✔
2543
            /** @var array{count: scalar} */
2544
            $attributes = $xmlSheet->tableParts->attributes() ?? ['count' => 0];
37✔
2545
            if (((int) $attributes['count']) > 0) {
37✔
2546
                $this->readTablesInTablesFile($xmlSheet, $dir, $fileWorksheet, $zip, $docSheet, $namespaceTable, $tableStyles, $dxfs);
33✔
2547
            }
2548
        }
2549
    }
2550

2551
    /**
2552
     * @param TableDxfsStyle[] $tableStyles
2553
     * @param Style[] $dxfs
2554
     */
2555
    private function readTablesInTablesFile(
33✔
2556
        SimpleXMLElement $xmlSheet,
2557
        string $dir,
2558
        string $fileWorksheet,
2559
        ZipArchive $zip,
2560
        Worksheet $docSheet,
2561
        string $namespaceTable,
2562
        array $tableStyles,
2563
        array $dxfs
2564
    ): void {
2565
        foreach ($xmlSheet->tableParts->tablePart as $tablePart) {
33✔
2566
            $relation = self::getAttributes($tablePart, Namespaces::SCHEMA_OFFICE_DOCUMENT);
33✔
2567
            $tablePartRel = (string) $relation['id'];
33✔
2568
            $relationsFileName = dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels';
33✔
2569

2570
            if ($zip->locateName($relationsFileName) !== false) {
33✔
2571
                $relsTableReferences = $this->loadZip($relationsFileName, Namespaces::RELATIONSHIPS);
33✔
2572
                foreach ($relsTableReferences->Relationship as $relationship) {
33✔
2573
                    $relationshipAttributes = self::getAttributes($relationship, '');
33✔
2574

2575
                    if ((string) $relationshipAttributes['Id'] === $tablePartRel) {
33✔
2576
                        $relationshipFileName = (string) $relationshipAttributes['Target'];
33✔
2577
                        $relationshipFilePath = dirname("$dir/$fileWorksheet") . '/' . $relationshipFileName;
33✔
2578
                        $relationshipFilePath = File::realpath($relationshipFilePath);
33✔
2579

2580
                        if ($this->fileExistsInArchive($this->zip, $relationshipFilePath)) {
33✔
2581
                            $tableXml = $this->loadZip($relationshipFilePath, $namespaceTable);
33✔
2582
                            (new TableReader($docSheet, $tableXml))->load($tableStyles, $dxfs);
33✔
2583
                        }
2584
                    }
2585
                }
2586
            }
2587
        }
2588
    }
2589

2590
    /** @return mixed[] */
2591
    private static function extractStyles(?SimpleXMLElement $sxml, string $node1, string $node2): array
756✔
2592
    {
2593
        $array = [];
756✔
2594
        if ($sxml && $sxml->{$node1}->{$node2}) {
756✔
2595
            /** @var SimpleXMLElement */
2596
            $temp = $sxml->{$node1}->{$node2};
756✔
2597
            foreach ($temp as $node) {
756✔
2598
                $array[] = $node;
756✔
2599
            }
2600
        }
2601

2602
        return $array;
756✔
2603
    }
2604

2605
    /** @return string[] */
2606
    private static function extractPalette(?SimpleXMLElement $sxml): array
756✔
2607
    {
2608
        $array = [];
756✔
2609
        if ($sxml && $sxml->colors->indexedColors) {
756✔
2610
            foreach ($sxml->colors->indexedColors->rgbColor as $node) {
16✔
2611
                $attr = $node->attributes();
16✔
2612
                if (isset($attr['rgb'])) {
16✔
2613
                    $array[] = (string) $attr['rgb'];
16✔
2614
                }
2615
            }
2616
        }
2617

2618
        return $array;
756✔
2619
    }
2620

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

2673
    private static function storeFormulaAttributes(SimpleXMLElement $f, Worksheet $docSheet, string $r): void
375✔
2674
    {
2675
        $formulaAttributes = [];
375✔
2676
        $attributes = $f->attributes();
375✔
2677
        if (isset($attributes['t'])) {
375✔
2678
            $formulaAttributes['t'] = (string) $attributes['t'];
249✔
2679
        }
2680
        if (isset($attributes['ref'])) {
375✔
2681
            $formulaAttributes['ref'] = (string) $attributes['ref'];
249✔
2682
        }
2683
        if (!empty($formulaAttributes)) {
375✔
2684
            $docSheet->getCell($r)->setFormulaAttributes($formulaAttributes);
249✔
2685
        }
2686
    }
2687

2688
    private static function onlyNoteVml(string $data): bool
22✔
2689
    {
2690
        $data = str_replace('<br>', '<br/>', $data);
22✔
2691

2692
        try {
2693
            $sxml = @simplexml_load_string($data);
22✔
UNCOV
2694
        } catch (Throwable) {
×
UNCOV
2695
            $sxml = false;
×
2696
        }
2697

2698
        if ($sxml === false) {
22✔
2699
            return false;
1✔
2700
        }
2701
        $shapes = $sxml->children(Namespaces::URN_VML);
21✔
2702
        foreach ($shapes->shape as $shape) {
21✔
2703
            $clientData = $shape->children(Namespaces::URN_EXCEL);
21✔
2704
            if (!isset($clientData->ClientData)) {
21✔
2705
                return false;
×
2706
            }
2707
            $attrs = $clientData->ClientData->attributes();
21✔
2708
            if (!isset($attrs['ObjectType'])) {
21✔
UNCOV
2709
                return false;
×
2710
            }
2711
            $objectType = (string) $attrs['ObjectType'];
21✔
2712
            if ($objectType !== 'Note') {
21✔
2713
                return false;
4✔
2714
            }
2715
        }
2716

2717
        return true;
18✔
2718
    }
2719
}
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