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

PHPOffice / PhpSpreadsheet / 23551431468

25 Mar 2026 04:13PM UTC coverage: 96.961% (+0.06%) from 96.904%
23551431468

Pull #4833

github

web-flow
Merge 972cbe815 into eb391d1f2
Pull Request #4833: Optimize XLS (BIFF8) reader performance with reduced per-cell overhead

74 of 77 new or added lines in 3 files covered. (96.1%)

111 existing lines in 4 files now uncovered.

47796 of 49294 relevant lines covered (96.96%)

384.48 hits per line

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

89.05
/src/PhpSpreadsheet/Reader/Xls.php
1
<?php
2

3
namespace PhpOffice\PhpSpreadsheet\Reader;
4

5
use Composer\Pcre\Preg;
6
use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
7
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
8
use PhpOffice\PhpSpreadsheet\Cell\DataType;
9
use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
10
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
11
use PhpOffice\PhpSpreadsheet\Reader\Xls\Style\CellFont;
12
use PhpOffice\PhpSpreadsheet\Reader\Xls\Style\FillPattern;
13
use PhpOffice\PhpSpreadsheet\RichText\RichText;
14
use PhpOffice\PhpSpreadsheet\Shared\CodePage;
15
use PhpOffice\PhpSpreadsheet\Shared\Date;
16
use PhpOffice\PhpSpreadsheet\Shared\Escher;
17
use PhpOffice\PhpSpreadsheet\Shared\File;
18
use PhpOffice\PhpSpreadsheet\Shared\OLE;
19
use PhpOffice\PhpSpreadsheet\Shared\OLERead;
20
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
21
use PhpOffice\PhpSpreadsheet\Spreadsheet;
22
use PhpOffice\PhpSpreadsheet\Style\Alignment;
23
use PhpOffice\PhpSpreadsheet\Style\Border;
24
use PhpOffice\PhpSpreadsheet\Style\Borders;
25
use PhpOffice\PhpSpreadsheet\Style\Conditional;
26
use PhpOffice\PhpSpreadsheet\Style\Fill;
27
use PhpOffice\PhpSpreadsheet\Style\Font;
28
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
29
use PhpOffice\PhpSpreadsheet\Style\Protection;
30
use PhpOffice\PhpSpreadsheet\Style\Style;
31
use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup;
32
use PhpOffice\PhpSpreadsheet\Worksheet\SheetView;
33
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
34

35
// Original file header of ParseXL (used as the base for this class):
36
// --------------------------------------------------------------------------------
37
// Adapted from Excel_Spreadsheet_Reader developed by users bizon153,
38
// trex005, and mmp11 (SourceForge.net)
39
// https://sourceforge.net/projects/phpexcelreader/
40
// Primary changes made by canyoncasa (dvc) for ParseXL 1.00 ...
41
//     Modelled moreso after Perl Excel Parse/Write modules
42
//     Added Parse_Excel_Spreadsheet object
43
//         Reads a whole worksheet or tab as row,column array or as
44
//         associated hash of indexed rows and named column fields
45
//     Added variables for worksheet (tab) indexes and names
46
//     Added an object call for loading individual woorksheets
47
//     Changed default indexing defaults to 0 based arrays
48
//     Fixed date/time and percent formats
49
//     Includes patches found at SourceForge...
50
//         unicode patch by nobody
51
//         unpack("d") machine depedency patch by matchy
52
//         boundsheet utf16 patch by bjaenichen
53
//     Renamed functions for shorter names
54
//     General code cleanup and rigor, including <80 column width
55
//     Included a testcase Excel file and PHP example calls
56
//     Code works for PHP 5.x
57

58
// Primary changes made by canyoncasa (dvc) for ParseXL 1.10 ...
59
// http://sourceforge.net/tracker/index.php?func=detail&aid=1466964&group_id=99160&atid=623334
60
//     Decoding of formula conditions, results, and tokens.
61
//     Support for user-defined named cells added as an array "namedcells"
62
//         Patch code for user-defined named cells supports single cells only.
63
//         NOTE: this patch only works for BIFF8 as BIFF5-7 use a different
64
//         external sheet reference structure
65
class Xls extends XlsBase
66
{
67
    /**
68
     * Summary Information stream data.
69
     */
70
    protected ?string $summaryInformation = null;
71

72
    /**
73
     * Extended Summary Information stream data.
74
     */
75
    protected ?string $documentSummaryInformation = null;
76

77
    /**
78
     * Workbook stream data. (Includes workbook globals substream as well as sheet substreams).
79
     */
80
    protected string $data;
81

82
    /**
83
     * Size in bytes of $this->data.
84
     */
85
    protected int $dataSize;
86

87
    /**
88
     * Current position in stream.
89
     */
90
    protected int $pos;
91

92
    /**
93
     * Workbook to be returned by the reader.
94
     */
95
    protected Spreadsheet $spreadsheet;
96

97
    /**
98
     * Worksheet that is currently being built by the reader.
99
     */
100
    protected Worksheet $phpSheet;
101

102
    /**
103
     * Cached sheet title for the current sheet being parsed.
104
     * Avoids repeated getTitle() calls in per-cell read filter checks.
105
     */
106
    protected string $phpSheetTitle = '';
107

108
    /**
109
     * Cached read filter reference.
110
     * Avoids repeated getReadFilter() calls in per-cell read filter checks.
111
     */
112
    protected IReadFilter $cachedReadFilter;
113

114
    /**
115
     * BIFF version.
116
     */
117
    protected int $version = 0;
118

119
    /**
120
     * Shared formats.
121
     *
122
     * @var mixed[]
123
     */
124
    protected array $formats;
125

126
    /**
127
     * Shared fonts.
128
     *
129
     * @var Font[]
130
     */
131
    protected array $objFonts;
132

133
    /**
134
     * Color palette.
135
     *
136
     * @var string[][]
137
     */
138
    protected array $palette;
139

140
    /**
141
     * Worksheets.
142
     *
143
     * @var array<array{name: string, offset: int, sheetState: string, sheetType: int|string}>
144
     */
145
    protected array $sheets;
146

147
    /**
148
     * External books.
149
     *
150
     * @var mixed[][]
151
     */
152
    protected array $externalBooks;
153

154
    /**
155
     * REF structures. Only applies to BIFF8.
156
     *
157
     * @var array<int, array{'externalBookIndex': int, 'firstSheetIndex': int, 'lastSheetIndex': int}>
158
     */
159
    protected array $ref;
160

161
    /**
162
     * External names.
163
     *
164
     * @var array<array<string, mixed>|string>
165
     */
166
    protected array $externalNames;
167

168
    /**
169
     * Defined names.
170
     *
171
     * @var array{isBuiltInName: int, name: string, formula: string, scope: int}
172
     */
173
    protected array $definedname;
174

175
    /**
176
     * Shared strings. Only applies to BIFF8.
177
     *
178
     * @var array<array{value: string, fmtRuns: mixed[]}>
179
     */
180
    protected array $sst;
181

182
    /**
183
     * Panes are frozen? (in sheet currently being read). See WINDOW2 record.
184
     */
185
    protected bool $frozen;
186

187
    /**
188
     * Fit printout to number of pages? (in sheet currently being read). See SHEETPR record.
189
     */
190
    protected bool $isFitToPages;
191

192
    /**
193
     * Objects. One OBJ record contributes with one entry.
194
     *
195
     * @var mixed[]
196
     */
197
    protected array $objs;
198

199
    /**
200
     * Text Objects. One TXO record corresponds with one entry.
201
     *
202
     * @var array<array{text: string, format: string, alignment: int, rotation: int}>
203
     */
204
    protected array $textObjects;
205

206
    /**
207
     * Cell Annotations (BIFF8).
208
     *
209
     * @var mixed[]
210
     */
211
    protected array $cellNotes;
212

213
    /**
214
     * The combined MSODRAWINGGROUP data.
215
     */
216
    protected string $drawingGroupData;
217

218
    /**
219
     * The combined MSODRAWING data (per sheet).
220
     */
221
    protected string $drawingData;
222

223
    /**
224
     * Keep track of XF index.
225
     */
226
    protected int $xfIndex;
227

228
    /**
229
     * Mapping of XF index (that is a cell XF) to final index in cellXf collection.
230
     *
231
     * @var int[]
232
     */
233
    protected array $mapCellXfIndex;
234

235
    /**
236
     * Mapping of XF index (that is a style XF) to final index in cellStyleXf collection.
237
     *
238
     * @var int[]
239
     */
240
    protected array $mapCellStyleXfIndex;
241

242
    /**
243
     * The shared formulas in a sheet. One SHAREDFMLA record contributes with one value.
244
     *
245
     * @var mixed[]
246
     */
247
    protected array $sharedFormulas;
248

249
    /**
250
     * The shared formula parts in a sheet. One FORMULA record contributes with one value if it
251
     * refers to a shared formula.
252
     *
253
     * @var mixed[]
254
     */
255
    protected array $sharedFormulaParts;
256

257
    /**
258
     * The type of encryption in use.
259
     */
260
    protected int $encryption = 0;
261

262
    /**
263
     * The position in the stream after which contents are encrypted.
264
     */
265
    protected int $encryptionStartPos = 0;
266

267
    protected string $encryptionPassword = 'VelvetSweatshop';
268

269
    /**
270
     * The current RC4 decryption object.
271
     */
272
    protected ?Xls\RC4 $rc4Key = null;
273

274
    /**
275
     * The position in the stream that the RC4 decryption object was left at.
276
     */
277
    protected int $rc4Pos = 0;
278

279
    /**
280
     * The current MD5 context state.
281
     * It is set via call-by-reference to verifyPassword.
282
     */
283
    private string $md5Ctxt = '';
284

285
    protected int $textObjRef;
286

287
    protected string $baseCell;
288

289
    protected bool $activeSheetSet = false;
290

291
    /**
292
     * Reads names of the worksheets from a file, without parsing the whole file to a PhpSpreadsheet object.
293
     *
294
     * @return string[]
295
     */
296
    public function listWorksheetNames(string $filename): array
7✔
297
    {
298
        return (new Xls\ListFunctions())->listWorksheetNames2($filename, $this);
7✔
299
    }
300

301
    /**
302
     * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
303
     *
304
     * @return array<int, array{worksheetName: string, lastColumnLetter: string, lastColumnIndex: int, totalRows: int, totalColumns: int, sheetState: string}>
305
     */
306
    public function listWorksheetInfo(string $filename): array
7✔
307
    {
308
        return (new Xls\ListFunctions())->listWorksheetInfo2($filename, $this);
7✔
309
    }
310

311
    /**
312
     * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
313
     *
314
     * @return array<int, array{worksheetName: string, dimensionsMinR: int, dimensionsMinC: int, dimensionsMaxR: int, dimensionsMaxC: int, lastColumnLetter: string}>
315
     */
316
    public function listWorksheetDimensions(string $filename): array
2✔
317
    {
318
        return (new Xls\ListFunctions())->listWorksheetDimensions2($filename, $this);
2✔
319
    }
320

321
    /**
322
     * Loads PhpSpreadsheet from file.
323
     */
324
    protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
141✔
325
    {
326
        return (new Xls\LoadSpreadsheet())->loadSpreadsheetFromFile2($filename, $this);
141✔
327
    }
328

329
    /**
330
     * Read record data from stream, decrypting as required.
331
     *
332
     * @param string $data Data stream to read from
333
     * @param int $pos Position to start reading from
334
     * @param int $len Record data length
335
     *
336
     * @return string Record data
337
     */
338
    protected function readRecordData(string $data, int $pos, int $len): string
154✔
339
    {
340
        $data = substr($data, $pos, $len);
154✔
341

342
        // File not encrypted, or record before encryption start point
343
        if ($this->encryption == self::MS_BIFF_CRYPTO_NONE || $pos < $this->encryptionStartPos) {
154✔
344
            return $data;
154✔
345
        }
346

347
        $recordData = '';
2✔
348
        if ($this->encryption == self::MS_BIFF_CRYPTO_RC4) {
2✔
349
            $oldBlock = floor($this->rc4Pos / self::REKEY_BLOCK);
2✔
350
            $block = (int) floor($pos / self::REKEY_BLOCK);
2✔
351
            $endBlock = (int) floor(($pos + $len) / self::REKEY_BLOCK);
2✔
352

353
            // Spin an RC4 decryptor to the right spot. If we have a decryptor sitting
354
            // at a point earlier in the current block, re-use it as we can save some time.
355
            if ($block != $oldBlock || $pos < $this->rc4Pos || !$this->rc4Key) {
2✔
356
                $this->rc4Key = $this->makeKey($block, $this->md5Ctxt);
2✔
357
                $step = $pos % self::REKEY_BLOCK;
2✔
358
            } else {
359
                $step = $pos - $this->rc4Pos;
2✔
360
            }
361
            $this->rc4Key->RC4(str_repeat("\0", $step));
2✔
362

363
            // Decrypt record data (re-keying at the end of every block)
364
            while ($block != $endBlock) {
2✔
365
                $step = self::REKEY_BLOCK - ($pos % self::REKEY_BLOCK);
1✔
366
                $recordData .= $this->rc4Key->RC4(substr($data, 0, $step));
1✔
367
                $data = substr($data, $step);
1✔
368
                $pos += $step;
1✔
369
                $len -= $step;
1✔
370
                ++$block;
1✔
371
                $this->rc4Key = $this->makeKey($block, $this->md5Ctxt);
1✔
372
            }
373
            $recordData .= $this->rc4Key->RC4(substr($data, 0, $len));
2✔
374

375
            // Keep track of the position of this decryptor.
376
            // We'll try and re-use it later if we can to speed things up
377
            $this->rc4Pos = $pos + $len;
2✔
378
        } elseif ($this->encryption == self::MS_BIFF_CRYPTO_XOR) {
×
379
            throw new Exception('XOr encryption not supported');
×
380
        }
381

382
        return $recordData;
2✔
383
    }
384

385
    /**
386
     * Use OLE reader to extract the relevant data streams from the OLE file.
387
     */
388
    protected function loadOLE(string $filename): void
154✔
389
    {
390
        // OLE reader
391
        $ole = new OLERead();
154✔
392
        // get excel data,
393
        $ole->read($filename);
154✔
394
        // Get workbook data: workbook stream + sheet streams
395
        $this->data = $ole->getStream($ole->wrkbook); // @phpstan-ignore-line
154✔
396
        // Get summary information data
397
        $this->summaryInformation = $ole->getStream($ole->summaryInformation);
154✔
398
        // Get additional document summary information data
399
        $this->documentSummaryInformation = $ole->getStream($ole->documentSummaryInformation);
154✔
400
    }
401

402
    /**
403
     * Read summary information.
404
     */
405
    protected function readSummaryInformation(): void
141✔
406
    {
407
        if (!isset($this->summaryInformation)) {
141✔
408
            return;
3✔
409
        }
410

411
        // offset: 0; size: 2; must be 0xFE 0xFF (UTF-16 LE byte order mark)
412
        // offset: 2; size: 2;
413
        // offset: 4; size: 2; OS version
414
        // offset: 6; size: 2; OS indicator
415
        // offset: 8; size: 16
416
        // offset: 24; size: 4; section count
417
        //$secCount = self::getInt4d($this->summaryInformation, 24);
418

419
        // offset: 28; size: 16; first section's class id: e0 85 9f f2 f9 4f 68 10 ab 91 08 00 2b 27 b3 d9
420
        // offset: 44; size: 4
421
        $secOffset = self::getInt4d($this->summaryInformation, 44);
138✔
422

423
        // section header
424
        // offset: $secOffset; size: 4; section length
425
        //$secLength = self::getInt4d($this->summaryInformation, $secOffset);
426

427
        // offset: $secOffset+4; size: 4; property count
428
        $countProperties = self::getInt4d($this->summaryInformation, $secOffset + 4);
138✔
429

430
        // initialize code page (used to resolve string values)
431
        $codePage = 'CP1252';
138✔
432

433
        // offset: ($secOffset+8); size: var
434
        // loop through property decarations and properties
435
        for ($i = 0; $i < $countProperties; ++$i) {
138✔
436
            // offset: ($secOffset+8) + (8 * $i); size: 4; property ID
437
            $id = self::getInt4d($this->summaryInformation, ($secOffset + 8) + (8 * $i));
138✔
438

439
            // Use value of property id as appropriate
440
            // offset: ($secOffset+12) + (8 * $i); size: 4; offset from beginning of section (48)
441
            $offset = self::getInt4d($this->summaryInformation, ($secOffset + 12) + (8 * $i));
138✔
442

443
            $type = self::getInt4d($this->summaryInformation, $secOffset + $offset);
138✔
444

445
            // initialize property value
446
            $value = null;
138✔
447

448
            // extract property value based on property type
449
            switch ($type) {
450
                case 0x02: // 2 byte signed integer
138✔
451
                    $value = self::getUInt2d($this->summaryInformation, $secOffset + 4 + $offset);
138✔
452

453
                    break;
138✔
454
                case 0x03: // 4 byte signed integer
138✔
455
                    $value = self::getInt4d($this->summaryInformation, $secOffset + 4 + $offset);
134✔
456

457
                    break;
134✔
458
                case 0x13: // 4 byte unsigned integer
138✔
459
                    // not needed yet, fix later if necessary
460
                    break;
1✔
461
                case 0x1E: // null-terminated string prepended by dword string length
138✔
462
                    $byteLength = self::getInt4d($this->summaryInformation, $secOffset + 4 + $offset);
136✔
463
                    $value = substr($this->summaryInformation, $secOffset + 8 + $offset, $byteLength);
136✔
464
                    $value = StringHelper::convertEncoding($value, 'UTF-8', $codePage);
136✔
465
                    $value = rtrim($value);
136✔
466

467
                    break;
136✔
468
                case 0x40: // Filetime (64-bit value representing the number of 100-nanosecond intervals since January 1, 1601)
138✔
469
                    // PHP-time
470
                    $value = OLE::OLE2LocalDate(substr($this->summaryInformation, $secOffset + 4 + $offset, 8));
138✔
471

472
                    break;
138✔
473
                case 0x47: // Clipboard format
2✔
474
                    // not needed yet, fix later if necessary
475
                    break;
×
476
            }
477

478
            switch ($id) {
479
                case 0x01:    //    Code Page
138✔
480
                    $codePage = CodePage::numberToName((int) $value);
138✔
481

482
                    break;
138✔
483
                case 0x02:    //    Title
138✔
484
                    $this->spreadsheet->getProperties()->setTitle("$value");
82✔
485

486
                    break;
82✔
487
                case 0x03:    //    Subject
138✔
488
                    $this->spreadsheet->getProperties()->setSubject("$value");
17✔
489

490
                    break;
17✔
491
                case 0x04:    //    Author (Creator)
138✔
492
                    $this->spreadsheet->getProperties()->setCreator("$value");
124✔
493

494
                    break;
124✔
495
                case 0x05:    //    Keywords
138✔
496
                    $this->spreadsheet->getProperties()->setKeywords("$value");
17✔
497

498
                    break;
17✔
499
                case 0x06:    //    Comments (Description)
138✔
500
                    $this->spreadsheet->getProperties()->setDescription("$value");
17✔
501

502
                    break;
17✔
503
                case 0x07:    //    Template
138✔
504
                    //    Not supported by PhpSpreadsheet
505
                    break;
×
506
                case 0x08:    //    Last Saved By (LastModifiedBy)
138✔
507
                    $this->spreadsheet->getProperties()->setLastModifiedBy("$value");
136✔
508

509
                    break;
136✔
510
                case 0x09:    //    Revision
138✔
511
                    //    Not supported by PhpSpreadsheet
512
                    break;
3✔
513
                case 0x0A:    //    Total Editing Time
138✔
514
                    //    Not supported by PhpSpreadsheet
515
                    break;
3✔
516
                case 0x0B:    //    Last Printed
138✔
517
                    //    Not supported by PhpSpreadsheet
518
                    break;
7✔
519
                case 0x0C:    //    Created Date/Time
138✔
520
                    $this->spreadsheet->getProperties()->setCreated($value);
131✔
521

522
                    break;
131✔
523
                case 0x0D:    //    Modified Date/Time
138✔
524
                    $this->spreadsheet->getProperties()->setModified($value);
137✔
525

526
                    break;
137✔
527
                case 0x0E:    //    Number of Pages
135✔
528
                    //    Not supported by PhpSpreadsheet
529
                    break;
×
530
                case 0x0F:    //    Number of Words
135✔
531
                    //    Not supported by PhpSpreadsheet
532
                    break;
×
533
                case 0x10:    //    Number of Characters
135✔
534
                    //    Not supported by PhpSpreadsheet
535
                    break;
×
536
                case 0x11:    //    Thumbnail
135✔
537
                    //    Not supported by PhpSpreadsheet
538
                    break;
×
539
                case 0x12:    //    Name of creating application
135✔
540
                    //    Not supported by PhpSpreadsheet
541
                    break;
50✔
542
                case 0x13:    //    Security
135✔
543
                    //    Not supported by PhpSpreadsheet
544
                    break;
134✔
545
            }
546
        }
547
    }
548

549
    /**
550
     * Read additional document summary information.
551
     */
552
    protected function readDocumentSummaryInformation(): void
141✔
553
    {
554
        if (!isset($this->documentSummaryInformation)) {
141✔
555
            return;
4✔
556
        }
557

558
        //    offset: 0;    size: 2;    must be 0xFE 0xFF (UTF-16 LE byte order mark)
559
        //    offset: 2;    size: 2;
560
        //    offset: 4;    size: 2;    OS version
561
        //    offset: 6;    size: 2;    OS indicator
562
        //    offset: 8;    size: 16
563
        //    offset: 24;    size: 4;    section count
564
        //$secCount = self::getInt4d($this->documentSummaryInformation, 24);
565

566
        // offset: 28;    size: 16;    first section's class id: 02 d5 cd d5 9c 2e 1b 10 93 97 08 00 2b 2c f9 ae
567
        // offset: 44;    size: 4;    first section offset
568
        $secOffset = self::getInt4d($this->documentSummaryInformation, 44);
137✔
569

570
        //    section header
571
        //    offset: $secOffset;    size: 4;    section length
572
        //$secLength = self::getInt4d($this->documentSummaryInformation, $secOffset);
573

574
        //    offset: $secOffset+4;    size: 4;    property count
575
        $countProperties = self::getInt4d($this->documentSummaryInformation, $secOffset + 4);
137✔
576

577
        // initialize code page (used to resolve string values)
578
        $codePage = 'CP1252';
137✔
579

580
        //    offset: ($secOffset+8);    size: var
581
        //    loop through property decarations and properties
582
        for ($i = 0; $i < $countProperties; ++$i) {
137✔
583
            //    offset: ($secOffset+8) + (8 * $i);    size: 4;    property ID
584
            $id = self::getInt4d($this->documentSummaryInformation, ($secOffset + 8) + (8 * $i));
137✔
585

586
            // Use value of property id as appropriate
587
            // offset: 60 + 8 * $i;    size: 4;    offset from beginning of section (48)
588
            $offset = self::getInt4d($this->documentSummaryInformation, ($secOffset + 12) + (8 * $i));
137✔
589

590
            $type = self::getInt4d($this->documentSummaryInformation, $secOffset + $offset);
137✔
591

592
            // initialize property value
593
            $value = null;
137✔
594

595
            // extract property value based on property type
596
            switch ($type) {
597
                case 0x02:    //    2 byte signed integer
137✔
598
                    $value = self::getUInt2d($this->documentSummaryInformation, $secOffset + 4 + $offset);
137✔
599

600
                    break;
137✔
601
                case 0x03:    //    4 byte signed integer
134✔
602
                    $value = self::getInt4d($this->documentSummaryInformation, $secOffset + 4 + $offset);
131✔
603

604
                    break;
131✔
605
                case 0x0B:  // Boolean
134✔
606
                    $value = self::getUInt2d($this->documentSummaryInformation, $secOffset + 4 + $offset);
134✔
607
                    $value = ($value == 0 ? false : true);
134✔
608

609
                    break;
134✔
610
                case 0x13:    //    4 byte unsigned integer
133✔
611
                    // not needed yet, fix later if necessary
612
                    break;
1✔
613
                case 0x1E:    //    null-terminated string prepended by dword string length
132✔
614
                    $byteLength = self::getInt4d($this->documentSummaryInformation, $secOffset + 4 + $offset);
52✔
615
                    $value = substr($this->documentSummaryInformation, $secOffset + 8 + $offset, $byteLength);
52✔
616
                    $value = StringHelper::convertEncoding($value, 'UTF-8', $codePage);
52✔
617
                    $value = rtrim($value);
52✔
618

619
                    break;
52✔
620
                case 0x40:    //    Filetime (64-bit value representing the number of 100-nanosecond intervals since January 1, 1601)
132✔
621
                    // PHP-Time
622
                    $value = OLE::OLE2LocalDate(substr($this->documentSummaryInformation, $secOffset + 4 + $offset, 8));
×
623

624
                    break;
×
625
                case 0x47:    //    Clipboard format
132✔
626
                    // not needed yet, fix later if necessary
627
                    break;
×
628
            }
629

630
            switch ($id) {
631
                case 0x01:    //    Code Page
137✔
632
                    $codePage = CodePage::numberToName((int) $value);
137✔
633

634
                    break;
137✔
635
                case 0x02:    //    Category
134✔
636
                    $this->spreadsheet->getProperties()->setCategory("$value");
17✔
637

638
                    break;
17✔
639
                case 0x03:    //    Presentation Target
134✔
640
                    //    Not supported by PhpSpreadsheet
641
                    break;
×
642
                case 0x04:    //    Bytes
134✔
643
                    //    Not supported by PhpSpreadsheet
644
                    break;
×
645
                case 0x05:    //    Lines
134✔
646
                    //    Not supported by PhpSpreadsheet
647
                    break;
×
648
                case 0x06:    //    Paragraphs
134✔
649
                    //    Not supported by PhpSpreadsheet
650
                    break;
×
651
                case 0x07:    //    Slides
134✔
652
                    //    Not supported by PhpSpreadsheet
653
                    break;
×
654
                case 0x08:    //    Notes
134✔
655
                    //    Not supported by PhpSpreadsheet
656
                    break;
×
657
                case 0x09:    //    Hidden Slides
134✔
658
                    //    Not supported by PhpSpreadsheet
659
                    break;
×
660
                case 0x0A:    //    MM Clips
134✔
661
                    //    Not supported by PhpSpreadsheet
662
                    break;
×
663
                case 0x0B:    //    Scale Crop
134✔
664
                    //    Not supported by PhpSpreadsheet
665
                    break;
134✔
666
                case 0x0C:    //    Heading Pairs
134✔
667
                    //    Not supported by PhpSpreadsheet
668
                    break;
132✔
669
                case 0x0D:    //    Titles of Parts
134✔
670
                    //    Not supported by PhpSpreadsheet
671
                    break;
132✔
672
                case 0x0E:    //    Manager
134✔
673
                    $this->spreadsheet->getProperties()->setManager("$value");
2✔
674

675
                    break;
2✔
676
                case 0x0F:    //    Company
134✔
677
                    $this->spreadsheet->getProperties()->setCompany("$value");
42✔
678

679
                    break;
42✔
680
                case 0x10:    //    Links up-to-date
134✔
681
                    //    Not supported by PhpSpreadsheet
682
                    break;
134✔
683
            }
684
        }
685
    }
686

687
    /**
688
     * Reads a general type of BIFF record. Does nothing except for moving stream pointer forward to next record.
689
     */
690
    protected function readDefault(): void
152✔
691
    {
692
        $length = self::getUInt2d($this->data, $this->pos + 2);
152✔
693

694
        // move stream pointer to next record
695
        $this->pos += 4 + $length;
152✔
696
    }
697

698
    /**
699
     *    The NOTE record specifies a comment associated with a particular cell. In Excel 95 (BIFF7) and earlier versions,
700
     *        this record stores a note (cell note). This feature was significantly enhanced in Excel 97.
701
     */
702
    protected function readNote(): void
3✔
703
    {
704
        $length = self::getUInt2d($this->data, $this->pos + 2);
3✔
705
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3✔
706

707
        // move stream pointer to next record
708
        $this->pos += 4 + $length;
3✔
709

710
        if ($this->readDataOnly) {
3✔
711
            return;
×
712
        }
713

714
        $cellAddress = Xls\Biff8::readBIFF8CellAddress(substr($recordData, 0, 4));
3✔
715
        if ($this->version == self::XLS_BIFF8) {
3✔
716
            $noteObjID = self::getUInt2d($recordData, 6);
2✔
717
            $noteAuthor = self::readUnicodeStringLong(substr($recordData, 8));
2✔
718
            $noteAuthor = $noteAuthor['value'];
2✔
719
            $this->cellNotes[$noteObjID] = [
2✔
720
                'cellRef' => $cellAddress,
2✔
721
                'objectID' => $noteObjID,
2✔
722
                'author' => $noteAuthor,
2✔
723
            ];
2✔
724
        } else {
725
            $extension = false;
1✔
726
            if ($cellAddress === '$B$' . AddressRange::MAX_ROW_XLS) {
1✔
727
                //    If the address row is -1 and the column is 0, (which translates as $B$65536) then this is a continuation
728
                //        note from the previous cell annotation. We're not yet handling this, so annotations longer than the
729
                //        max 2048 bytes will probably throw a wobbly.
730
                //$row = self::getUInt2d($recordData, 0);
731
                $extension = true;
×
732
                $arrayKeys = array_keys($this->phpSheet->getComments());
×
733
                $cellAddress = array_pop($arrayKeys);
×
734
            }
735

736
            $cellAddress = str_replace('$', '', (string) $cellAddress);
1✔
737
            //$noteLength = self::getUInt2d($recordData, 4);
738
            $noteText = trim(substr($recordData, 6));
1✔
739

740
            if ($extension) {
1✔
741
                //    Concatenate this extension with the currently set comment for the cell
742
                $comment = $this->phpSheet->getComment($cellAddress);
×
743
                $commentText = $comment->getText()->getPlainText();
×
744
                $comment->setText($this->parseRichText($commentText . $noteText));
×
745
            } else {
746
                //    Set comment for the cell
747
                $this->phpSheet->getComment($cellAddress)->setText($this->parseRichText($noteText));
1✔
748
//                                                    ->setAuthor($author)
749
            }
750
        }
751
    }
752

753
    /**
754
     * The TEXT Object record contains the text associated with a cell annotation.
755
     */
756
    protected function readTextObject(): void
2✔
757
    {
758
        $length = self::getUInt2d($this->data, $this->pos + 2);
2✔
759
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2✔
760

761
        // move stream pointer to next record
762
        $this->pos += 4 + $length;
2✔
763

764
        if ($this->readDataOnly) {
2✔
765
            return;
×
766
        }
767

768
        // recordData consists of an array of subrecords looking like this:
769
        //    grbit: 2 bytes; Option Flags
770
        //    rot: 2 bytes; rotation
771
        //    cchText: 2 bytes; length of the text (in the first continue record)
772
        //    cbRuns: 2 bytes; length of the formatting (in the second continue record)
773
        // followed by the continuation records containing the actual text and formatting
774
        $grbitOpts = self::getUInt2d($recordData, 0);
2✔
775
        $rot = self::getUInt2d($recordData, 2);
2✔
776
        //$cchText = self::getUInt2d($recordData, 10);
777
        $cbRuns = self::getUInt2d($recordData, 12);
2✔
778
        $text = $this->getSplicedRecordData();
2✔
779

780
        /** @var int[] */
781
        $tempSplice = $text['spliceOffsets'];
2✔
782
        /** @var int */
783
        $temp = $tempSplice[0];
2✔
784
        /** @var int */
785
        $temp1 = $tempSplice[1];
2✔
786
        $textByte = $temp1 - $temp - 1;
2✔
787
        /** @var string */
788
        $textRecordData = $text['recordData'];
2✔
789
        $textStr = substr($textRecordData, $temp + 1, $textByte);
2✔
790
        // get 1 byte
791
        $is16Bit = ord($textRecordData[0]);
2✔
792
        // it is possible to use a compressed format,
793
        // which omits the high bytes of all characters, if they are all zero
794
        if (($is16Bit & 0x01) === 0) {
2✔
795
            $textStr = StringHelper::ConvertEncoding($textStr, 'UTF-8', 'ISO-8859-1');
2✔
796
        } else {
797
            $textStr = $this->decodeCodepage($textStr);
×
798
        }
799

800
        $this->textObjects[$this->textObjRef] = [
2✔
801
            'text' => $textStr,
2✔
802
            'format' => substr($textRecordData, $tempSplice[1], $cbRuns),
2✔
803
            'alignment' => $grbitOpts,
2✔
804
            'rotation' => $rot,
2✔
805
        ];
2✔
806
    }
807

808
    /**
809
     * Read BOF.
810
     */
811
    protected function readBof(): void
154✔
812
    {
813
        $length = self::getUInt2d($this->data, $this->pos + 2);
154✔
814
        $recordData = substr($this->data, $this->pos + 4, $length);
154✔
815

816
        // move stream pointer to next record
817
        $this->pos += 4 + $length;
154✔
818

819
        // offset: 2; size: 2; type of the following data
820
        $substreamType = self::getUInt2d($recordData, 2);
154✔
821

822
        switch ($substreamType) {
823
            case self::XLS_WORKBOOKGLOBALS:
154✔
824
                $version = self::getUInt2d($recordData, 0);
154✔
825
                if (($version != self::XLS_BIFF8) && ($version != self::XLS_BIFF7)) {
154✔
826
                    throw new Exception('Cannot read this Excel file. Version is too old.');
×
827
                }
828
                $this->version = $version;
154✔
829

830
                break;
154✔
831
            case self::XLS_WORKSHEET:
143✔
832
                // do not use this version information for anything
833
                // it is unreliable (OpenOffice doc, 5.8), use only version information from the global stream
834
                break;
143✔
835
            default:
836
                // substream, e.g. chart
837
                // just skip the entire substream
838
                do {
839
                    $code = self::getUInt2d($this->data, $this->pos);
×
840
                    $this->readDefault();
×
841
                } while ($code != self::XLS_TYPE_EOF && $this->pos < $this->dataSize);
×
842

843
                break;
×
844
        }
845
    }
846

847
    public function setEncryptionPassword(string $encryptionPassword): self
1✔
848
    {
849
        $this->encryptionPassword = $encryptionPassword;
1✔
850

851
        return $this;
1✔
852
    }
853

854
    /**
855
     * FILEPASS.
856
     *
857
     * This record is part of the File Protection Block. It
858
     * contains information about the read/write password of the
859
     * file. All record contents following this record will be
860
     * encrypted.
861
     *
862
     * --    "OpenOffice.org's Documentation of the Microsoft
863
     *         Excel File Format"
864
     *
865
     * The decryption functions and objects used from here on in
866
     * are based on the source of Spreadsheet-ParseExcel:
867
     * https://metacpan.org/release/Spreadsheet-ParseExcel
868
     */
869
    protected function readFilepass(): void
4✔
870
    {
871
        $length = self::getUInt2d($this->data, $this->pos + 2);
4✔
872

873
        if ($length < 54) {
4✔
874
            throw new Exception('Unexpected file pass record length');
×
875
        }
876

877
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4✔
878

879
        // move stream pointer to next record
880
        $this->pos += 4 + $length;
4✔
881

882
        if (substr($recordData, 0, 2) !== "\x01\x00" || substr($recordData, 4, 2) !== "\x01\x00") {
4✔
883
            throw new Exception('Unsupported encryption algorithm');
1✔
884
        }
885
        if (!$this->verifyPassword($this->encryptionPassword, substr($recordData, 6, 16), substr($recordData, 22, 16), substr($recordData, 38, 16), $this->md5Ctxt)) {
3✔
886
            throw new Exception('Decryption password incorrect');
1✔
887
        }
888

889
        $this->encryption = self::MS_BIFF_CRYPTO_RC4;
2✔
890

891
        // Decryption required from the record after next onwards
892
        $this->encryptionStartPos = $this->pos + self::getUInt2d($this->data, $this->pos + 2);
2✔
893
    }
894

895
    /**
896
     * Make an RC4 decryptor for the given block.
897
     *
898
     * @param int $block Block for which to create decrypto
899
     * @param string $valContext MD5 context state
900
     */
901
    private function makeKey(int $block, string $valContext): Xls\RC4
3✔
902
    {
903
        $pwarray = str_repeat("\0", 64);
3✔
904

905
        for ($i = 0; $i < 5; ++$i) {
3✔
906
            $pwarray[$i] = $valContext[$i];
3✔
907
        }
908

909
        $pwarray[5] = chr($block & 0xFF);
3✔
910
        $pwarray[6] = chr(($block >> 8) & 0xFF);
3✔
911
        $pwarray[7] = chr(($block >> 16) & 0xFF);
3✔
912
        $pwarray[8] = chr(($block >> 24) & 0xFF);
3✔
913

914
        $pwarray[9] = "\x80";
3✔
915
        $pwarray[56] = "\x48";
3✔
916

917
        $md5 = new Xls\MD5();
3✔
918
        $md5->add($pwarray);
3✔
919

920
        $s = $md5->getContext();
3✔
921

922
        return new Xls\RC4($s);
3✔
923
    }
924

925
    /**
926
     * Verify RC4 file password.
927
     *
928
     * @param string $password Password to check
929
     * @param string $docid Document id
930
     * @param string $salt_data Salt data
931
     * @param string $hashedsalt_data Hashed salt data
932
     * @param string $valContext Set to the MD5 context of the value
933
     *
934
     * @return bool Success
935
     */
936
    private function verifyPassword(string $password, string $docid, string $salt_data, string $hashedsalt_data, string &$valContext): bool
3✔
937
    {
938
        $pwarray = str_repeat("\0", 64);
3✔
939

940
        $iMax = strlen($password);
3✔
941
        for ($i = 0; $i < $iMax; ++$i) {
3✔
942
            $o = ord(substr($password, $i, 1));
3✔
943
            $pwarray[2 * $i] = chr($o & 0xFF);
3✔
944
            $pwarray[2 * $i + 1] = chr(($o >> 8) & 0xFF);
3✔
945
        }
946
        $pwarray[2 * $i] = chr(0x80);
3✔
947
        $pwarray[56] = chr(($i << 4) & 0xFF);
3✔
948

949
        $md5 = new Xls\MD5();
3✔
950
        $md5->add($pwarray);
3✔
951

952
        $mdContext1 = $md5->getContext();
3✔
953

954
        $offset = 0;
3✔
955
        $keyoffset = 0;
3✔
956
        $tocopy = 5;
3✔
957

958
        $md5->reset();
3✔
959

960
        while ($offset != 16) {
3✔
961
            if ((64 - $offset) < 5) {
3✔
962
                $tocopy = 64 - $offset;
3✔
963
            }
964
            for ($i = 0; $i <= $tocopy; ++$i) {
3✔
965
                $pwarray[$offset + $i] = $mdContext1[$keyoffset + $i];
3✔
966
            }
967
            $offset += $tocopy;
3✔
968

969
            if ($offset == 64) {
3✔
970
                $md5->add($pwarray);
3✔
971
                $keyoffset = $tocopy;
3✔
972
                $tocopy = 5 - $tocopy;
3✔
973
                $offset = 0;
3✔
974

975
                continue;
3✔
976
            }
977

978
            $keyoffset = 0;
3✔
979
            $tocopy = 5;
3✔
980
            for ($i = 0; $i < 16; ++$i) {
3✔
981
                $pwarray[$offset + $i] = $docid[$i];
3✔
982
            }
983
            $offset += 16;
3✔
984
        }
985

986
        $pwarray[16] = "\x80";
3✔
987
        for ($i = 0; $i < 47; ++$i) {
3✔
988
            $pwarray[17 + $i] = "\0";
3✔
989
        }
990
        $pwarray[56] = "\x80";
3✔
991
        $pwarray[57] = "\x0a";
3✔
992

993
        $md5->add($pwarray);
3✔
994
        $valContext = $md5->getContext();
3✔
995

996
        $key = $this->makeKey(0, $valContext);
3✔
997

998
        $salt = $key->RC4($salt_data);
3✔
999
        $hashedsalt = $key->RC4($hashedsalt_data);
3✔
1000

1001
        $salt .= "\x80" . str_repeat("\0", 47);
3✔
1002
        $salt[56] = "\x80";
3✔
1003

1004
        $md5->reset();
3✔
1005
        $md5->add($salt);
3✔
1006
        $mdContext2 = $md5->getContext();
3✔
1007

1008
        return $mdContext2 == $hashedsalt;
3✔
1009
    }
1010

1011
    /**
1012
     * CODEPAGE.
1013
     *
1014
     * This record stores the text encoding used to write byte
1015
     * strings, stored as MS Windows code page identifier.
1016
     *
1017
     * --    "OpenOffice.org's Documentation of the Microsoft
1018
     *         Excel File Format"
1019
     */
1020
    protected function readCodepage(): void
148✔
1021
    {
1022
        $length = self::getUInt2d($this->data, $this->pos + 2);
148✔
1023
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
148✔
1024

1025
        // move stream pointer to next record
1026
        $this->pos += 4 + $length;
148✔
1027

1028
        // offset: 0; size: 2; code page identifier
1029
        $codepage = self::getUInt2d($recordData, 0);
148✔
1030

1031
        $this->codepage = CodePage::numberToName($codepage);
148✔
1032
    }
1033

1034
    /**
1035
     * DATEMODE.
1036
     *
1037
     * This record specifies the base date for displaying date
1038
     * values. All dates are stored as count of days past this
1039
     * base date. In BIFF2-BIFF4 this record is part of the
1040
     * Calculation Settings Block. In BIFF5-BIFF8 it is
1041
     * stored in the Workbook Globals Substream.
1042
     *
1043
     * --    "OpenOffice.org's Documentation of the Microsoft
1044
     *         Excel File Format"
1045
     */
1046
    protected function readDateMode(): void
138✔
1047
    {
1048
        $length = self::getUInt2d($this->data, $this->pos + 2);
138✔
1049
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
138✔
1050

1051
        // move stream pointer to next record
1052
        $this->pos += 4 + $length;
138✔
1053

1054
        // offset: 0; size: 2; 0 = base 1900, 1 = base 1904
1055
        Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900);
138✔
1056
        $this->spreadsheet->setExcelCalendar(Date::CALENDAR_WINDOWS_1900);
138✔
1057
        if (ord($recordData[0]) == 1) {
138✔
1058
            Date::setExcelCalendar(Date::CALENDAR_MAC_1904);
4✔
1059
            $this->spreadsheet->setExcelCalendar(Date::CALENDAR_MAC_1904);
4✔
1060
        }
1061
    }
1062

1063
    /**
1064
     * Read a FONT record.
1065
     */
1066
    protected function readFont(): void
138✔
1067
    {
1068
        $length = self::getUInt2d($this->data, $this->pos + 2);
138✔
1069
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
138✔
1070

1071
        // move stream pointer to next record
1072
        $this->pos += 4 + $length;
138✔
1073

1074
        if (!$this->readDataOnly) {
138✔
1075
            $objFont = new Font();
136✔
1076

1077
            // offset: 0; size: 2; height of the font (in twips = 1/20 of a point)
1078
            $size = self::getUInt2d($recordData, 0);
136✔
1079
            $objFont->setSize($size / 20);
136✔
1080

1081
            // offset: 2; size: 2; option flags
1082
            // bit: 0; mask 0x0001; bold (redundant in BIFF5-BIFF8)
1083
            // bit: 1; mask 0x0002; italic
1084
            $isItalic = (0x0002 & self::getUInt2d($recordData, 2)) >> 1;
136✔
1085
            if ($isItalic) {
136✔
1086
                $objFont->setItalic(true);
58✔
1087
            }
1088

1089
            // bit: 2; mask 0x0004; underlined (redundant in BIFF5-BIFF8)
1090
            // bit: 3; mask 0x0008; strikethrough
1091
            $isStrike = (0x0008 & self::getUInt2d($recordData, 2)) >> 3;
136✔
1092
            if ($isStrike) {
136✔
1093
                $objFont->setStrikethrough(true);
×
1094
            }
1095

1096
            // offset: 4; size: 2; colour index
1097
            $colorIndex = self::getUInt2d($recordData, 4);
136✔
1098
            $objFont->colorIndex = $colorIndex;
136✔
1099

1100
            // offset: 6; size: 2; font weight
1101
            $weight = self::getUInt2d($recordData, 6); // regular=400 bold=700
136✔
1102
            if ($weight >= 550) {
136✔
1103
                $objFont->setBold(true);
69✔
1104
            }
1105

1106
            // offset: 8; size: 2; escapement type
1107
            $escapement = self::getUInt2d($recordData, 8);
136✔
1108
            CellFont::escapement($objFont, $escapement);
136✔
1109

1110
            // offset: 10; size: 1; underline type
1111
            $underlineType = ord($recordData[10]);
136✔
1112
            CellFont::underline($objFont, $underlineType);
136✔
1113

1114
            // offset: 11; size: 1; font family
1115
            // offset: 12; size: 1; character set
1116
            // offset: 13; size: 1; not used
1117
            // offset: 14; size: var; font name
1118
            if ($this->version == self::XLS_BIFF8) {
136✔
1119
                $string = self::readUnicodeStringShort(substr($recordData, 14));
134✔
1120
            } else {
1121
                $string = $this->readByteStringShort(substr($recordData, 14));
2✔
1122
            }
1123
            /** @var string[] $string */
1124
            $objFont->setName($string['value']);
136✔
1125

1126
            $this->objFonts[] = $objFont;
136✔
1127
        }
1128
    }
1129

1130
    /**
1131
     * FORMAT.
1132
     *
1133
     * This record contains information about a number format.
1134
     * All FORMAT records occur together in a sequential list.
1135
     *
1136
     * In BIFF2-BIFF4 other records referencing a FORMAT record
1137
     * contain a zero-based index into this list. From BIFF5 on
1138
     * the FORMAT record contains the index itself that will be
1139
     * used by other records.
1140
     *
1141
     * --    "OpenOffice.org's Documentation of the Microsoft
1142
     *         Excel File Format"
1143
     */
1144
    protected function readFormat(): void
71✔
1145
    {
1146
        $length = self::getUInt2d($this->data, $this->pos + 2);
71✔
1147
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
71✔
1148

1149
        // move stream pointer to next record
1150
        $this->pos += 4 + $length;
71✔
1151

1152
        if (!$this->readDataOnly) {
71✔
1153
            $indexCode = self::getUInt2d($recordData, 0);
69✔
1154

1155
            if ($this->version == self::XLS_BIFF8) {
69✔
1156
                $string = self::readUnicodeStringLong(substr($recordData, 2));
67✔
1157
            } else {
1158
                // BIFF7
1159
                $string = $this->readByteStringShort(substr($recordData, 2));
2✔
1160
            }
1161

1162
            $formatString = $string['value'];
69✔
1163
            // Apache Open Office sets wrong case writing to xls - issue 2239
1164
            if ($formatString === 'GENERAL') {
69✔
1165
                $formatString = NumberFormat::FORMAT_GENERAL;
1✔
1166
            }
1167
            $this->formats[$indexCode] = $formatString;
69✔
1168
        }
1169
    }
1170

1171
    /**
1172
     * XF - Extended Format.
1173
     *
1174
     * This record contains formatting information for cells, rows, columns or styles.
1175
     * According to https://support.microsoft.com/en-us/help/147732 there are always at least 15 cell style XF
1176
     * and 1 cell XF.
1177
     * Inspection of Excel files generated by MS Office Excel shows that XF records 0-14 are cell style XF
1178
     * and XF record 15 is a cell XF
1179
     * We only read the first cell style XF and skip the remaining cell style XF records
1180
     * We read all cell XF records.
1181
     *
1182
     * --    "OpenOffice.org's Documentation of the Microsoft
1183
     *         Excel File Format"
1184
     */
1185
    protected function readXf(): void
139✔
1186
    {
1187
        $length = self::getUInt2d($this->data, $this->pos + 2);
139✔
1188
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
139✔
1189

1190
        // move stream pointer to next record
1191
        $this->pos += 4 + $length;
139✔
1192

1193
        $objStyle = new Style();
139✔
1194

1195
        if (!$this->readDataOnly) {
139✔
1196
            // offset:  0; size: 2; Index to FONT record
1197
            if (self::getUInt2d($recordData, 0) < 4) {
137✔
1198
                $fontIndex = self::getUInt2d($recordData, 0);
137✔
1199
            } else {
1200
                // this has to do with that index 4 is omitted in all BIFF versions for some strange reason
1201
                // check the OpenOffice documentation of the FONT record
1202
                $fontIndex = self::getUInt2d($recordData, 0) - 1;
63✔
1203
            }
1204
            if (isset($this->objFonts[$fontIndex])) {
137✔
1205
                $objStyle->setFont($this->objFonts[$fontIndex]);
136✔
1206
            }
1207

1208
            // offset:  2; size: 2; Index to FORMAT record
1209
            $numberFormatIndex = self::getUInt2d($recordData, 2);
137✔
1210
            if (isset($this->formats[$numberFormatIndex])) {
137✔
1211
                // then we have user-defined format code
1212
                $numberFormat = ['formatCode' => $this->formats[$numberFormatIndex]];
63✔
1213
            } elseif (($code = NumberFormat::builtInFormatCode($numberFormatIndex)) !== '') {
137✔
1214
                // then we have built-in format code
1215
                $numberFormat = ['formatCode' => $code];
137✔
1216
            } else {
1217
                // we set the general format code
1218
                $numberFormat = ['formatCode' => NumberFormat::FORMAT_GENERAL];
4✔
1219
            }
1220
            /** @var string[] $numberFormat */
1221
            $objStyle->getNumberFormat()
137✔
1222
                ->setFormatCode($numberFormat['formatCode']);
137✔
1223

1224
            // offset:  4; size: 2; XF type, cell protection, and parent style XF
1225
            // bit 2-0; mask 0x0007; XF_TYPE_PROT
1226
            $xfTypeProt = self::getUInt2d($recordData, 4);
137✔
1227
            // bit 0; mask 0x01; 1 = cell is locked
1228
            $isLocked = (0x01 & $xfTypeProt) >> 0;
137✔
1229
            $objStyle->getProtection()->setLocked($isLocked ? Protection::PROTECTION_INHERIT : Protection::PROTECTION_UNPROTECTED);
137✔
1230

1231
            // bit 1; mask 0x02; 1 = Formula is hidden
1232
            $isHidden = (0x02 & $xfTypeProt) >> 1;
137✔
1233
            $objStyle->getProtection()->setHidden($isHidden ? Protection::PROTECTION_PROTECTED : Protection::PROTECTION_UNPROTECTED);
137✔
1234

1235
            // bit 2; mask 0x04; 0 = Cell XF, 1 = Cell Style XF
1236
            $isCellStyleXf = (0x04 & $xfTypeProt) >> 2;
137✔
1237

1238
            // offset:  6; size: 1; Alignment and text break
1239
            // bit 2-0, mask 0x07; horizontal alignment
1240
            $horAlign = (0x07 & ord($recordData[6])) >> 0;
137✔
1241
            Xls\Style\CellAlignment::horizontal($objStyle->getAlignment(), $horAlign);
137✔
1242

1243
            // bit 3, mask 0x08; wrap text
1244
            $wrapText = (0x08 & ord($recordData[6])) >> 3;
137✔
1245
            Xls\Style\CellAlignment::wrap($objStyle->getAlignment(), $wrapText);
137✔
1246

1247
            // bit 6-4, mask 0x70; vertical alignment
1248
            $vertAlign = (0x70 & ord($recordData[6])) >> 4;
137✔
1249
            Xls\Style\CellAlignment::vertical($objStyle->getAlignment(), $vertAlign);
137✔
1250

1251
            if ($this->version == self::XLS_BIFF8) {
137✔
1252
                // offset:  7; size: 1; XF_ROTATION: Text rotation angle
1253
                $angle = ord($recordData[7]);
135✔
1254
                $rotation = 0;
135✔
1255
                if ($angle <= 90) {
135✔
1256
                    $rotation = $angle;
135✔
1257
                } elseif ($angle <= 180) {
4✔
1258
                    $rotation = 90 - $angle;
×
1259
                } elseif ($angle == Alignment::TEXTROTATION_STACK_EXCEL) {
4✔
1260
                    $rotation = Alignment::TEXTROTATION_STACK_PHPSPREADSHEET;
4✔
1261
                }
1262
                $objStyle->getAlignment()->setTextRotation($rotation);
135✔
1263

1264
                // offset:  8; size: 1; Indentation, shrink to cell size, and text direction
1265
                // bit: 3-0; mask: 0x0F; indent level
1266
                $indent = (0x0F & ord($recordData[8])) >> 0;
135✔
1267
                $objStyle->getAlignment()->setIndent($indent);
135✔
1268

1269
                // bit: 4; mask: 0x10; 1 = shrink content to fit into cell
1270
                $shrinkToFit = (0x10 & ord($recordData[8])) >> 4;
135✔
1271
                switch ($shrinkToFit) {
1272
                    case 0:
135✔
1273
                        $objStyle->getAlignment()->setShrinkToFit(false);
135✔
1274

1275
                        break;
135✔
1276
                    case 1:
1✔
1277
                        $objStyle->getAlignment()->setShrinkToFit(true);
1✔
1278

1279
                        break;
1✔
1280
                }
1281
                $readOrder = (0xC0 & ord($recordData[8])) >> 6;
135✔
1282
                $objStyle->getAlignment()->setReadOrder($readOrder);
135✔
1283

1284
                // offset:  9; size: 1; Flags used for attribute groups
1285

1286
                // offset: 10; size: 4; Cell border lines and background area
1287
                // bit: 3-0; mask: 0x0000000F; left style
1288
                if ($bordersLeftStyle = Xls\Style\Border::lookup((0x0000000F & self::getInt4d($recordData, 10)) >> 0)) {
135✔
1289
                    $objStyle->getBorders()->getLeft()->setBorderStyle($bordersLeftStyle);
135✔
1290
                }
1291
                // bit: 7-4; mask: 0x000000F0; right style
1292
                if ($bordersRightStyle = Xls\Style\Border::lookup((0x000000F0 & self::getInt4d($recordData, 10)) >> 4)) {
135✔
1293
                    $objStyle->getBorders()->getRight()->setBorderStyle($bordersRightStyle);
135✔
1294
                }
1295
                // bit: 11-8; mask: 0x00000F00; top style
1296
                if ($bordersTopStyle = Xls\Style\Border::lookup((0x00000F00 & self::getInt4d($recordData, 10)) >> 8)) {
135✔
1297
                    $objStyle->getBorders()->getTop()->setBorderStyle($bordersTopStyle);
135✔
1298
                }
1299
                // bit: 15-12; mask: 0x0000F000; bottom style
1300
                if ($bordersBottomStyle = Xls\Style\Border::lookup((0x0000F000 & self::getInt4d($recordData, 10)) >> 12)) {
135✔
1301
                    $objStyle->getBorders()->getBottom()->setBorderStyle($bordersBottomStyle);
135✔
1302
                }
1303
                // bit: 22-16; mask: 0x007F0000; left color
1304
                $objStyle->getBorders()->getLeft()->colorIndex = (0x007F0000 & self::getInt4d($recordData, 10)) >> 16;
135✔
1305

1306
                // bit: 29-23; mask: 0x3F800000; right color
1307
                $objStyle->getBorders()->getRight()->colorIndex = (0x3F800000 & self::getInt4d($recordData, 10)) >> 23;
135✔
1308

1309
                // bit: 30; mask: 0x40000000; 1 = diagonal line from top left to right bottom
1310
                $diagonalDown = (0x40000000 & self::getInt4d($recordData, 10)) >> 30 ? true : false;
135✔
1311

1312
                // bit: 31; mask: 0x800000; 1 = diagonal line from bottom left to top right
1313
                $diagonalUp = (self::HIGH_ORDER_BIT & self::getInt4d($recordData, 10)) >> 31 ? true : false;
135✔
1314

1315
                if ($diagonalUp === false) {
135✔
1316
                    if ($diagonalDown === false) {
135✔
1317
                        $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_NONE);
135✔
1318
                    } else {
1319
                        $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_DOWN);
1✔
1320
                    }
1321
                } elseif ($diagonalDown === false) {
1✔
1322
                    $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_UP);
1✔
1323
                } else {
1324
                    $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_BOTH);
1✔
1325
                }
1326

1327
                // offset: 14; size: 4;
1328
                // bit: 6-0; mask: 0x0000007F; top color
1329
                $objStyle->getBorders()->getTop()->colorIndex = (0x0000007F & self::getInt4d($recordData, 14)) >> 0;
135✔
1330

1331
                // bit: 13-7; mask: 0x00003F80; bottom color
1332
                $objStyle->getBorders()->getBottom()->colorIndex = (0x00003F80 & self::getInt4d($recordData, 14)) >> 7;
135✔
1333

1334
                // bit: 20-14; mask: 0x001FC000; diagonal color
1335
                $objStyle->getBorders()->getDiagonal()->colorIndex = (0x001FC000 & self::getInt4d($recordData, 14)) >> 14;
135✔
1336

1337
                // bit: 24-21; mask: 0x01E00000; diagonal style
1338
                if ($bordersDiagonalStyle = Xls\Style\Border::lookup((0x01E00000 & self::getInt4d($recordData, 14)) >> 21)) {
135✔
1339
                    $objStyle->getBorders()->getDiagonal()->setBorderStyle($bordersDiagonalStyle);
135✔
1340
                }
1341

1342
                // bit: 31-26; mask: 0xFC000000 fill pattern
1343
                if ($fillType = FillPattern::lookup((self::FC000000 & self::getInt4d($recordData, 14)) >> 26)) {
135✔
1344
                    $objStyle->getFill()->setFillType($fillType);
135✔
1345
                }
1346
                // offset: 18; size: 2; pattern and background colour
1347
                // bit: 6-0; mask: 0x007F; color index for pattern color
1348
                $objStyle->getFill()->startcolorIndex = (0x007F & self::getUInt2d($recordData, 18)) >> 0;
135✔
1349

1350
                // bit: 13-7; mask: 0x3F80; color index for pattern background
1351
                $objStyle->getFill()->endcolorIndex = (0x3F80 & self::getUInt2d($recordData, 18)) >> 7;
135✔
1352
            } else {
1353
                // BIFF5
1354

1355
                // offset: 7; size: 1; Text orientation and flags
1356
                $orientationAndFlags = ord($recordData[7]);
2✔
1357

1358
                // bit: 1-0; mask: 0x03; XF_ORIENTATION: Text orientation
1359
                $xfOrientation = (0x03 & $orientationAndFlags) >> 0;
2✔
1360
                switch ($xfOrientation) {
1361
                    case 0:
2✔
1362
                        $objStyle->getAlignment()->setTextRotation(0);
2✔
1363

1364
                        break;
2✔
1365
                    case 1:
1✔
1366
                        $objStyle->getAlignment()->setTextRotation(Alignment::TEXTROTATION_STACK_PHPSPREADSHEET);
1✔
1367

1368
                        break;
1✔
1369
                    case 2:
×
1370
                        $objStyle->getAlignment()->setTextRotation(90);
×
1371

1372
                        break;
×
1373
                    case 3:
×
1374
                        $objStyle->getAlignment()->setTextRotation(-90);
×
1375

1376
                        break;
×
1377
                }
1378

1379
                // offset: 8; size: 4; cell border lines and background area
1380
                $borderAndBackground = self::getInt4d($recordData, 8);
2✔
1381

1382
                // bit: 6-0; mask: 0x0000007F; color index for pattern color
1383
                $objStyle->getFill()->startcolorIndex = (0x0000007F & $borderAndBackground) >> 0;
2✔
1384

1385
                // bit: 13-7; mask: 0x00003F80; color index for pattern background
1386
                $objStyle->getFill()->endcolorIndex = (0x00003F80 & $borderAndBackground) >> 7;
2✔
1387

1388
                // bit: 21-16; mask: 0x003F0000; fill pattern
1389
                $objStyle->getFill()->setFillType(FillPattern::lookup((0x003F0000 & $borderAndBackground) >> 16));
2✔
1390

1391
                // bit: 24-22; mask: 0x01C00000; bottom line style
1392
                $objStyle->getBorders()->getBottom()->setBorderStyle(Xls\Style\Border::lookup((0x01C00000 & $borderAndBackground) >> 22));
2✔
1393

1394
                // bit: 31-25; mask: 0xFE000000; bottom line color
1395
                $objStyle->getBorders()->getBottom()->colorIndex = (self::FE000000 & $borderAndBackground) >> 25;
2✔
1396

1397
                // offset: 12; size: 4; cell border lines
1398
                $borderLines = self::getInt4d($recordData, 12);
2✔
1399

1400
                // bit: 2-0; mask: 0x00000007; top line style
1401
                $objStyle->getBorders()->getTop()->setBorderStyle(Xls\Style\Border::lookup((0x00000007 & $borderLines) >> 0));
2✔
1402

1403
                // bit: 5-3; mask: 0x00000038; left line style
1404
                $objStyle->getBorders()->getLeft()->setBorderStyle(Xls\Style\Border::lookup((0x00000038 & $borderLines) >> 3));
2✔
1405

1406
                // bit: 8-6; mask: 0x000001C0; right line style
1407
                $objStyle->getBorders()->getRight()->setBorderStyle(Xls\Style\Border::lookup((0x000001C0 & $borderLines) >> 6));
2✔
1408

1409
                // bit: 15-9; mask: 0x0000FE00; top line color index
1410
                $objStyle->getBorders()->getTop()->colorIndex = (0x0000FE00 & $borderLines) >> 9;
2✔
1411

1412
                // bit: 22-16; mask: 0x007F0000; left line color index
1413
                $objStyle->getBorders()->getLeft()->colorIndex = (0x007F0000 & $borderLines) >> 16;
2✔
1414

1415
                // bit: 29-23; mask: 0x3F800000; right line color index
1416
                $objStyle->getBorders()->getRight()->colorIndex = (0x3F800000 & $borderLines) >> 23;
2✔
1417
            }
1418

1419
            // add cellStyleXf or cellXf and update mapping
1420
            if ($isCellStyleXf) {
137✔
1421
                // we only read one style XF record which is always the first
1422
                if ($this->xfIndex == 0) {
137✔
1423
                    $this->spreadsheet->addCellStyleXf($objStyle);
137✔
1424
                    $this->mapCellStyleXfIndex[$this->xfIndex] = 0;
137✔
1425
                }
1426
            } else {
1427
                // we read all cell XF records
1428
                $this->spreadsheet->addCellXf($objStyle);
137✔
1429
                $this->mapCellXfIndex[$this->xfIndex] = count($this->spreadsheet->getCellXfCollection()) - 1;
137✔
1430
            }
1431

1432
            // update XF index for when we read next record
1433
            ++$this->xfIndex;
137✔
1434
        }
1435
    }
1436

1437
    protected function readXfExt(): void
54✔
1438
    {
1439
        $length = self::getUInt2d($this->data, $this->pos + 2);
54✔
1440
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
54✔
1441

1442
        // move stream pointer to next record
1443
        $this->pos += 4 + $length;
54✔
1444

1445
        if (!$this->readDataOnly) {
54✔
1446
            // offset: 0; size: 2; 0x087D = repeated header
1447

1448
            // offset: 2; size: 2
1449

1450
            // offset: 4; size: 8; not used
1451

1452
            // offset: 12; size: 2; record version
1453

1454
            // offset: 14; size: 2; index to XF record which this record modifies
1455
            $ixfe = self::getUInt2d($recordData, 14);
52✔
1456

1457
            // offset: 16; size: 2; not used
1458

1459
            // offset: 18; size: 2; number of extension properties that follow
1460
            //$cexts = self::getUInt2d($recordData, 18);
1461

1462
            // start reading the actual extension data
1463
            $offset = 20;
52✔
1464
            while ($offset < $length) {
52✔
1465
                // extension type
1466
                $extType = self::getUInt2d($recordData, $offset);
52✔
1467

1468
                // extension length
1469
                $cb = self::getUInt2d($recordData, $offset + 2);
52✔
1470

1471
                // extension data
1472
                $extData = substr($recordData, $offset + 4, $cb);
52✔
1473

1474
                switch ($extType) {
1475
                    case 4:        // fill start color
52✔
1476
                        $xclfType = self::getUInt2d($extData, 0); // color type
52✔
1477
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
52✔
1478

1479
                        if ($xclfType == 2) {
52✔
1480
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
50✔
1481

1482
                            // modify the relevant style property
1483
                            if (isset($this->mapCellXfIndex[$ixfe])) {
50✔
1484
                                $fill = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getFill();
7✔
1485
                                $fill->getStartColor()->setRGB($rgb);
7✔
1486
                                $fill->startcolorIndex = null; // normal color index does not apply, discard
7✔
1487
                            }
1488
                        }
1489

1490
                        break;
52✔
1491
                    case 5:        // fill end color
50✔
1492
                        $xclfType = self::getUInt2d($extData, 0); // color type
5✔
1493
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
5✔
1494

1495
                        if ($xclfType == 2) {
5✔
1496
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
5✔
1497

1498
                            // modify the relevant style property
1499
                            if (isset($this->mapCellXfIndex[$ixfe])) {
5✔
1500
                                $fill = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getFill();
5✔
1501
                                $fill->getEndColor()->setRGB($rgb);
5✔
1502
                                $fill->endcolorIndex = null; // normal color index does not apply, discard
5✔
1503
                            }
1504
                        }
1505

1506
                        break;
5✔
1507
                    case 7:        // border color top
50✔
1508
                        $xclfType = self::getUInt2d($extData, 0); // color type
50✔
1509
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
50✔
1510

1511
                        if ($xclfType == 2) {
50✔
1512
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
50✔
1513

1514
                            // modify the relevant style property
1515
                            if (isset($this->mapCellXfIndex[$ixfe])) {
50✔
1516
                                $top = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getTop();
4✔
1517
                                $top->getColor()->setRGB($rgb);
4✔
1518
                                $top->colorIndex = null; // normal color index does not apply, discard
4✔
1519
                            }
1520
                        }
1521

1522
                        break;
50✔
1523
                    case 8:        // border color bottom
50✔
1524
                        $xclfType = self::getUInt2d($extData, 0); // color type
50✔
1525
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
50✔
1526

1527
                        if ($xclfType == 2) {
50✔
1528
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
50✔
1529

1530
                            // modify the relevant style property
1531
                            if (isset($this->mapCellXfIndex[$ixfe])) {
50✔
1532
                                $bottom = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getBottom();
5✔
1533
                                $bottom->getColor()->setRGB($rgb);
5✔
1534
                                $bottom->colorIndex = null; // normal color index does not apply, discard
5✔
1535
                            }
1536
                        }
1537

1538
                        break;
50✔
1539
                    case 9:        // border color left
50✔
1540
                        $xclfType = self::getUInt2d($extData, 0); // color type
50✔
1541
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
50✔
1542

1543
                        if ($xclfType == 2) {
50✔
1544
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
50✔
1545

1546
                            // modify the relevant style property
1547
                            if (isset($this->mapCellXfIndex[$ixfe])) {
50✔
1548
                                $left = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getLeft();
4✔
1549
                                $left->getColor()->setRGB($rgb);
4✔
1550
                                $left->colorIndex = null; // normal color index does not apply, discard
4✔
1551
                            }
1552
                        }
1553

1554
                        break;
50✔
1555
                    case 10:        // border color right
50✔
1556
                        $xclfType = self::getUInt2d($extData, 0); // color type
50✔
1557
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
50✔
1558

1559
                        if ($xclfType == 2) {
50✔
1560
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
50✔
1561

1562
                            // modify the relevant style property
1563
                            if (isset($this->mapCellXfIndex[$ixfe])) {
50✔
1564
                                $right = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getRight();
4✔
1565
                                $right->getColor()->setRGB($rgb);
4✔
1566
                                $right->colorIndex = null; // normal color index does not apply, discard
4✔
1567
                            }
1568
                        }
1569

1570
                        break;
50✔
1571
                    case 11:        // border color diagonal
50✔
1572
                        $xclfType = self::getUInt2d($extData, 0); // color type
×
1573
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
×
1574

1575
                        if ($xclfType == 2) {
×
1576
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
×
1577

1578
                            // modify the relevant style property
1579
                            if (isset($this->mapCellXfIndex[$ixfe])) {
×
1580
                                $diagonal = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getDiagonal();
×
1581
                                $diagonal->getColor()->setRGB($rgb);
×
1582
                                $diagonal->colorIndex = null; // normal color index does not apply, discard
×
1583
                            }
1584
                        }
1585

1586
                        break;
×
1587
                    case 13:    // font color
50✔
1588
                        $xclfType = self::getUInt2d($extData, 0); // color type
50✔
1589
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
50✔
1590

1591
                        if ($xclfType == 2) {
50✔
1592
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
50✔
1593

1594
                            // modify the relevant style property
1595
                            if (isset($this->mapCellXfIndex[$ixfe])) {
50✔
1596
                                $font = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getFont();
9✔
1597
                                $font->getColor()->setRGB($rgb);
9✔
1598
                                $font->colorIndex = null; // normal color index does not apply, discard
9✔
1599
                            }
1600
                        }
1601

1602
                        break;
50✔
1603
                }
1604

1605
                $offset += $cb;
52✔
1606
            }
1607
        }
1608
    }
1609

1610
    /**
1611
     * Read STYLE record.
1612
     */
1613
    protected function readStyle(): void
139✔
1614
    {
1615
        $length = self::getUInt2d($this->data, $this->pos + 2);
139✔
1616
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
139✔
1617

1618
        // move stream pointer to next record
1619
        $this->pos += 4 + $length;
139✔
1620

1621
        if (!$this->readDataOnly) {
139✔
1622
            // offset: 0; size: 2; index to XF record and flag for built-in style
1623
            $ixfe = self::getUInt2d($recordData, 0);
137✔
1624

1625
            // bit: 11-0; mask 0x0FFF; index to XF record
1626
            //$xfIndex = (0x0FFF & $ixfe) >> 0;
1627

1628
            // bit: 15; mask 0x8000; 0 = user-defined style, 1 = built-in style
1629
            $isBuiltIn = (bool) ((0x8000 & $ixfe) >> 15);
137✔
1630

1631
            if ($isBuiltIn) {
137✔
1632
                // offset: 2; size: 1; identifier for built-in style
1633
                $builtInId = ord($recordData[2]);
137✔
1634

1635
                switch ($builtInId) {
1636
                    case 0x00:
137✔
1637
                        // currently, we are not using this for anything
1638
                        break;
137✔
1639
                    default:
1640
                        break;
60✔
1641
                }
1642
            }
1643
            // user-defined; not supported by PhpSpreadsheet
1644
        }
1645
    }
1646

1647
    /**
1648
     * Read PALETTE record.
1649
     */
1650
    protected function readPalette(): void
93✔
1651
    {
1652
        $length = self::getUInt2d($this->data, $this->pos + 2);
93✔
1653
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
93✔
1654

1655
        // move stream pointer to next record
1656
        $this->pos += 4 + $length;
93✔
1657

1658
        if (!$this->readDataOnly) {
93✔
1659
            // offset: 0; size: 2; number of following colors
1660
            $nm = self::getUInt2d($recordData, 0);
93✔
1661

1662
            // list of RGB colors
1663
            for ($i = 0; $i < $nm; ++$i) {
93✔
1664
                $rgb = substr($recordData, 2 + 4 * $i, 4);
93✔
1665
                $this->palette[] = self::readRGB($rgb);
93✔
1666
            }
1667
        }
1668
    }
1669

1670
    /**
1671
     * SHEET.
1672
     *
1673
     * This record is  located in the  Workbook Globals
1674
     * Substream  and represents a sheet inside the workbook.
1675
     * One SHEET record is written for each sheet. It stores the
1676
     * sheet name and a stream offset to the BOF record of the
1677
     * respective Sheet Substream within the Workbook Stream.
1678
     *
1679
     * --    "OpenOffice.org's Documentation of the Microsoft
1680
     *         Excel File Format"
1681
     */
1682
    protected function readSheet(): void
152✔
1683
    {
1684
        $length = self::getUInt2d($this->data, $this->pos + 2);
152✔
1685
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
152✔
1686

1687
        // offset: 0; size: 4; absolute stream position of the BOF record of the sheet
1688
        // NOTE: not encrypted
1689
        $rec_offset = self::getInt4d($this->data, $this->pos + 4);
152✔
1690

1691
        // move stream pointer to next record
1692
        $this->pos += 4 + $length;
152✔
1693

1694
        // offset: 4; size: 1; sheet state
1695
        $sheetState = match (ord($recordData[4])) {
152✔
1696
            0x00 => Worksheet::SHEETSTATE_VISIBLE,
152✔
1697
            0x01 => Worksheet::SHEETSTATE_HIDDEN,
6✔
1698
            0x02 => Worksheet::SHEETSTATE_VERYHIDDEN,
2✔
1699
            default => Worksheet::SHEETSTATE_VISIBLE,
×
1700
        };
152✔
1701

1702
        // offset: 5; size: 1; sheet type
1703
        $sheetType = ord($recordData[5]);
152✔
1704

1705
        // offset: 6; size: var; sheet name
1706
        $rec_name = null;
152✔
1707
        if ($this->version == self::XLS_BIFF8) {
152✔
1708
            $string = self::readUnicodeStringShort(substr($recordData, 6));
145✔
1709
            $rec_name = $string['value'];
145✔
1710
        } elseif ($this->version == self::XLS_BIFF7) {
7✔
1711
            $string = $this->readByteStringShort(substr($recordData, 6));
7✔
1712
            $rec_name = $string['value'];
7✔
1713
        }
1714
        /** @var string $rec_name */
1715
        $this->sheets[] = [
152✔
1716
            'name' => $rec_name,
152✔
1717
            'offset' => $rec_offset,
152✔
1718
            'sheetState' => $sheetState,
152✔
1719
            'sheetType' => $sheetType,
152✔
1720
        ];
152✔
1721
    }
1722

1723
    /**
1724
     * Read EXTERNALBOOK record.
1725
     */
1726
    protected function readExternalBook(): void
107✔
1727
    {
1728
        $length = self::getUInt2d($this->data, $this->pos + 2);
107✔
1729
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
107✔
1730

1731
        // move stream pointer to next record
1732
        $this->pos += 4 + $length;
107✔
1733

1734
        // offset within record data
1735
        $offset = 0;
107✔
1736

1737
        // there are 4 types of records
1738
        if (strlen($recordData) > 4) {
107✔
1739
            // external reference
1740
            // offset: 0; size: 2; number of sheet names ($nm)
1741
            $nm = self::getUInt2d($recordData, 0);
×
1742
            $offset += 2;
×
1743

1744
            // offset: 2; size: var; encoded URL without sheet name (Unicode string, 16-bit length)
1745
            $encodedUrlString = self::readUnicodeStringLong(substr($recordData, 2));
×
1746
            $offset += $encodedUrlString['size'];
×
1747

1748
            // offset: var; size: var; list of $nm sheet names (Unicode strings, 16-bit length)
1749
            $externalSheetNames = [];
×
1750
            for ($i = 0; $i < $nm; ++$i) {
×
1751
                $externalSheetNameString = self::readUnicodeStringLong(substr($recordData, $offset));
×
1752
                $externalSheetNames[] = $externalSheetNameString['value'];
×
1753
                $offset += $externalSheetNameString['size'];
×
1754
            }
1755

1756
            // store the record data
1757
            $this->externalBooks[] = [
×
1758
                'type' => 'external',
×
1759
                'encodedUrl' => $encodedUrlString['value'],
×
1760
                'externalSheetNames' => $externalSheetNames,
×
1761
            ];
×
1762
        } elseif (substr($recordData, 2, 2) == pack('CC', 0x01, 0x04)) {
107✔
1763
            // internal reference
1764
            // offset: 0; size: 2; number of sheet in this document
1765
            // offset: 2; size: 2; 0x01 0x04
1766
            $this->externalBooks[] = [
107✔
1767
                'type' => 'internal',
107✔
1768
            ];
107✔
1769
        } elseif (substr($recordData, 0, 4) == pack('vCC', 0x0001, 0x01, 0x3A)) {
1✔
1770
            // add-in function
1771
            // offset: 0; size: 2; 0x0001
1772
            $this->externalBooks[] = [
1✔
1773
                'type' => 'addInFunction',
1✔
1774
            ];
1✔
1775
        } elseif (substr($recordData, 0, 2) == pack('v', 0x0000)) {
×
1776
            // DDE links, OLE links
1777
            // offset: 0; size: 2; 0x0000
1778
            // offset: 2; size: var; encoded source document name
1779
            $this->externalBooks[] = [
×
1780
                'type' => 'DDEorOLE',
×
1781
            ];
×
1782
        }
1783
    }
1784

1785
    /**
1786
     * Read EXTERNNAME record.
1787
     */
1788
    protected function readExternName(): void
1✔
1789
    {
1790
        $length = self::getUInt2d($this->data, $this->pos + 2);
1✔
1791
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
1✔
1792

1793
        // move stream pointer to next record
1794
        $this->pos += 4 + $length;
1✔
1795

1796
        // external sheet references provided for named cells
1797
        if ($this->version == self::XLS_BIFF8) {
1✔
1798
            // offset: 0; size: 2; options
1799
            //$options = self::getUInt2d($recordData, 0);
1800

1801
            // offset: 2; size: 2;
1802

1803
            // offset: 4; size: 2; not used
1804

1805
            // offset: 6; size: var
1806
            $nameString = self::readUnicodeStringShort(substr($recordData, 6));
1✔
1807

1808
            // offset: var; size: var; formula data
1809
            $offset = 6 + $nameString['size'];
1✔
1810
            $formula = $this->getFormulaFromStructure(substr($recordData, $offset));
1✔
1811

1812
            $this->externalNames[] = [
1✔
1813
                'name' => $nameString['value'],
1✔
1814
                'formula' => $formula,
1✔
1815
            ];
1✔
1816
        }
1817
    }
1818

1819
    /**
1820
     * Read EXTERNSHEET record.
1821
     */
1822
    protected function readExternSheet(): void
108✔
1823
    {
1824
        $length = self::getUInt2d($this->data, $this->pos + 2);
108✔
1825
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
108✔
1826

1827
        // move stream pointer to next record
1828
        $this->pos += 4 + $length;
108✔
1829

1830
        // external sheet references provided for named cells
1831
        if ($this->version == self::XLS_BIFF8) {
108✔
1832
            // offset: 0; size: 2; number of following ref structures
1833
            $nm = self::getUInt2d($recordData, 0);
107✔
1834
            for ($i = 0; $i < $nm; ++$i) {
107✔
1835
                $this->ref[] = [
105✔
1836
                    // offset: 2 + 6 * $i; index to EXTERNALBOOK record
1837
                    'externalBookIndex' => self::getUInt2d($recordData, 2 + 6 * $i),
105✔
1838
                    // offset: 4 + 6 * $i; index to first sheet in EXTERNALBOOK record
1839
                    'firstSheetIndex' => self::getUInt2d($recordData, 4 + 6 * $i),
105✔
1840
                    // offset: 6 + 6 * $i; index to last sheet in EXTERNALBOOK record
1841
                    'lastSheetIndex' => self::getUInt2d($recordData, 6 + 6 * $i),
105✔
1842
                ];
105✔
1843
            }
1844
        }
1845
    }
1846

1847
    /**
1848
     * DEFINEDNAME.
1849
     *
1850
     * This record is part of a Link Table. It contains the name
1851
     * and the token array of an internal defined name. Token
1852
     * arrays of defined names contain tokens with aberrant
1853
     * token classes.
1854
     *
1855
     * --    "OpenOffice.org's Documentation of the Microsoft
1856
     *         Excel File Format"
1857
     */
1858
    protected function readDefinedName(): void
21✔
1859
    {
1860
        $length = self::getUInt2d($this->data, $this->pos + 2);
21✔
1861
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
21✔
1862

1863
        // move stream pointer to next record
1864
        $this->pos += 4 + $length;
21✔
1865

1866
        if ($this->version == self::XLS_BIFF8) {
21✔
1867
            // retrieves named cells
1868

1869
            // offset: 0; size: 2; option flags
1870
            $opts = self::getUInt2d($recordData, 0);
20✔
1871

1872
            // bit: 5; mask: 0x0020; 0 = user-defined name, 1 = built-in-name
1873
            $isBuiltInName = (0x0020 & $opts) >> 5;
20✔
1874

1875
            // offset: 2; size: 1; keyboard shortcut
1876

1877
            // offset: 3; size: 1; length of the name (character count)
1878
            $nlen = ord($recordData[3]);
20✔
1879

1880
            // offset: 4; size: 2; size of the formula data (it can happen that this is zero)
1881
            // note: there can also be additional data, this is not included in $flen
1882
            $flen = self::getUInt2d($recordData, 4);
20✔
1883

1884
            // offset: 8; size: 2; 0=Global name, otherwise index to sheet (1-based)
1885
            $scope = self::getUInt2d($recordData, 8);
20✔
1886

1887
            // offset: 14; size: var; Name (Unicode string without length field)
1888
            $string = self::readUnicodeString(substr($recordData, 14), $nlen);
20✔
1889

1890
            // offset: var; size: $flen; formula data
1891
            $offset = 14 + $string['size'];
20✔
1892
            $formulaStructure = pack('v', $flen) . substr($recordData, $offset);
20✔
1893

1894
            try {
1895
                $formula = $this->getFormulaFromStructure($formulaStructure);
20✔
1896
            } catch (PhpSpreadsheetException) {
1✔
1897
                $formula = '';
1✔
1898
                $isBuiltInName = 0;
1✔
1899
            }
1900

1901
            $this->definedname[] = [
20✔
1902
                'isBuiltInName' => $isBuiltInName,
20✔
1903
                'name' => $string['value'],
20✔
1904
                'formula' => $formula,
20✔
1905
                'scope' => $scope,
20✔
1906
            ];
20✔
1907
        }
1908
    }
1909

1910
    /**
1911
     * Read MSODRAWINGGROUP record.
1912
     */
1913
    protected function readMsoDrawingGroup(): void
22✔
1914
    {
1915
        //$length = self::getUInt2d($this->data, $this->pos + 2);
1916

1917
        // get spliced record data
1918
        $splicedRecordData = $this->getSplicedRecordData();
22✔
1919
        /** @var string */
1920
        $recordData = $splicedRecordData['recordData'];
22✔
1921

1922
        $this->drawingGroupData .= $recordData;
22✔
1923
    }
1924

1925
    /**
1926
     * SST - Shared String Table.
1927
     *
1928
     * This record contains a list of all strings used anywhere
1929
     * in the workbook. Each string occurs only once. The
1930
     * workbook uses indexes into the list to reference the
1931
     * strings.
1932
     *
1933
     * --    "OpenOffice.org's Documentation of the Microsoft
1934
     *         Excel File Format"
1935
     */
1936
    protected function readSst(): void
133✔
1937
    {
1938
        // offset within (spliced) record data
1939
        $pos = 0;
133✔
1940

1941
        // Limit global SST position, further control for bad SST Length in BIFF8 data
1942
        $limitposSST = 0;
133✔
1943

1944
        // get spliced record data
1945
        $splicedRecordData = $this->getSplicedRecordData();
133✔
1946

1947
        $recordData = $splicedRecordData['recordData'];
133✔
1948
        /** @var mixed[] */
1949
        $spliceOffsets = $splicedRecordData['spliceOffsets'];
133✔
1950

1951
        // offset: 0; size: 4; total number of strings in the workbook
1952
        $pos += 4;
133✔
1953

1954
        // offset: 4; size: 4; number of following strings ($nm)
1955
        /** @var string $recordData */
1956
        $nm = self::getInt4d($recordData, 4);
133✔
1957
        $pos += 4;
133✔
1958

1959
        // look up limit position (last splice offset where pos fits)
1960
        $spliceCount = count($spliceOffsets);
133✔
1961
        for ($si = 0; $si < $spliceCount; ++$si) {
133✔
1962
            if ($pos <= $spliceOffsets[$si]) {
133✔
1963
                $limitposSST = $spliceOffsets[$si];
133✔
1964
            }
1965
        }
1966

1967
        // loop through the Unicode strings (16-bit length)
1968
        for ($i = 0; $i < $nm && $pos < $limitposSST; ++$i) {
133✔
1969
            // number of characters in the Unicode string
1970
            /** @var int $pos */
1971
            $numChars = self::getUInt2d($recordData, $pos);
80✔
1972
            /** @var int $pos */
1973
            $pos += 2;
80✔
1974

1975
            // option flags
1976
            /** @var string $recordData */
1977
            $optionFlags = ord($recordData[$pos]);
80✔
1978
            ++$pos;
80✔
1979

1980
            // bit: 0; mask: 0x01; 0 = compressed; 1 = uncompressed
1981
            $isCompressed = (($optionFlags & 0x01) == 0);
80✔
1982

1983
            // bit: 2; mask: 0x02; 0 = ordinary; 1 = Asian phonetic
1984
            $hasAsian = (($optionFlags & 0x04) != 0);
80✔
1985

1986
            // bit: 3; mask: 0x03; 0 = ordinary; 1 = Rich-Text
1987
            $hasRichText = (($optionFlags & 0x08) != 0);
80✔
1988

1989
            $formattingRuns = 0;
80✔
1990
            if ($hasRichText) {
80✔
1991
                // number of Rich-Text formatting runs
1992
                $formattingRuns = self::getUInt2d($recordData, $pos);
8✔
1993
                $pos += 2;
8✔
1994
            }
1995

1996
            $extendedRunLength = 0;
80✔
1997
            if ($hasAsian) {
80✔
1998
                // size of Asian phonetic setting
1999
                $extendedRunLength = self::getInt4d($recordData, $pos);
×
2000
                $pos += 4;
×
2001
            }
2002

2003
            // expected byte length of character array if not split
2004
            $len = ($isCompressed) ? $numChars : $numChars * 2;
80✔
2005

2006
            // look up limit position - find the first splice offset at or beyond current pos
2007
            $limitpos = null;
80✔
2008
            for ($si = 0; $si < $spliceCount; ++$si) {
80✔
2009
                if ($pos <= $spliceOffsets[$si]) {
80✔
2010
                    $limitpos = $spliceOffsets[$si];
80✔
2011

2012
                    break;
80✔
2013
                }
2014
            }
2015

2016
            /** @var int $limitpos */
2017
            if ($pos + $len <= $limitpos) {
80✔
2018
                // character array is not split between records
2019

2020
                $retstr = substr($recordData, $pos, $len);
80✔
2021
                $pos += $len;
80✔
2022
            } else {
2023
                // character array is split between records
2024

2025
                // first part of character array
2026
                $retstr = substr($recordData, $pos, $limitpos - $pos);
1✔
2027

2028
                $bytesRead = $limitpos - $pos;
1✔
2029

2030
                // remaining characters in Unicode string
2031
                $charsLeft = $numChars - (($isCompressed) ? $bytesRead : ($bytesRead / 2));
1✔
2032

2033
                $pos = $limitpos;
1✔
2034

2035
                // keep reading the characters
2036
                while ($charsLeft > 0) {
1✔
2037
                    // look up next limit position, in case the string spans more than one continue record
2038
                    for ($si = 0; $si < $spliceCount; ++$si) {
1✔
2039
                        if ($pos < $spliceOffsets[$si]) {
1✔
2040
                            $limitpos = $spliceOffsets[$si];
1✔
2041

2042
                            break;
1✔
2043
                        }
2044
                    }
2045

2046
                    // repeated option flags
2047
                    // OpenOffice.org documentation 5.21
2048
                    /** @var int $pos */
2049
                    $option = ord($recordData[$pos]);
1✔
2050
                    ++$pos;
1✔
2051

2052
                    /** @var int $limitpos */
2053
                    if ($isCompressed && ($option == 0)) {
1✔
2054
                        // 1st fragment compressed
2055
                        // this fragment compressed
2056
                        /** @var int */
2057
                        $len = min($charsLeft, $limitpos - $pos);
×
2058
                        $retstr .= substr($recordData, $pos, $len);
×
2059
                        $charsLeft -= $len;
×
2060
                        $isCompressed = true;
×
2061
                    } elseif (!$isCompressed && ($option != 0)) {
1✔
2062
                        // 1st fragment uncompressed
2063
                        // this fragment uncompressed
2064
                        /** @var int */
2065
                        $len = min($charsLeft * 2, $limitpos - $pos);
1✔
2066
                        $retstr .= substr($recordData, $pos, $len);
1✔
2067
                        $charsLeft -= $len / 2;
1✔
2068
                        $isCompressed = false;
1✔
2069
                    } elseif (!$isCompressed && ($option == 0)) {
×
2070
                        // 1st fragment uncompressed
2071
                        // this fragment compressed
NEW
2072
                        $len = (int) min($charsLeft, $limitpos - $pos);
×
2073
                        // Pad each byte with a null byte to expand to UTF-16LE
NEW
2074
                        $retstr .= chunk_split(substr($recordData, $pos, $len), 1, "\x00");
×
2075
                        $charsLeft -= $len;
×
2076
                        $isCompressed = false;
×
2077
                    } else {
2078
                        // 1st fragment compressed
2079
                        // this fragment uncompressed
2080
                        // Pad existing compressed string bytes with null bytes
NEW
2081
                        $retstr = chunk_split($retstr, 1, "\x00");
×
2082
                        /** @var int */
2083
                        $len = min($charsLeft * 2, $limitpos - $pos);
×
2084
                        $retstr .= substr($recordData, $pos, $len);
×
2085
                        $charsLeft -= $len / 2;
×
2086
                        $isCompressed = false;
×
2087
                    }
2088

2089
                    $pos += $len;
1✔
2090
                }
2091
            }
2092

2093
            // convert to UTF-8
2094
            $retstr = self::encodeUTF16($retstr, $isCompressed);
80✔
2095

2096
            // read additional Rich-Text information, if any
2097
            $fmtRuns = [];
80✔
2098
            if ($hasRichText) {
80✔
2099
                // list of formatting runs
2100
                for ($j = 0; $j < $formattingRuns; ++$j) {
8✔
2101
                    // first formatted character; zero-based
2102
                    /** @var int $pos */
2103
                    $charPos = self::getUInt2d($recordData, $pos + $j * 4);
8✔
2104

2105
                    // index to font record
2106
                    $fontIndex = self::getUInt2d($recordData, $pos + 2 + $j * 4);
8✔
2107

2108
                    $fmtRuns[] = [
8✔
2109
                        'charPos' => $charPos,
8✔
2110
                        'fontIndex' => $fontIndex,
8✔
2111
                    ];
8✔
2112
                }
2113
                $pos += 4 * $formattingRuns;
8✔
2114
            }
2115

2116
            // read additional Asian phonetics information, if any
2117
            if ($hasAsian) {
80✔
2118
                // For Asian phonetic settings, we skip the extended string data
2119
                $pos += $extendedRunLength;
×
2120
            }
2121

2122
            // store the shared sting
2123
            $this->sst[] = [
80✔
2124
                'value' => $retstr,
80✔
2125
                'fmtRuns' => $fmtRuns,
80✔
2126
            ];
80✔
2127
        }
2128

2129
        // getSplicedRecordData() takes care of moving current position in data stream
2130
    }
2131

2132
    /**
2133
     * Read PRINTGRIDLINES record.
2134
     */
2135
    protected function readPrintGridlines(): void
133✔
2136
    {
2137
        $length = self::getUInt2d($this->data, $this->pos + 2);
133✔
2138
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
133✔
2139

2140
        // move stream pointer to next record
2141
        $this->pos += 4 + $length;
133✔
2142

2143
        if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) {
133✔
2144
            // offset: 0; size: 2; 0 = do not print sheet grid lines; 1 = print sheet gridlines
2145
            $printGridlines = (bool) self::getUInt2d($recordData, 0);
129✔
2146
            $this->phpSheet->setPrintGridlines($printGridlines);
129✔
2147
        }
2148
    }
2149

2150
    /**
2151
     * Read DEFAULTROWHEIGHT record.
2152
     */
2153
    protected function readDefaultRowHeight(): void
67✔
2154
    {
2155
        $length = self::getUInt2d($this->data, $this->pos + 2);
67✔
2156
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
67✔
2157

2158
        // move stream pointer to next record
2159
        $this->pos += 4 + $length;
67✔
2160

2161
        // offset: 0; size: 2; option flags
2162
        // offset: 2; size: 2; default height for unused rows, (twips 1/20 point)
2163
        $height = self::getUInt2d($recordData, 2);
67✔
2164
        $this->phpSheet->getDefaultRowDimension()->setRowHeight($height / 20);
67✔
2165
    }
2166

2167
    /**
2168
     * Read SHEETPR record.
2169
     */
2170
    protected function readSheetPr(): void
135✔
2171
    {
2172
        $length = self::getUInt2d($this->data, $this->pos + 2);
135✔
2173
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
135✔
2174

2175
        // move stream pointer to next record
2176
        $this->pos += 4 + $length;
135✔
2177

2178
        // offset: 0; size: 2
2179

2180
        // bit: 6; mask: 0x0040; 0 = outline buttons above outline group
2181
        $isSummaryBelow = (0x0040 & self::getUInt2d($recordData, 0)) >> 6;
135✔
2182
        $this->phpSheet->setShowSummaryBelow((bool) $isSummaryBelow);
135✔
2183

2184
        // bit: 7; mask: 0x0080; 0 = outline buttons left of outline group
2185
        $isSummaryRight = (0x0080 & self::getUInt2d($recordData, 0)) >> 7;
135✔
2186
        $this->phpSheet->setShowSummaryRight((bool) $isSummaryRight);
135✔
2187

2188
        // bit: 8; mask: 0x100; 0 = scale printout in percent, 1 = fit printout to number of pages
2189
        // this corresponds to radio button setting in page setup dialog in Excel
2190
        $this->isFitToPages = (bool) ((0x0100 & self::getUInt2d($recordData, 0)) >> 8);
135✔
2191
    }
2192

2193
    /**
2194
     * Read HORIZONTALPAGEBREAKS record.
2195
     */
2196
    protected function readHorizontalPageBreaks(): void
5✔
2197
    {
2198
        $length = self::getUInt2d($this->data, $this->pos + 2);
5✔
2199
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
5✔
2200

2201
        // move stream pointer to next record
2202
        $this->pos += 4 + $length;
5✔
2203

2204
        if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) {
5✔
2205
            // offset: 0; size: 2; number of the following row index structures
2206
            $nm = self::getUInt2d($recordData, 0);
5✔
2207

2208
            // offset: 2; size: 6 * $nm; list of $nm row index structures
2209
            for ($i = 0; $i < $nm; ++$i) {
5✔
2210
                $r = self::getUInt2d($recordData, 2 + 6 * $i);
3✔
2211
                $cf = self::getUInt2d($recordData, 2 + 6 * $i + 2);
3✔
2212
                //$cl = self::getUInt2d($recordData, 2 + 6 * $i + 4);
2213

2214
                // not sure why two column indexes are necessary?
2215
                $this->phpSheet->setBreak([$cf + 1, $r], Worksheet::BREAK_ROW);
3✔
2216
            }
2217
        }
2218
    }
2219

2220
    /**
2221
     * Read VERTICALPAGEBREAKS record.
2222
     */
2223
    protected function readVerticalPageBreaks(): void
5✔
2224
    {
2225
        $length = self::getUInt2d($this->data, $this->pos + 2);
5✔
2226
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
5✔
2227

2228
        // move stream pointer to next record
2229
        $this->pos += 4 + $length;
5✔
2230

2231
        if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) {
5✔
2232
            // offset: 0; size: 2; number of the following column index structures
2233
            $nm = self::getUInt2d($recordData, 0);
5✔
2234

2235
            // offset: 2; size: 6 * $nm; list of $nm row index structures
2236
            for ($i = 0; $i < $nm; ++$i) {
5✔
2237
                $c = self::getUInt2d($recordData, 2 + 6 * $i);
3✔
2238
                $rf = self::getUInt2d($recordData, 2 + 6 * $i + 2);
3✔
2239
                //$rl = self::getUInt2d($recordData, 2 + 6 * $i + 4);
2240

2241
                // not sure why two row indexes are necessary?
2242
                $this->phpSheet->setBreak([$c + 1, ($rf > 0) ? $rf : 1], Worksheet::BREAK_COLUMN);
3✔
2243
            }
2244
        }
2245
    }
2246

2247
    /**
2248
     * Read HEADER record.
2249
     */
2250
    protected function readHeader(): void
133✔
2251
    {
2252
        $length = self::getUInt2d($this->data, $this->pos + 2);
133✔
2253
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
133✔
2254

2255
        // move stream pointer to next record
2256
        $this->pos += 4 + $length;
133✔
2257

2258
        if (!$this->readDataOnly) {
133✔
2259
            // offset: 0; size: var
2260
            // realized that $recordData can be empty even when record exists
2261
            if ($recordData) {
131✔
2262
                if ($this->version == self::XLS_BIFF8) {
83✔
2263
                    $string = self::readUnicodeStringLong($recordData);
82✔
2264
                } else {
2265
                    $string = $this->readByteStringShort($recordData);
1✔
2266
                }
2267

2268
                /** @var string[] $string */
2269
                $this->phpSheet
83✔
2270
                    ->getHeaderFooter()
83✔
2271
                    ->setOddHeader($string['value']);
83✔
2272
                $this->phpSheet
83✔
2273
                    ->getHeaderFooter()
83✔
2274
                    ->setEvenHeader($string['value']);
83✔
2275
            }
2276
        }
2277
    }
2278

2279
    /**
2280
     * Read FOOTER record.
2281
     */
2282
    protected function readFooter(): void
133✔
2283
    {
2284
        $length = self::getUInt2d($this->data, $this->pos + 2);
133✔
2285
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
133✔
2286

2287
        // move stream pointer to next record
2288
        $this->pos += 4 + $length;
133✔
2289

2290
        if (!$this->readDataOnly) {
133✔
2291
            // offset: 0; size: var
2292
            // realized that $recordData can be empty even when record exists
2293
            if ($recordData) {
131✔
2294
                if ($this->version == self::XLS_BIFF8) {
85✔
2295
                    $string = self::readUnicodeStringLong($recordData);
83✔
2296
                } else {
2297
                    $string = $this->readByteStringShort($recordData);
2✔
2298
                }
2299
                /** @var string */
2300
                $temp = $string['value'];
85✔
2301
                $this->phpSheet
85✔
2302
                    ->getHeaderFooter()
85✔
2303
                    ->setOddFooter($temp);
85✔
2304
                $this->phpSheet
85✔
2305
                    ->getHeaderFooter()
85✔
2306
                    ->setEvenFooter($temp);
85✔
2307
            }
2308
        }
2309
    }
2310

2311
    /**
2312
     * Read HCENTER record.
2313
     */
2314
    protected function readHcenter(): void
133✔
2315
    {
2316
        $length = self::getUInt2d($this->data, $this->pos + 2);
133✔
2317
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
133✔
2318

2319
        // move stream pointer to next record
2320
        $this->pos += 4 + $length;
133✔
2321

2322
        if (!$this->readDataOnly) {
133✔
2323
            // offset: 0; size: 2; 0 = print sheet left aligned, 1 = print sheet centered horizontally
2324
            $isHorizontalCentered = (bool) self::getUInt2d($recordData, 0);
131✔
2325

2326
            $this->phpSheet->getPageSetup()->setHorizontalCentered($isHorizontalCentered);
131✔
2327
        }
2328
    }
2329

2330
    /**
2331
     * Read VCENTER record.
2332
     */
2333
    protected function readVcenter(): void
133✔
2334
    {
2335
        $length = self::getUInt2d($this->data, $this->pos + 2);
133✔
2336
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
133✔
2337

2338
        // move stream pointer to next record
2339
        $this->pos += 4 + $length;
133✔
2340

2341
        if (!$this->readDataOnly) {
133✔
2342
            // offset: 0; size: 2; 0 = print sheet aligned at top page border, 1 = print sheet vertically centered
2343
            $isVerticalCentered = (bool) self::getUInt2d($recordData, 0);
131✔
2344

2345
            $this->phpSheet->getPageSetup()->setVerticalCentered($isVerticalCentered);
131✔
2346
        }
2347
    }
2348

2349
    /**
2350
     * Read LEFTMARGIN record.
2351
     */
2352
    protected function readLeftMargin(): void
126✔
2353
    {
2354
        $length = self::getUInt2d($this->data, $this->pos + 2);
126✔
2355
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
126✔
2356

2357
        // move stream pointer to next record
2358
        $this->pos += 4 + $length;
126✔
2359

2360
        if (!$this->readDataOnly) {
126✔
2361
            // offset: 0; size: 8
2362
            $this->phpSheet->getPageMargins()->setLeft(self::extractNumber($recordData));
124✔
2363
        }
2364
    }
2365

2366
    /**
2367
     * Read RIGHTMARGIN record.
2368
     */
2369
    protected function readRightMargin(): void
126✔
2370
    {
2371
        $length = self::getUInt2d($this->data, $this->pos + 2);
126✔
2372
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
126✔
2373

2374
        // move stream pointer to next record
2375
        $this->pos += 4 + $length;
126✔
2376

2377
        if (!$this->readDataOnly) {
126✔
2378
            // offset: 0; size: 8
2379
            $this->phpSheet->getPageMargins()->setRight(self::extractNumber($recordData));
124✔
2380
        }
2381
    }
2382

2383
    /**
2384
     * Read TOPMARGIN record.
2385
     */
2386
    protected function readTopMargin(): void
126✔
2387
    {
2388
        $length = self::getUInt2d($this->data, $this->pos + 2);
126✔
2389
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
126✔
2390

2391
        // move stream pointer to next record
2392
        $this->pos += 4 + $length;
126✔
2393

2394
        if (!$this->readDataOnly) {
126✔
2395
            // offset: 0; size: 8
2396
            $this->phpSheet->getPageMargins()->setTop(self::extractNumber($recordData));
124✔
2397
        }
2398
    }
2399

2400
    /**
2401
     * Read BOTTOMMARGIN record.
2402
     */
2403
    protected function readBottomMargin(): void
126✔
2404
    {
2405
        $length = self::getUInt2d($this->data, $this->pos + 2);
126✔
2406
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
126✔
2407

2408
        // move stream pointer to next record
2409
        $this->pos += 4 + $length;
126✔
2410

2411
        if (!$this->readDataOnly) {
126✔
2412
            // offset: 0; size: 8
2413
            $this->phpSheet->getPageMargins()->setBottom(self::extractNumber($recordData));
124✔
2414
        }
2415
    }
2416

2417
    /**
2418
     * Read PAGESETUP record.
2419
     */
2420
    protected function readPageSetup(): void
135✔
2421
    {
2422
        $length = self::getUInt2d($this->data, $this->pos + 2);
135✔
2423
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
135✔
2424

2425
        // move stream pointer to next record
2426
        $this->pos += 4 + $length;
135✔
2427

2428
        if (!$this->readDataOnly) {
135✔
2429
            // offset: 0; size: 2; paper size
2430
            $paperSize = self::getUInt2d($recordData, 0);
133✔
2431

2432
            // offset: 2; size: 2; scaling factor
2433
            $scale = self::getUInt2d($recordData, 2);
133✔
2434

2435
            // offset: 6; size: 2; fit worksheet width to this number of pages, 0 = use as many as needed
2436
            $fitToWidth = self::getUInt2d($recordData, 6);
133✔
2437

2438
            // offset: 8; size: 2; fit worksheet height to this number of pages, 0 = use as many as needed
2439
            $fitToHeight = self::getUInt2d($recordData, 8);
133✔
2440

2441
            // offset: 10; size: 2; option flags
2442

2443
            // bit: 0; mask: 0x0001; 0=down then over, 1=over then down
2444
            $isOverThenDown = (0x0001 & self::getUInt2d($recordData, 10));
133✔
2445

2446
            // bit: 1; mask: 0x0002; 0=landscape, 1=portrait
2447
            $isPortrait = (0x0002 & self::getUInt2d($recordData, 10)) >> 1;
133✔
2448

2449
            // bit: 2; mask: 0x0004; 1= paper size, scaling factor, paper orient. not init
2450
            // when this bit is set, do not use flags for those properties
2451
            $isNotInit = (0x0004 & self::getUInt2d($recordData, 10)) >> 2;
133✔
2452

2453
            if (!$isNotInit) {
133✔
2454
                $this->phpSheet->getPageSetup()->setPaperSize($paperSize);
122✔
2455
                $this->phpSheet->getPageSetup()->setPageOrder(((bool) $isOverThenDown) ? PageSetup::PAGEORDER_OVER_THEN_DOWN : PageSetup::PAGEORDER_DOWN_THEN_OVER);
122✔
2456
                $this->phpSheet->getPageSetup()->setOrientation(((bool) $isPortrait) ? PageSetup::ORIENTATION_PORTRAIT : PageSetup::ORIENTATION_LANDSCAPE);
122✔
2457

2458
                $this->phpSheet->getPageSetup()->setScale($scale, false);
122✔
2459
                $this->phpSheet->getPageSetup()->setFitToPage((bool) $this->isFitToPages);
122✔
2460
                $this->phpSheet->getPageSetup()->setFitToWidth($fitToWidth, false);
122✔
2461
                $this->phpSheet->getPageSetup()->setFitToHeight($fitToHeight, false);
122✔
2462
            }
2463

2464
            // offset: 16; size: 8; header margin (IEEE 754 floating-point value)
2465
            $marginHeader = self::extractNumber(substr($recordData, 16, 8));
133✔
2466
            $this->phpSheet->getPageMargins()->setHeader($marginHeader);
133✔
2467

2468
            // offset: 24; size: 8; footer margin (IEEE 754 floating-point value)
2469
            $marginFooter = self::extractNumber(substr($recordData, 24, 8));
133✔
2470
            $this->phpSheet->getPageMargins()->setFooter($marginFooter);
133✔
2471
        }
2472
    }
2473

2474
    /**
2475
     * PROTECT - Sheet protection (BIFF2 through BIFF8)
2476
     *   if this record is omitted, then it also means no sheet protection.
2477
     */
2478
    protected function readProtect(): void
7✔
2479
    {
2480
        $length = self::getUInt2d($this->data, $this->pos + 2);
7✔
2481
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
7✔
2482

2483
        // move stream pointer to next record
2484
        $this->pos += 4 + $length;
7✔
2485

2486
        if ($this->readDataOnly) {
7✔
2487
            return;
×
2488
        }
2489

2490
        // offset: 0; size: 2;
2491

2492
        // bit 0, mask 0x01; 1 = sheet is protected
2493
        $bool = (0x01 & self::getUInt2d($recordData, 0)) >> 0;
7✔
2494
        $this->phpSheet->getProtection()->setSheet((bool) $bool);
7✔
2495
    }
2496

2497
    /**
2498
     * SCENPROTECT.
2499
     */
2500
    protected function readScenProtect(): void
×
2501
    {
2502
        $length = self::getUInt2d($this->data, $this->pos + 2);
×
2503
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
×
2504

2505
        // move stream pointer to next record
2506
        $this->pos += 4 + $length;
×
2507

2508
        if ($this->readDataOnly) {
×
2509
            return;
×
2510
        }
2511

2512
        // offset: 0; size: 2;
2513

2514
        // bit: 0, mask 0x01; 1 = scenarios are protected
2515
        $bool = (0x01 & self::getUInt2d($recordData, 0)) >> 0;
×
2516

2517
        $this->phpSheet->getProtection()->setScenarios((bool) $bool);
×
2518
    }
2519

2520
    /**
2521
     * OBJECTPROTECT.
2522
     */
2523
    protected function readObjectProtect(): void
2✔
2524
    {
2525
        $length = self::getUInt2d($this->data, $this->pos + 2);
2✔
2526
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2✔
2527

2528
        // move stream pointer to next record
2529
        $this->pos += 4 + $length;
2✔
2530

2531
        if ($this->readDataOnly) {
2✔
2532
            return;
×
2533
        }
2534

2535
        // offset: 0; size: 2;
2536

2537
        // bit: 0, mask 0x01; 1 = objects are protected
2538
        $bool = (0x01 & self::getUInt2d($recordData, 0)) >> 0;
2✔
2539

2540
        $this->phpSheet->getProtection()->setObjects((bool) $bool);
2✔
2541
    }
2542

2543
    /**
2544
     * PASSWORD - Sheet protection (hashed) password (BIFF2 through BIFF8).
2545
     */
2546
    protected function readPassword(): void
3✔
2547
    {
2548
        $length = self::getUInt2d($this->data, $this->pos + 2);
3✔
2549
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3✔
2550

2551
        // move stream pointer to next record
2552
        $this->pos += 4 + $length;
3✔
2553

2554
        if (!$this->readDataOnly) {
3✔
2555
            // offset: 0; size: 2; 16-bit hash value of password
2556
            $password = strtoupper(dechex(self::getUInt2d($recordData, 0))); // the hashed password
3✔
2557
            $this->phpSheet->getProtection()->setPassword($password, true);
3✔
2558
        }
2559
    }
2560

2561
    /**
2562
     * Read DEFCOLWIDTH record.
2563
     */
2564
    protected function readDefColWidth(): void
134✔
2565
    {
2566
        $length = self::getUInt2d($this->data, $this->pos + 2);
134✔
2567
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
134✔
2568

2569
        // move stream pointer to next record
2570
        $this->pos += 4 + $length;
134✔
2571

2572
        // offset: 0; size: 2; default column width
2573
        $width = self::getUInt2d($recordData, 0);
134✔
2574
        if ($width != 8) {
134✔
2575
            $this->phpSheet->getDefaultColumnDimension()->setWidth($width);
5✔
2576
        }
2577
    }
2578

2579
    /**
2580
     * Read COLINFO record.
2581
     */
2582
    protected function readColInfo(): void
120✔
2583
    {
2584
        $length = self::getUInt2d($this->data, $this->pos + 2);
120✔
2585
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
120✔
2586

2587
        // move stream pointer to next record
2588
        $this->pos += 4 + $length;
120✔
2589

2590
        if (!$this->readDataOnly) {
120✔
2591
            // offset: 0; size: 2; index to first column in range
2592
            $firstColumnIndex = self::getUInt2d($recordData, 0);
118✔
2593

2594
            // offset: 2; size: 2; index to last column in range
2595
            $lastColumnIndex = self::getUInt2d($recordData, 2);
118✔
2596

2597
            // offset: 4; size: 2; width of the column in 1/256 of the width of the zero character
2598
            $width = self::getUInt2d($recordData, 4);
118✔
2599

2600
            // offset: 6; size: 2; index to XF record for default column formatting
2601
            $xfIndex = self::getUInt2d($recordData, 6);
118✔
2602

2603
            // offset: 8; size: 2; option flags
2604
            // bit: 0; mask: 0x0001; 1= columns are hidden
2605
            $isHidden = (0x0001 & self::getUInt2d($recordData, 8)) >> 0;
118✔
2606

2607
            // bit: 10-8; mask: 0x0700; outline level of the columns (0 = no outline)
2608
            $level = (0x0700 & self::getUInt2d($recordData, 8)) >> 8;
118✔
2609

2610
            // bit: 12; mask: 0x1000; 1 = collapsed
2611
            $isCollapsed = (bool) ((0x1000 & self::getUInt2d($recordData, 8)) >> 12);
118✔
2612

2613
            // offset: 10; size: 2; not used
2614

2615
            for ($i = $firstColumnIndex + 1; $i <= $lastColumnIndex + 1; ++$i) {
118✔
2616
                if ($lastColumnIndex == AddressRange::MAX_COLUMN_INT_XLS - 1 || $lastColumnIndex == AddressRange::MAX_COLUMN_INT) {
118✔
2617
                    $this->phpSheet->getDefaultColumnDimension()->setWidth($width / 256);
1✔
2618

2619
                    break;
1✔
2620
                }
2621
                $this->phpSheet->getColumnDimensionByColumn($i)->setWidth($width / 256);
118✔
2622
                $this->phpSheet->getColumnDimensionByColumn($i)->setVisible(!$isHidden);
118✔
2623
                $this->phpSheet->getColumnDimensionByColumn($i)->setOutlineLevel($level);
118✔
2624
                $this->phpSheet->getColumnDimensionByColumn($i)->setCollapsed($isCollapsed);
118✔
2625
                if (isset($this->mapCellXfIndex[$xfIndex])) {
118✔
2626
                    $this->phpSheet->getColumnDimensionByColumn($i)->setXfIndex($this->mapCellXfIndex[$xfIndex]);
116✔
2627
                }
2628
            }
2629
        }
2630
    }
2631

2632
    /**
2633
     * ROW.
2634
     *
2635
     * This record contains the properties of a single row in a
2636
     * sheet. Rows and cells in a sheet are divided into blocks
2637
     * of 32 rows.
2638
     *
2639
     * --    "OpenOffice.org's Documentation of the Microsoft
2640
     *         Excel File Format"
2641
     */
2642
    protected function readRow(): void
76✔
2643
    {
2644
        $length = self::getUInt2d($this->data, $this->pos + 2);
76✔
2645
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
76✔
2646

2647
        // move stream pointer to next record
2648
        $this->pos += 4 + $length;
76✔
2649

2650
        if (!$this->readDataOnly) {
76✔
2651
            // offset: 0; size: 2; index of this row
2652
            $r = self::getUInt2d($recordData, 0);
74✔
2653

2654
            // offset: 2; size: 2; index to column of the first cell which is described by a cell record
2655

2656
            // offset: 4; size: 2; index to column of the last cell which is described by a cell record, increased by 1
2657

2658
            // offset: 6; size: 2;
2659

2660
            // bit: 14-0; mask: 0x7FFF; height of the row, in twips = 1/20 of a point
2661
            $height = (0x7FFF & self::getUInt2d($recordData, 6)) >> 0;
74✔
2662

2663
            // bit: 15: mask: 0x8000; 0 = row has custom height; 1= row has default height
2664
            $useDefaultHeight = (0x8000 & self::getUInt2d($recordData, 6)) >> 15;
74✔
2665

2666
            if (!$useDefaultHeight) {
74✔
2667
                if (
2668
                    $this->phpSheet->getDefaultRowDimension()->getRowHeight() > 0
72✔
2669
                ) {
2670
                    $this->phpSheet->getRowDimension($r + 1)
60✔
2671
                        ->setCustomFormat(true, ($height === 255) ? -1 : ($height / 20));
60✔
2672
                } else {
2673
                    $this->phpSheet->getRowDimension($r + 1)->setRowHeight($height / 20);
12✔
2674
                }
2675
            }
2676

2677
            // offset: 8; size: 2; not used
2678

2679
            // offset: 10; size: 2; not used in BIFF5-BIFF8
2680

2681
            // offset: 12; size: 4; option flags and default row formatting
2682

2683
            // bit: 2-0: mask: 0x00000007; outline level of the row
2684
            $level = (0x00000007 & self::getInt4d($recordData, 12)) >> 0;
74✔
2685
            $this->phpSheet->getRowDimension($r + 1)->setOutlineLevel($level);
74✔
2686

2687
            // bit: 4; mask: 0x00000010; 1 = outline group start or ends here... and is collapsed
2688
            $isCollapsed = (bool) ((0x00000010 & self::getInt4d($recordData, 12)) >> 4);
74✔
2689
            $this->phpSheet->getRowDimension($r + 1)->setCollapsed($isCollapsed);
74✔
2690

2691
            // bit: 5; mask: 0x00000020; 1 = row is hidden
2692
            $isHidden = (0x00000020 & self::getInt4d($recordData, 12)) >> 5;
74✔
2693
            $this->phpSheet->getRowDimension($r + 1)->setVisible(!$isHidden);
74✔
2694

2695
            // bit: 7; mask: 0x00000080; 1 = row has explicit format
2696
            $hasExplicitFormat = (0x00000080 & self::getInt4d($recordData, 12)) >> 7;
74✔
2697

2698
            // bit: 27-16; mask: 0x0FFF0000; only applies when hasExplicitFormat = 1; index to XF record
2699
            $xfIndex = (0x0FFF0000 & self::getInt4d($recordData, 12)) >> 16;
74✔
2700

2701
            if ($hasExplicitFormat && isset($this->mapCellXfIndex[$xfIndex])) {
74✔
2702
                $this->phpSheet->getRowDimension($r + 1)->setXfIndex($this->mapCellXfIndex[$xfIndex]);
6✔
2703
            }
2704
        }
2705
    }
2706

2707
    /**
2708
     * Read RK record
2709
     * This record represents a cell that contains an RK value
2710
     * (encoded integer or floating-point value). If a
2711
     * floating-point value cannot be encoded to an RK value,
2712
     * a NUMBER record will be written. This record replaces the
2713
     * record INTEGER written in BIFF2.
2714
     *
2715
     * --    "OpenOffice.org's Documentation of the Microsoft
2716
     *         Excel File Format"
2717
     */
2718
    protected function readRk(): void
37✔
2719
    {
2720
        $length = self::getUInt2d($this->data, $this->pos + 2);
37✔
2721
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
37✔
2722

2723
        // move stream pointer to next record
2724
        $this->pos += 4 + $length;
37✔
2725

2726
        // offset: 0; size: 2; index to row
2727
        $row = self::getUInt2d($recordData, 0);
37✔
2728

2729
        // offset: 2; size: 2; index to column
2730
        $column = self::getUInt2d($recordData, 2);
37✔
2731
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
37✔
2732
        $cellCoordinate = $columnString . ($row + 1);
37✔
2733

2734
        // Read cell?
2735
        if ($this->cachedReadFilter->readCell($columnString, $row + 1, $this->phpSheetTitle)) {
37✔
2736
            // offset: 4; size: 2; index to XF record
2737
            $xfIndex = self::getUInt2d($recordData, 4);
37✔
2738

2739
            // offset: 6; size: 4; RK value
2740
            $rknum = self::getInt4d($recordData, 6);
37✔
2741
            $numValue = self::getIEEE754($rknum);
37✔
2742

2743
            $cell = $this->phpSheet->getCell($cellCoordinate);
37✔
2744
            if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
37✔
2745
                // add style information
2746
                $cell->setXfIndexNoUpdate($this->mapCellXfIndex[$xfIndex]);
33✔
2747
            }
2748

2749
            // add cell
2750
            $cell->setValueExplicit($numValue, DataType::TYPE_NUMERIC);
37✔
2751
        }
2752
    }
2753

2754
    /**
2755
     * Read LABELSST record
2756
     * This record represents a cell that contains a string. It
2757
     * replaces the LABEL record and RSTRING record used in
2758
     * BIFF2-BIFF5.
2759
     *
2760
     * --    "OpenOffice.org's Documentation of the Microsoft
2761
     *         Excel File Format"
2762
     */
2763
    protected function readLabelSst(): void
77✔
2764
    {
2765
        $length = self::getUInt2d($this->data, $this->pos + 2);
77✔
2766
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
77✔
2767

2768
        // move stream pointer to next record
2769
        $this->pos += 4 + $length;
77✔
2770

2771
        // offset: 0; size: 2; index to row
2772
        $row = self::getUInt2d($recordData, 0);
77✔
2773

2774
        // offset: 2; size: 2; index to column
2775
        $column = self::getUInt2d($recordData, 2);
77✔
2776
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
77✔
2777
        $cellCoordinate = $columnString . ($row + 1);
77✔
2778

2779
        // Read cell?
2780
        if ($this->cachedReadFilter->readCell($columnString, $row + 1, $this->phpSheetTitle)) {
77✔
2781
            // offset: 4; size: 2; index to XF record
2782
            $xfIndex = self::getUInt2d($recordData, 4);
77✔
2783

2784
            // offset: 6; size: 4; index to SST record
2785
            $index = self::getInt4d($recordData, 6);
77✔
2786

2787
            // cache SST entry locally to avoid repeated array lookups
2788
            $sstValue = $this->sst[$index]['value'];
77✔
2789
            $fmtRuns = $this->sst[$index]['fmtRuns'];
77✔
2790

2791
            // add cell
2792
            if ($fmtRuns && !$this->readDataOnly) {
77✔
2793
                // then we should treat as rich text
2794
                $richText = new RichText();
6✔
2795
                $charPos = 0;
6✔
2796
                $sstCount = count($fmtRuns);
6✔
2797
                $sstValueLength = StringHelper::countCharacters($sstValue);
6✔
2798
                $lastFontIndex = count($this->objFonts) - 1;
6✔
2799
                for ($i = 0; $i <= $sstCount; ++$i) {
6✔
2800
                    /** @var mixed[][] $fmtRuns */
2801
                    if (isset($fmtRuns[$i])) {
6✔
2802
                        /** @var int[] */
2803
                        $temp = $fmtRuns[$i];
6✔
2804
                        $temp = $temp['charPos'];
6✔
2805
                        /** @var int $charPos */
2806
                        $text = StringHelper::substring($sstValue, $charPos, $temp - $charPos);
6✔
2807
                        $charPos = $temp;
6✔
2808
                    } else {
2809
                        $text = StringHelper::substring($sstValue, $charPos, $sstValueLength);
6✔
2810
                    }
2811

2812
                    if (StringHelper::countCharacters($text) > 0) {
6✔
2813
                        if ($i == 0) { // first text run, no style
6✔
2814
                            $richText->createText($text);
3✔
2815
                        } else {
2816
                            $textRun = $richText->createTextRun($text);
6✔
2817
                            /** @var int[][] $fmtRuns */
2818
                            if (isset($fmtRuns[$i - 1])) {
6✔
2819
                                if ($fmtRuns[$i - 1]['fontIndex'] < 4) {
6✔
2820
                                    $fontIndex = $fmtRuns[$i - 1]['fontIndex'];
5✔
2821
                                } else {
2822
                                    // this has to do with that index 4 is omitted in all BIFF versions for some stra          nge reason
2823
                                    // check the OpenOffice documentation of the FONT record
2824
                                    /** @var int */
2825
                                    $temp = $fmtRuns[$i - 1]['fontIndex'];
4✔
2826
                                    $fontIndex = $temp - 1;
4✔
2827
                                }
2828
                                if ($fontIndex > $lastFontIndex) {
6✔
2829
                                    $fontIndex = $lastFontIndex;
1✔
2830
                                }
2831
                                $textRun->setFont(clone $this->objFonts[$fontIndex]);
6✔
2832
                            }
2833
                        }
2834
                    }
2835
                }
2836
                if ($this->readEmptyCells || trim($richText->getPlainText()) !== '') {
6✔
2837
                    $cell = $this->phpSheet->getCell($cellCoordinate);
6✔
2838
                    if (isset($this->mapCellXfIndex[$xfIndex])) {
6✔
2839
                        $cell->setXfIndexNoUpdate($this->mapCellXfIndex[$xfIndex]);
6✔
2840
                    }
2841
                    $cell->setValueExplicit($richText, DataType::TYPE_STRING);
6✔
2842
                }
2843
            } else {
2844
                if ($this->readEmptyCells || trim($sstValue) !== '') {
76✔
2845
                    $cell = $this->phpSheet->getCell($cellCoordinate);
76✔
2846
                    if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
76✔
2847
                        $cell->setXfIndexNoUpdate($this->mapCellXfIndex[$xfIndex]);
74✔
2848
                    }
2849
                    $cell->setValueExplicit($sstValue, DataType::TYPE_STRING);
76✔
2850
                }
2851
            }
2852
        }
2853
    }
2854

2855
    /**
2856
     * Read MULRK record
2857
     * This record represents a cell range containing RK value
2858
     * cells. All cells are located in the same row.
2859
     *
2860
     * --    "OpenOffice.org's Documentation of the Microsoft
2861
     *         Excel File Format"
2862
     */
2863
    protected function readMulRk(): void
22✔
2864
    {
2865
        $length = self::getUInt2d($this->data, $this->pos + 2);
22✔
2866
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
22✔
2867

2868
        // move stream pointer to next record
2869
        $this->pos += 4 + $length;
22✔
2870

2871
        // offset: 0; size: 2; index to row
2872
        $row = self::getUInt2d($recordData, 0);
22✔
2873

2874
        // offset: 2; size: 2; index to first column
2875
        $colFirst = self::getUInt2d($recordData, 2);
22✔
2876

2877
        // offset: var; size: 2; index to last column
2878
        $colLast = self::getUInt2d($recordData, $length - 2);
22✔
2879
        $columns = $colLast - $colFirst + 1;
22✔
2880

2881
        // offset within record data
2882
        $offset = 4;
22✔
2883

2884
        $rowIndex = $row + 1;
22✔
2885
        for ($i = 1; $i <= $columns; ++$i) {
22✔
2886
            $columnString = Coordinate::stringFromColumnIndex($colFirst + $i);
22✔
2887

2888
            // Read cell?
2889
            if ($this->cachedReadFilter->readCell($columnString, $rowIndex, $this->phpSheetTitle)) {
22✔
2890
                // offset: var; size: 2; index to XF record
2891
                $xfIndex = self::getUInt2d($recordData, $offset);
22✔
2892

2893
                // offset: var; size: 4; RK value
2894
                $numValue = self::getIEEE754(self::getInt4d($recordData, $offset + 2));
22✔
2895
                $cell = $this->phpSheet->getCell($columnString . $rowIndex);
22✔
2896
                if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
22✔
2897
                    // add style
2898
                    $cell->setXfIndexNoUpdate($this->mapCellXfIndex[$xfIndex]);
20✔
2899
                }
2900

2901
                // add cell value
2902
                $cell->setValueExplicit($numValue, DataType::TYPE_NUMERIC);
22✔
2903
            }
2904

2905
            $offset += 6;
22✔
2906
        }
2907
    }
2908

2909
    /**
2910
     * Read NUMBER record
2911
     * This record represents a cell that contains a
2912
     * floating-point value.
2913
     *
2914
     * --    "OpenOffice.org's Documentation of the Microsoft
2915
     *         Excel File Format"
2916
     */
2917
    protected function readNumber(): void
60✔
2918
    {
2919
        $length = self::getUInt2d($this->data, $this->pos + 2);
60✔
2920
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
60✔
2921

2922
        // move stream pointer to next record
2923
        $this->pos += 4 + $length;
60✔
2924

2925
        // offset: 0; size: 2; index to row
2926
        $row = self::getUInt2d($recordData, 0);
60✔
2927

2928
        // offset: 2; size 2; index to column
2929
        $column = self::getUInt2d($recordData, 2);
60✔
2930
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
60✔
2931
        $cellCoordinate = $columnString . ($row + 1);
60✔
2932

2933
        // Read cell?
2934
        if ($this->cachedReadFilter->readCell($columnString, $row + 1, $this->phpSheetTitle)) {
60✔
2935
            // offset 4; size: 2; index to XF record
2936
            $xfIndex = self::getUInt2d($recordData, 4);
60✔
2937

2938
            $numValue = self::extractNumber(substr($recordData, 6, 8));
60✔
2939

2940
            $cell = $this->phpSheet->getCell($cellCoordinate);
60✔
2941
            if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
60✔
2942
                // add cell style
2943
                $cell->setXfIndexNoUpdate($this->mapCellXfIndex[$xfIndex]);
58✔
2944
            }
2945

2946
            // add cell value
2947
            $cell->setValueExplicit($numValue, DataType::TYPE_NUMERIC);
60✔
2948
        }
2949
    }
2950

2951
    /**
2952
     * Read FORMULA record + perhaps a following STRING record if formula result is a string
2953
     * This record contains the token array and the result of a
2954
     * formula cell.
2955
     *
2956
     * --    "OpenOffice.org's Documentation of the Microsoft
2957
     *         Excel File Format"
2958
     */
2959
    protected function readFormula(): void
44✔
2960
    {
2961
        $length = self::getUInt2d($this->data, $this->pos + 2);
44✔
2962
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
44✔
2963

2964
        // move stream pointer to next record
2965
        $this->pos += 4 + $length;
44✔
2966

2967
        // offset: 0; size: 2; row index
2968
        $row = self::getUInt2d($recordData, 0);
44✔
2969

2970
        // offset: 2; size: 2; col index
2971
        $column = self::getUInt2d($recordData, 2);
44✔
2972
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
44✔
2973
        $cellCoordinate = $columnString . ($row + 1);
44✔
2974

2975
        // offset: 20: size: variable; formula structure
2976
        $formulaStructure = substr($recordData, 20);
44✔
2977

2978
        // offset: 14: size: 2; option flags, recalculate always, recalculate on open etc.
2979
        $options = self::getUInt2d($recordData, 14);
44✔
2980

2981
        // bit: 0; mask: 0x0001; 1 = recalculate always
2982
        // bit: 1; mask: 0x0002; 1 = calculate on open
2983
        // bit: 2; mask: 0x0008; 1 = part of a shared formula
2984
        $isPartOfSharedFormula = (bool) (0x0008 & $options);
44✔
2985

2986
        // WARNING:
2987
        // We can apparently not rely on $isPartOfSharedFormula. Even when $isPartOfSharedFormula = true
2988
        // the formula data may be ordinary formula data, therefore we need to check
2989
        // explicitly for the tExp token (0x01)
2990
        $isPartOfSharedFormula = $isPartOfSharedFormula && ord($formulaStructure[2]) == 0x01;
44✔
2991

2992
        if ($isPartOfSharedFormula) {
44✔
2993
            // part of shared formula which means there will be a formula with a tExp token and nothing else
2994
            // get the base cell, grab tExp token
2995
            $baseRow = self::getUInt2d($formulaStructure, 3);
1✔
2996
            $baseCol = self::getUInt2d($formulaStructure, 5);
1✔
2997
            $this->baseCell = Coordinate::stringFromColumnIndex($baseCol + 1) . ($baseRow + 1);
1✔
2998
        }
2999

3000
        // Read cell?
3001
        if ($this->cachedReadFilter->readCell($columnString, $row + 1, $this->phpSheetTitle)) {
44✔
3002
            if ($isPartOfSharedFormula) {
44✔
3003
                // formula is added to this cell after the sheet has been read
3004
                $this->sharedFormulaParts[$cellCoordinate] = $this->baseCell;
1✔
3005
            }
3006

3007
            // offset: 16: size: 4; not used
3008

3009
            // offset: 4; size: 2; XF index
3010
            $xfIndex = self::getUInt2d($recordData, 4);
44✔
3011

3012
            // offset: 6; size: 8; result of the formula
3013
            $resultType = ord($recordData[6]);
44✔
3014
            $isSpecialResult = (ord($recordData[12]) == 255) && (ord($recordData[13]) == 255);
44✔
3015
            if (($resultType == 0) && $isSpecialResult) {
44✔
3016
                // String formula. Result follows in appended STRING record
3017
                $dataType = DataType::TYPE_STRING;
11✔
3018

3019
                // read possible SHAREDFMLA record
3020
                $code = self::getUInt2d($this->data, $this->pos);
11✔
3021
                if ($code == self::XLS_TYPE_SHAREDFMLA) {
11✔
3022
                    $this->readSharedFmla();
×
3023
                }
3024

3025
                // read STRING record
3026
                $value = $this->readString();
11✔
3027
            } elseif (($resultType == 1) && $isSpecialResult) {
38✔
3028
                // Boolean formula. Result is in +2; 0=false, 1=true
3029
                $dataType = DataType::TYPE_BOOL;
3✔
3030
                $value = (bool) ord($recordData[8]);
3✔
3031
            } elseif (($resultType == 2) && $isSpecialResult) {
36✔
3032
                // Error formula. Error code is in +2
3033
                $dataType = DataType::TYPE_ERROR;
11✔
3034
                $value = Xls\ErrorCode::lookup(ord($recordData[8]));
11✔
3035
            } elseif (($resultType == 3) && $isSpecialResult) {
36✔
3036
                // Formula result is a null string
3037
                $dataType = DataType::TYPE_NULL;
2✔
3038
                $value = '';
2✔
3039
            } else {
3040
                // forumla result is a number, first 14 bytes like _NUMBER record
3041
                $dataType = DataType::TYPE_NUMERIC;
36✔
3042
                $value = self::extractNumber(substr($recordData, 6, 8));
36✔
3043
            }
3044

3045
            $cell = $this->phpSheet->getCell($cellCoordinate);
44✔
3046
            if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
44✔
3047
                // add cell style
3048
                $cell->setXfIndexNoUpdate($this->mapCellXfIndex[$xfIndex]);
42✔
3049
            }
3050

3051
            // store the formula
3052
            if (!$isPartOfSharedFormula) {
44✔
3053
                // not part of shared formula
3054
                // add cell value. If we can read formula, populate with formula, otherwise just used cached value
3055
                try {
3056
                    if ($this->version != self::XLS_BIFF8) {
44✔
3057
                        throw new Exception('Not BIFF8. Can only read BIFF8 formulas');
1✔
3058
                    }
3059
                    $formula = $this->getFormulaFromStructure($formulaStructure); // get formula in human language
43✔
3060
                    $cell->setValueExplicit('=' . $formula, DataType::TYPE_FORMULA);
43✔
3061
                } catch (PhpSpreadsheetException) {
2✔
3062
                    $cell->setValueExplicit($value, $dataType);
2✔
3063
                }
3064
            } else {
3065
                if ($this->version == self::XLS_BIFF8) {
1✔
3066
                    // do nothing at this point, formula id added later in the code
3067
                } else {
3068
                    $cell->setValueExplicit($value, $dataType);
×
3069
                }
3070
            }
3071

3072
            // store the cached calculated value
3073
            $cell->setCalculatedValue($value, $dataType === DataType::TYPE_NUMERIC);
44✔
3074
        }
3075
    }
3076

3077
    /**
3078
     * Read a SHAREDFMLA record. This function just stores the binary shared formula in the reader,
3079
     * which usually contains relative references.
3080
     * These will be used to construct the formula in each shared formula part after the sheet is read.
3081
     */
3082
    protected function readSharedFmla(): void
1✔
3083
    {
3084
        $length = self::getUInt2d($this->data, $this->pos + 2);
1✔
3085
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
1✔
3086

3087
        // move stream pointer to next record
3088
        $this->pos += 4 + $length;
1✔
3089

3090
        // offset: 0, size: 6; cell range address of the area used by the shared formula, not used for anything
3091
        //$cellRange = substr($recordData, 0, 6);
3092
        //$cellRange = Xls\Biff5::readBIFF5CellRangeAddressFixed($cellRange); // note: even BIFF8 uses BIFF5 syntax
3093

3094
        // offset: 6, size: 1; not used
3095

3096
        // offset: 7, size: 1; number of existing FORMULA records for this shared formula
3097
        //$no = ord($recordData[7]);
3098

3099
        // offset: 8, size: var; Binary token array of the shared formula
3100
        $formula = substr($recordData, 8);
1✔
3101

3102
        // at this point we only store the shared formula for later use
3103
        $this->sharedFormulas[$this->baseCell] = $formula;
1✔
3104
    }
3105

3106
    /**
3107
     * Read a STRING record from current stream position and advance the stream pointer to next record
3108
     * This record is used for storing result from FORMULA record when it is a string, and
3109
     * it occurs directly after the FORMULA record.
3110
     *
3111
     * @return string The string contents as UTF-8
3112
     */
3113
    protected function readString(): string
11✔
3114
    {
3115
        $length = self::getUInt2d($this->data, $this->pos + 2);
11✔
3116
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
11✔
3117

3118
        // move stream pointer to next record
3119
        $this->pos += 4 + $length;
11✔
3120

3121
        if ($this->version == self::XLS_BIFF8) {
11✔
3122
            $string = self::readUnicodeStringLong($recordData);
11✔
3123
            $value = $string['value'];
11✔
3124
        } else {
3125
            $string = $this->readByteStringLong($recordData);
×
3126
            $value = $string['value'];
×
3127
        }
3128
        /** @var string $value */
3129

3130
        return $value;
11✔
3131
    }
3132

3133
    /**
3134
     * Read BOOLERR record
3135
     * This record represents a Boolean value or error value
3136
     * cell.
3137
     *
3138
     * --    "OpenOffice.org's Documentation of the Microsoft
3139
     *         Excel File Format"
3140
     */
3141
    protected function readBoolErr(): void
11✔
3142
    {
3143
        $length = self::getUInt2d($this->data, $this->pos + 2);
11✔
3144
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
11✔
3145

3146
        // move stream pointer to next record
3147
        $this->pos += 4 + $length;
11✔
3148

3149
        // offset: 0; size: 2; row index
3150
        $row = self::getUInt2d($recordData, 0);
11✔
3151

3152
        // offset: 2; size: 2; column index
3153
        $column = self::getUInt2d($recordData, 2);
11✔
3154
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
11✔
3155
        $cellCoordinate = $columnString . ($row + 1);
11✔
3156

3157
        // Read cell?
3158
        if ($this->cachedReadFilter->readCell($columnString, $row + 1, $this->phpSheetTitle)) {
11✔
3159
            // offset: 4; size: 2; index to XF record
3160
            $xfIndex = self::getUInt2d($recordData, 4);
11✔
3161

3162
            // offset: 6; size: 1; the boolean value or error value
3163
            $boolErr = ord($recordData[6]);
11✔
3164

3165
            // offset: 7; size: 1; 0=boolean; 1=error
3166
            $isError = ord($recordData[7]);
11✔
3167

3168
            $cell = $this->phpSheet->getCell($cellCoordinate);
11✔
3169
            if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
11✔
3170
                // add cell style
3171
                $cell->setXfIndexNoUpdate($this->mapCellXfIndex[$xfIndex]);
9✔
3172
            }
3173
            switch ($isError) {
3174
                case 0: // boolean
11✔
3175
                    $value = (bool) $boolErr;
11✔
3176

3177
                    // add cell value
3178
                    $cell->setValueExplicit($value, DataType::TYPE_BOOL);
11✔
3179

3180
                    break;
11✔
3181
                case 1: // error type
×
3182
                    $value = Xls\ErrorCode::lookup($boolErr);
×
3183

3184
                    // add cell value
3185
                    $cell->setValueExplicit($value, DataType::TYPE_ERROR);
×
3186

3187
                    break;
×
3188
            }
3189
        }
3190
    }
3191

3192
    /**
3193
     * Read MULBLANK record
3194
     * This record represents a cell range of empty cells. All
3195
     * cells are located in the same row.
3196
     *
3197
     * --    "OpenOffice.org's Documentation of the Microsoft
3198
     *         Excel File Format"
3199
     */
3200
    protected function readMulBlank(): void
26✔
3201
    {
3202
        $length = self::getUInt2d($this->data, $this->pos + 2);
26✔
3203
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
26✔
3204

3205
        // move stream pointer to next record
3206
        $this->pos += 4 + $length;
26✔
3207

3208
        // offset: 0; size: 2; index to row
3209
        $row = self::getUInt2d($recordData, 0);
26✔
3210

3211
        // offset: 2; size: 2; index to first column
3212
        $fc = self::getUInt2d($recordData, 2);
26✔
3213

3214
        // offset: 4; size: 2 x nc; list of indexes to XF records
3215
        // add style information
3216
        if (!$this->readDataOnly && $this->readEmptyCells) {
26✔
3217
            $rowIndex = $row + 1;
24✔
3218
            for ($i = 0; $i < $length / 2 - 3; ++$i) {
24✔
3219
                $columnString = Coordinate::stringFromColumnIndex($fc + $i + 1);
24✔
3220

3221
                // Read cell?
3222
                if ($this->cachedReadFilter->readCell($columnString, $rowIndex, $this->phpSheetTitle)) {
24✔
3223
                    $xfIndex = self::getUInt2d($recordData, 4 + 2 * $i);
24✔
3224
                    if (isset($this->mapCellXfIndex[$xfIndex])) {
24✔
3225
                        $this->phpSheet->getCell($columnString . $rowIndex)->setXfIndexNoUpdate($this->mapCellXfIndex[$xfIndex]);
24✔
3226
                    }
3227
                }
3228
            }
3229
        }
3230

3231
        // offset: 6; size 2; index to last column (not needed)
3232
    }
3233

3234
    /**
3235
     * Read LABEL record
3236
     * This record represents a cell that contains a string. In
3237
     * BIFF8 it is usually replaced by the LABELSST record.
3238
     * Excel still uses this record, if it copies unformatted
3239
     * text cells to the clipboard.
3240
     *
3241
     * --    "OpenOffice.org's Documentation of the Microsoft
3242
     *         Excel File Format"
3243
     */
3244
    protected function readLabel(): void
4✔
3245
    {
3246
        $length = self::getUInt2d($this->data, $this->pos + 2);
4✔
3247
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4✔
3248

3249
        // move stream pointer to next record
3250
        $this->pos += 4 + $length;
4✔
3251

3252
        // offset: 0; size: 2; index to row
3253
        $row = self::getUInt2d($recordData, 0);
4✔
3254

3255
        // offset: 2; size: 2; index to column
3256
        $column = self::getUInt2d($recordData, 2);
4✔
3257
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
4✔
3258
        $cellCoordinate = $columnString . ($row + 1);
4✔
3259

3260
        // Read cell?
3261
        if ($this->cachedReadFilter->readCell($columnString, $row + 1, $this->phpSheetTitle)) {
4✔
3262
            // offset: 4; size: 2; XF index
3263
            $xfIndex = self::getUInt2d($recordData, 4);
4✔
3264

3265
            // add cell value
3266
            // todo: what if string is very long? continue record
3267
            if ($this->version == self::XLS_BIFF8) {
4✔
3268
                $string = self::readUnicodeStringLong(substr($recordData, 6));
2✔
3269
                $value = $string['value'];
2✔
3270
            } else {
3271
                $string = $this->readByteStringLong(substr($recordData, 6));
2✔
3272
                $value = $string['value'];
2✔
3273
            }
3274
            /** @var string $value */
3275
            if ($this->readEmptyCells || trim($value) !== '') {
4✔
3276
                $cell = $this->phpSheet->getCell($cellCoordinate);
4✔
3277
                if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
4✔
3278
                    // add cell style
3279
                    $cell->setXfIndexNoUpdate($this->mapCellXfIndex[$xfIndex]);
4✔
3280
                }
3281
                $cell->setValueExplicit($value, DataType::TYPE_STRING);
4✔
3282
            }
3283
        }
3284
    }
3285

3286
    /**
3287
     * Read BLANK record.
3288
     */
3289
    protected function readBlank(): void
27✔
3290
    {
3291
        $length = self::getUInt2d($this->data, $this->pos + 2);
27✔
3292
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
27✔
3293

3294
        // move stream pointer to next record
3295
        $this->pos += 4 + $length;
27✔
3296

3297
        // offset: 0; size: 2; row index
3298
        $row = self::getUInt2d($recordData, 0);
27✔
3299

3300
        // offset: 2; size: 2; col index
3301
        $col = self::getUInt2d($recordData, 2);
27✔
3302
        $columnString = Coordinate::stringFromColumnIndex($col + 1);
27✔
3303

3304
        $rowIndex = $row + 1;
27✔
3305

3306
        // Read cell?
3307
        if ($this->cachedReadFilter->readCell($columnString, $rowIndex, $this->phpSheetTitle)) {
27✔
3308
            // offset: 4; size: 2; XF index
3309
            $xfIndex = self::getUInt2d($recordData, 4);
27✔
3310

3311
            // add style information
3312
            if (!$this->readDataOnly && $this->readEmptyCells && isset($this->mapCellXfIndex[$xfIndex])) {
27✔
3313
                $this->phpSheet->getCell($columnString . $rowIndex)->setXfIndexNoUpdate($this->mapCellXfIndex[$xfIndex]);
27✔
3314
            }
3315
        }
3316
    }
3317

3318
    /**
3319
     * Read MSODRAWING record.
3320
     */
3321
    protected function readMsoDrawing(): void
19✔
3322
    {
3323
        //$length = self::getUInt2d($this->data, $this->pos + 2);
3324

3325
        // get spliced record data
3326
        $splicedRecordData = $this->getSplicedRecordData();
19✔
3327
        $recordData = $splicedRecordData['recordData'];
19✔
3328

3329
        $this->drawingData .= StringHelper::convertToString($recordData);
19✔
3330
    }
3331

3332
    /**
3333
     * Read OBJ record.
3334
     */
3335
    protected function readObj(): void
15✔
3336
    {
3337
        $length = self::getUInt2d($this->data, $this->pos + 2);
15✔
3338
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
15✔
3339

3340
        // move stream pointer to next record
3341
        $this->pos += 4 + $length;
15✔
3342

3343
        if ($this->readDataOnly || $this->version != self::XLS_BIFF8) {
15✔
3344
            return;
1✔
3345
        }
3346

3347
        // recordData consists of an array of subrecords looking like this:
3348
        //    ft: 2 bytes; ftCmo type (0x15)
3349
        //    cb: 2 bytes; size in bytes of ftCmo data
3350
        //    ot: 2 bytes; Object Type
3351
        //    id: 2 bytes; Object id number
3352
        //    grbit: 2 bytes; Option Flags
3353
        //    data: var; subrecord data
3354

3355
        // for now, we are just interested in the second subrecord containing the object type
3356
        $ftCmoType = self::getUInt2d($recordData, 0);
14✔
3357
        $cbCmoSize = self::getUInt2d($recordData, 2);
14✔
3358
        $otObjType = self::getUInt2d($recordData, 4);
14✔
3359
        $idObjID = self::getUInt2d($recordData, 6);
14✔
3360
        $grbitOpts = self::getUInt2d($recordData, 6);
14✔
3361

3362
        $this->objs[] = [
14✔
3363
            'ftCmoType' => $ftCmoType,
14✔
3364
            'cbCmoSize' => $cbCmoSize,
14✔
3365
            'otObjType' => $otObjType,
14✔
3366
            'idObjID' => $idObjID,
14✔
3367
            'grbitOpts' => $grbitOpts,
14✔
3368
        ];
14✔
3369
        $this->textObjRef = $idObjID;
14✔
3370
    }
3371

3372
    /**
3373
     * Read WINDOW2 record.
3374
     */
3375
    protected function readWindow2(): void
136✔
3376
    {
3377
        $length = self::getUInt2d($this->data, $this->pos + 2);
136✔
3378
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
136✔
3379

3380
        // move stream pointer to next record
3381
        $this->pos += 4 + $length;
136✔
3382

3383
        // offset: 0; size: 2; option flags
3384
        $options = self::getUInt2d($recordData, 0);
136✔
3385

3386
        // offset: 2; size: 2; index to first visible row
3387
        //$firstVisibleRow = self::getUInt2d($recordData, 2);
3388

3389
        // offset: 4; size: 2; index to first visible colum
3390
        //$firstVisibleColumn = self::getUInt2d($recordData, 4);
3391
        $zoomscaleInPageBreakPreview = 0;
136✔
3392
        $zoomscaleInNormalView = 0;
136✔
3393
        if ($this->version === self::XLS_BIFF8) {
136✔
3394
            // offset:  8; size: 2; not used
3395
            // offset: 10; size: 2; cached magnification factor in page break preview (in percent); 0 = Default (60%)
3396
            // offset: 12; size: 2; cached magnification factor in normal view (in percent); 0 = Default (100%)
3397
            // offset: 14; size: 4; not used
3398
            if (!isset($recordData[10])) {
134✔
3399
                $zoomscaleInPageBreakPreview = 0;
×
3400
            } else {
3401
                $zoomscaleInPageBreakPreview = self::getUInt2d($recordData, 10);
134✔
3402
            }
3403

3404
            if ($zoomscaleInPageBreakPreview === 0) {
134✔
3405
                $zoomscaleInPageBreakPreview = 60;
130✔
3406
            }
3407

3408
            if (!isset($recordData[12])) {
134✔
3409
                $zoomscaleInNormalView = 0;
×
3410
            } else {
3411
                $zoomscaleInNormalView = self::getUInt2d($recordData, 12);
134✔
3412
            }
3413

3414
            if ($zoomscaleInNormalView === 0) {
134✔
3415
                $zoomscaleInNormalView = 100;
54✔
3416
            }
3417
        }
3418

3419
        // bit: 1; mask: 0x0002; 0 = do not show gridlines, 1 = show gridlines
3420
        $showGridlines = (bool) ((0x0002 & $options) >> 1);
136✔
3421
        $this->phpSheet->setShowGridlines($showGridlines);
136✔
3422

3423
        // bit: 2; mask: 0x0004; 0 = do not show headers, 1 = show headers
3424
        $showRowColHeaders = (bool) ((0x0004 & $options) >> 2);
136✔
3425
        $this->phpSheet->setShowRowColHeaders($showRowColHeaders);
136✔
3426

3427
        // bit: 3; mask: 0x0008; 0 = panes are not frozen, 1 = panes are frozen
3428
        $this->frozen = (bool) ((0x0008 & $options) >> 3);
136✔
3429

3430
        // bit: 6; mask: 0x0040; 0 = columns from left to right, 1 = columns from right to left
3431
        $this->phpSheet->setRightToLeft((bool) ((0x0040 & $options) >> 6));
136✔
3432

3433
        // bit: 10; mask: 0x0400; 0 = sheet not active, 1 = sheet active
3434
        $isActive = (bool) ((0x0400 & $options) >> 10);
136✔
3435
        if ($isActive) {
136✔
3436
            $this->spreadsheet->setActiveSheetIndex($this->spreadsheet->getIndex($this->phpSheet));
132✔
3437
            $this->activeSheetSet = true;
132✔
3438
        }
3439

3440
        // bit: 11; mask: 0x0800; 0 = normal view, 1 = page break view
3441
        $isPageBreakPreview = (bool) ((0x0800 & $options) >> 11);
136✔
3442

3443
        //FIXME: set $firstVisibleRow and $firstVisibleColumn
3444

3445
        if ($this->phpSheet->getSheetView()->getView() !== SheetView::SHEETVIEW_PAGE_LAYOUT) {
136✔
3446
            //NOTE: this setting is inferior to page layout view(Excel2007-)
3447
            $view = $isPageBreakPreview ? SheetView::SHEETVIEW_PAGE_BREAK_PREVIEW : SheetView::SHEETVIEW_NORMAL;
136✔
3448
            $this->phpSheet->getSheetView()->setView($view);
136✔
3449
            if ($this->version === self::XLS_BIFF8) {
136✔
3450
                $zoomScale = $isPageBreakPreview ? $zoomscaleInPageBreakPreview : $zoomscaleInNormalView;
134✔
3451
                $this->phpSheet->getSheetView()->setZoomScale($zoomScale);
134✔
3452
                $this->phpSheet->getSheetView()->setZoomScaleNormal($zoomscaleInNormalView);
134✔
3453
            }
3454
        }
3455
    }
3456

3457
    /**
3458
     * Read PLV Record(Created by Excel2007 or upper).
3459
     */
3460
    protected function readPageLayoutView(): void
121✔
3461
    {
3462
        $length = self::getUInt2d($this->data, $this->pos + 2);
121✔
3463
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
121✔
3464

3465
        // move stream pointer to next record
3466
        $this->pos += 4 + $length;
121✔
3467

3468
        // offset: 0; size: 2; rt
3469
        //->ignore
3470
        //$rt = self::getUInt2d($recordData, 0);
3471
        // offset: 2; size: 2; grbitfr
3472
        //->ignore
3473
        //$grbitFrt = self::getUInt2d($recordData, 2);
3474
        // offset: 4; size: 8; reserved
3475
        //->ignore
3476

3477
        // offset: 12; size 2; zoom scale
3478
        $wScalePLV = self::getUInt2d($recordData, 12);
121✔
3479
        // offset: 14; size 2; grbit
3480
        $grbit = self::getUInt2d($recordData, 14);
121✔
3481

3482
        // decomprise grbit
3483
        $fPageLayoutView = $grbit & 0x01;
121✔
3484
        //$fRulerVisible = ($grbit >> 1) & 0x01; //no support
3485
        //$fWhitespaceHidden = ($grbit >> 3) & 0x01; //no support
3486

3487
        if ($fPageLayoutView === 1) {
121✔
3488
            $this->phpSheet->getSheetView()->setView(SheetView::SHEETVIEW_PAGE_LAYOUT);
×
3489
            $this->phpSheet->getSheetView()->setZoomScale($wScalePLV); //set by Excel2007 only if SHEETVIEW_PAGE_LAYOUT
×
3490
        }
3491
        //otherwise, we cannot know whether SHEETVIEW_PAGE_LAYOUT or SHEETVIEW_PAGE_BREAK_PREVIEW.
3492
    }
3493

3494
    /**
3495
     * Read SCL record.
3496
     */
3497
    protected function readScl(): void
6✔
3498
    {
3499
        $length = self::getUInt2d($this->data, $this->pos + 2);
6✔
3500
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
6✔
3501

3502
        // move stream pointer to next record
3503
        $this->pos += 4 + $length;
6✔
3504

3505
        // offset: 0; size: 2; numerator of the view magnification
3506
        $numerator = self::getUInt2d($recordData, 0);
6✔
3507

3508
        // offset: 2; size: 2; numerator of the view magnification
3509
        $denumerator = self::getUInt2d($recordData, 2);
6✔
3510

3511
        // set the zoom scale (in percent)
3512
        $this->phpSheet->getSheetView()->setZoomScale($numerator * 100 / $denumerator);
6✔
3513
    }
3514

3515
    /**
3516
     * Read PANE record.
3517
     */
3518
    protected function readPane(): void
8✔
3519
    {
3520
        $length = self::getUInt2d($this->data, $this->pos + 2);
8✔
3521
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
8✔
3522

3523
        // move stream pointer to next record
3524
        $this->pos += 4 + $length;
8✔
3525

3526
        if (!$this->readDataOnly) {
8✔
3527
            // offset: 0; size: 2; position of vertical split
3528
            $px = self::getUInt2d($recordData, 0);
8✔
3529

3530
            // offset: 2; size: 2; position of horizontal split
3531
            $py = self::getUInt2d($recordData, 2);
8✔
3532

3533
            // offset: 4; size: 2; top most visible row in the bottom pane
3534
            $rwTop = self::getUInt2d($recordData, 4);
8✔
3535

3536
            // offset: 6; size: 2; first visible left column in the right pane
3537
            $colLeft = self::getUInt2d($recordData, 6);
8✔
3538

3539
            if ($this->frozen) {
8✔
3540
                // frozen panes
3541
                $cell = Coordinate::stringFromColumnIndex($px + 1) . ($py + 1);
8✔
3542
                $topLeftCell = Coordinate::stringFromColumnIndex($colLeft + 1) . ($rwTop + 1);
8✔
3543
                $this->phpSheet->freezePane($cell, $topLeftCell);
8✔
3544
            }
3545
            // unfrozen panes; split windows; not supported by PhpSpreadsheet core
3546
        }
3547
    }
3548

3549
    private const REGEX_WHOLE_COLUMN = '/^([A-Z]+1\:[A-Z]+)'
3550
        . '(' . AddressRange::MAX_ROW_XLS_OLD . '|' . AddressRange::MAX_ROW_XLS . ')'
3551
        . '$/';
3552
    private const REGEX_WHOLE_COLUMN_REPLACE = '${1}' . AddressRange::MAX_ROW;
3553
    private const REGEX_WHOLE_ROW = '/^(A\d+\:)'
3554
        . AddressRange::MAX_COLUMN_XLS
3555
        . '(\d+)$/';
3556
    private const REGEX_WHOLE_ROW_REPLACE = '${1}'
3557
        . AddressRange::MAX_COLUMN
3558
        . '${2}';
3559

3560
    /**
3561
     * Read SELECTION record. There is one such record for each pane in the sheet.
3562
     */
3563
    protected function readSelection(): string
133✔
3564
    {
3565
        $length = self::getUInt2d($this->data, $this->pos + 2);
133✔
3566
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
133✔
3567
        $selectedCells = '';
133✔
3568

3569
        // move stream pointer to next record
3570
        $this->pos += 4 + $length;
133✔
3571

3572
        if (!$this->readDataOnly) {
133✔
3573
            // offset: 0; size: 1; pane identifier
3574
            //$paneId = ord($recordData[0]);
3575

3576
            // offset: 1; size: 2; index to row of the active cell
3577
            //$r = self::getUInt2d($recordData, 1);
3578

3579
            // offset: 3; size: 2; index to column of the active cell
3580
            //$c = self::getUInt2d($recordData, 3);
3581

3582
            // offset: 5; size: 2; index into the following cell range list to the
3583
            //  entry that contains the active cell
3584
            //$index = self::getUInt2d($recordData, 5);
3585

3586
            // offset: 7; size: var; cell range address list containing all selected cell ranges
3587
            $data = substr($recordData, 7);
131✔
3588
            $cellRangeAddressList = Xls\Biff5::readBIFF5CellRangeAddressList($data); // note: also BIFF8 uses BIFF5 syntax
131✔
3589

3590
            $selectedCells = $cellRangeAddressList['cellRangeAddresses'][0];
131✔
3591

3592
            // first row '1' + last row '16384' or '65536' indicates that full column is selected (apparently also in BIFF8!)
3593
            if (Preg::isMatch(self::REGEX_WHOLE_COLUMN, $selectedCells)) {
131✔
3594
                $selectedCells = Preg::replace(self::REGEX_WHOLE_COLUMN, self::REGEX_WHOLE_COLUMN_REPLACE, $selectedCells);
1✔
3595
            }
3596

3597
            // first column 'A' + last column 'IV' indicates that full row is selected
3598
            if (Preg::isMatch(self::REGEX_WHOLE_ROW, $selectedCells)) {
131✔
3599
                $selectedCells = Preg::replace(self::REGEX_WHOLE_ROW, self::REGEX_WHOLE_ROW_REPLACE, $selectedCells);
3✔
3600
            }
3601

3602
            $this->phpSheet->setSelectedCells($selectedCells);
131✔
3603
        }
3604

3605
        return $selectedCells;
133✔
3606
    }
3607

3608
    private function includeCellRangeFiltered(string $cellRangeAddress): bool
17✔
3609
    {
3610
        $includeCellRange = false;
17✔
3611
        $rangeBoundaries = Coordinate::getRangeBoundaries($cellRangeAddress);
17✔
3612
        StringHelper::stringIncrement($rangeBoundaries[1][0]);
17✔
3613
        for ($row = $rangeBoundaries[0][1]; $row <= $rangeBoundaries[1][1]; ++$row) {
17✔
3614
            for ($column = $rangeBoundaries[0][0]; $column != $rangeBoundaries[1][0]; StringHelper::stringIncrement($column)) {
17✔
3615
                if ($this->cachedReadFilter->readCell($column, $row, $this->phpSheetTitle)) {
17✔
3616
                    $includeCellRange = true;
17✔
3617

3618
                    break 2;
17✔
3619
                }
3620
            }
3621
        }
3622

3623
        return $includeCellRange;
17✔
3624
    }
3625

3626
    /**
3627
     * MERGEDCELLS.
3628
     *
3629
     * This record contains the addresses of merged cell ranges
3630
     * in the current sheet.
3631
     *
3632
     * --    "OpenOffice.org's Documentation of the Microsoft
3633
     *         Excel File Format"
3634
     */
3635
    protected function readMergedCells(): void
19✔
3636
    {
3637
        $length = self::getUInt2d($this->data, $this->pos + 2);
19✔
3638
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
19✔
3639

3640
        // move stream pointer to next record
3641
        $this->pos += 4 + $length;
19✔
3642

3643
        if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) {
19✔
3644
            $cellRangeAddressList = Xls\Biff8::readBIFF8CellRangeAddressList($recordData);
17✔
3645
            foreach ($cellRangeAddressList['cellRangeAddresses'] as $cellRangeAddress) {
17✔
3646
                /** @var string $cellRangeAddress */
3647
                if (
3648
                    (str_contains($cellRangeAddress, ':'))
17✔
3649
                    && ($this->includeCellRangeFiltered($cellRangeAddress))
17✔
3650
                ) {
3651
                    $this->phpSheet->mergeCells($cellRangeAddress, Worksheet::MERGE_CELL_CONTENT_HIDE);
17✔
3652
                }
3653
            }
3654
        }
3655
    }
3656

3657
    /**
3658
     * Read HYPERLINK record.
3659
     */
3660
    protected function readHyperLink(): void
7✔
3661
    {
3662
        $length = self::getUInt2d($this->data, $this->pos + 2);
7✔
3663
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
7✔
3664

3665
        // move stream pointer forward to next record
3666
        $this->pos += 4 + $length;
7✔
3667

3668
        if (!$this->readDataOnly) {
7✔
3669
            // offset: 0; size: 8; cell range address of all cells containing this hyperlink
3670
            try {
3671
                $cellRange = Xls\Biff8::readBIFF8CellRangeAddressFixed($recordData);
7✔
3672
            } catch (PhpSpreadsheetException) {
×
3673
                return;
×
3674
            }
3675

3676
            // offset: 8, size: 16; GUID of StdLink
3677

3678
            // offset: 24, size: 4; unknown value
3679

3680
            // offset: 28, size: 4; option flags
3681
            // bit: 0; mask: 0x00000001; 0 = no link or extant, 1 = file link or URL
3682
            $isFileLinkOrUrl = (0x00000001 & self::getUInt2d($recordData, 28)) >> 0;
7✔
3683

3684
            // bit: 1; mask: 0x00000002; 0 = relative path, 1 = absolute path or URL
3685
            //$isAbsPathOrUrl = (0x00000001 & self::getUInt2d($recordData, 28)) >> 1;
3686

3687
            // bit: 2 (and 4); mask: 0x00000014; 0 = no description
3688
            $hasDesc = (0x00000014 & self::getUInt2d($recordData, 28)) >> 2;
7✔
3689

3690
            // bit: 3; mask: 0x00000008; 0 = no text, 1 = has text
3691
            $hasText = (0x00000008 & self::getUInt2d($recordData, 28)) >> 3;
7✔
3692

3693
            // bit: 7; mask: 0x00000080; 0 = no target frame, 1 = has target frame
3694
            $hasFrame = (0x00000080 & self::getUInt2d($recordData, 28)) >> 7;
7✔
3695

3696
            // bit: 8; mask: 0x00000100; 0 = file link or URL, 1 = UNC path (inc. server name)
3697
            $isUNC = (0x00000100 & self::getUInt2d($recordData, 28)) >> 8;
7✔
3698

3699
            // offset within record data
3700
            $offset = 32;
7✔
3701

3702
            if ($hasDesc) {
7✔
3703
                // offset: 32; size: var; character count of description text
3704
                $dl = self::getInt4d($recordData, 32);
3✔
3705
                // offset: 36; size: var; character array of description text, no Unicode string header, always 16-bit characters, zero terminated
3706
                //$desc = self::encodeUTF16(substr($recordData, 36, 2 * ($dl - 1)), false);
3707
                $offset += 4 + 2 * $dl;
3✔
3708
            }
3709
            if ($hasFrame) {
7✔
3710
                $fl = self::getInt4d($recordData, $offset);
×
3711
                $offset += 4 + 2 * $fl;
×
3712
            }
3713

3714
            // detect type of hyperlink (there are 4 types)
3715
            $hyperlinkType = null;
7✔
3716

3717
            if ($isUNC) {
7✔
3718
                $hyperlinkType = 'UNC';
×
3719
            } elseif (!$isFileLinkOrUrl) {
7✔
3720
                $hyperlinkType = 'workbook';
4✔
3721
            } elseif (ord($recordData[$offset]) == 0x03) {
7✔
3722
                $hyperlinkType = 'local';
×
3723
            } elseif (ord($recordData[$offset]) == 0xE0) {
7✔
3724
                $hyperlinkType = 'URL';
7✔
3725
            }
3726

3727
            switch ($hyperlinkType) {
3728
                case 'URL':
7✔
3729
                    // section 5.58.2: Hyperlink containing a URL
3730
                    // e.g. http://example.org/index.php
3731

3732
                    // offset: var; size: 16; GUID of URL Moniker
3733
                    $offset += 16;
7✔
3734
                    // offset: var; size: 4; size (in bytes) of character array of the URL including trailing zero word
3735
                    $us = self::getInt4d($recordData, $offset);
7✔
3736
                    $offset += 4;
7✔
3737
                    // offset: var; size: $us; character array of the URL, no Unicode string header, always 16-bit characters, zero-terminated
3738
                    $url = self::encodeUTF16(substr($recordData, $offset, $us - 2), false);
7✔
3739
                    $nullOffset = strpos($url, chr(0x00));
7✔
3740
                    if ($nullOffset) {
7✔
3741
                        $url = substr($url, 0, $nullOffset);
3✔
3742
                    }
3743
                    $url .= $hasText ? '#' : '';
7✔
3744
                    $offset += $us;
7✔
3745

3746
                    break;
7✔
3747
                case 'local':
4✔
3748
                    // section 5.58.3: Hyperlink to local file
3749
                    // examples:
3750
                    //   mydoc.txt
3751
                    //   ../../somedoc.xls#Sheet!A1
3752

3753
                    // offset: var; size: 16; GUI of File Moniker
3754
                    $offset += 16;
×
3755

3756
                    // offset: var; size: 2; directory up-level count.
3757
                    $upLevelCount = self::getUInt2d($recordData, $offset);
×
3758
                    $offset += 2;
×
3759

3760
                    // offset: var; size: 4; character count of the shortened file path and name, including trailing zero word
3761
                    $sl = self::getInt4d($recordData, $offset);
×
3762
                    $offset += 4;
×
3763

3764
                    // offset: var; size: sl; character array of the shortened file path and name in 8.3-DOS-format (compressed Unicode string)
3765
                    $shortenedFilePath = substr($recordData, $offset, $sl);
×
3766
                    $shortenedFilePath = self::encodeUTF16($shortenedFilePath, true);
×
3767
                    $shortenedFilePath = substr($shortenedFilePath, 0, -1); // remove trailing zero
×
3768

3769
                    $offset += $sl;
×
3770

3771
                    // offset: var; size: 24; unknown sequence
3772
                    $offset += 24;
×
3773

3774
                    // extended file path
3775
                    // offset: var; size: 4; size of the following file link field including string lenth mark
3776
                    $sz = self::getInt4d($recordData, $offset);
×
3777
                    $offset += 4;
×
3778

3779
                    $extendedFilePath = '';
×
3780
                    // only present if $sz > 0
3781
                    if ($sz > 0) {
×
3782
                        // offset: var; size: 4; size of the character array of the extended file path and name
3783
                        $xl = self::getInt4d($recordData, $offset);
×
3784
                        $offset += 4;
×
3785

3786
                        // offset: var; size 2; unknown
3787
                        $offset += 2;
×
3788

3789
                        // offset: var; size $xl; character array of the extended file path and name.
3790
                        $extendedFilePath = substr($recordData, $offset, $xl);
×
3791
                        $extendedFilePath = self::encodeUTF16($extendedFilePath, false);
×
3792
                        $offset += $xl;
×
3793
                    }
3794

3795
                    // construct the path
3796
                    $url = str_repeat('..\\', $upLevelCount);
×
3797
                    $url .= ($sz > 0) ? $extendedFilePath : $shortenedFilePath; // use extended path if available
×
3798
                    $url .= $hasText ? '#' : '';
×
3799

3800
                    break;
×
3801
                case 'UNC':
4✔
3802
                    // section 5.58.4: Hyperlink to a File with UNC (Universal Naming Convention) Path
3803
                    // todo: implement
3804
                    return;
×
3805
                case 'workbook':
4✔
3806
                    // section 5.58.5: Hyperlink to the Current Workbook
3807
                    // e.g. Sheet2!B1:C2, stored in text mark field
3808
                    $url = 'sheet://';
4✔
3809

3810
                    break;
4✔
3811
                default:
3812
                    return;
×
3813
            }
3814

3815
            if ($hasText) {
7✔
3816
                // offset: var; size: 4; character count of text mark including trailing zero word
3817
                $tl = self::getInt4d($recordData, $offset);
4✔
3818
                $offset += 4;
4✔
3819
                // offset: var; size: var; character array of the text mark without the # sign, no Unicode header, always 16-bit characters, zero-terminated
3820
                $text = self::encodeUTF16(substr($recordData, $offset, 2 * ($tl - 1)), false);
4✔
3821
                $url .= $text;
4✔
3822
            }
3823

3824
            // apply the hyperlink to all the relevant cells
3825
            foreach (Coordinate::extractAllCellReferencesInRange($cellRange) as $coordinate) {
7✔
3826
                $this->phpSheet->getCell($coordinate)->getHyperLink()->setUrl($url);
7✔
3827
            }
3828
        }
3829
    }
3830

3831
    /**
3832
     * Read DATAVALIDATIONS record.
3833
     */
3834
    protected function readDataValidations(): void
5✔
3835
    {
3836
        $length = self::getUInt2d($this->data, $this->pos + 2);
5✔
3837
        //$recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3838

3839
        // move stream pointer forward to next record
3840
        $this->pos += 4 + $length;
5✔
3841
    }
3842

3843
    /**
3844
     * Read DATAVALIDATION record.
3845
     */
3846
    protected function readDataValidation(): void
5✔
3847
    {
3848
        (new Xls\DataValidationHelper())->readDataValidation2($this);
5✔
3849
    }
3850

3851
    /**
3852
     * Read SHEETLAYOUT record. Stores sheet tab color information.
3853
     */
3854
    protected function readSheetLayout(): void
5✔
3855
    {
3856
        $length = self::getUInt2d($this->data, $this->pos + 2);
5✔
3857
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
5✔
3858

3859
        // move stream pointer to next record
3860
        $this->pos += 4 + $length;
5✔
3861

3862
        if (!$this->readDataOnly) {
5✔
3863
            // offset: 0; size: 2; repeated record identifier 0x0862
3864

3865
            // offset: 2; size: 10; not used
3866

3867
            // offset: 12; size: 4; size of record data
3868
            // Excel 2003 uses size of 0x14 (documented), Excel 2007 uses size of 0x28 (not documented?)
3869
            $sz = self::getInt4d($recordData, 12);
5✔
3870

3871
            switch ($sz) {
3872
                case 0x14:
5✔
3873
                    // offset: 16; size: 2; color index for sheet tab
3874
                    $colorIndex = self::getUInt2d($recordData, 16);
1✔
3875
                    /** @var string[] */
3876
                    $color = Xls\Color::map($colorIndex, $this->palette, $this->version);
1✔
3877
                    $this->phpSheet->getTabColor()->setRGB($color['rgb']);
1✔
3878

3879
                    break;
1✔
3880
                case 0x28:
4✔
3881
                    // TODO: Investigate structure for .xls SHEETLAYOUT record as saved by MS Office Excel 2007
3882
                    return;
4✔
3883
            }
3884
        }
3885
    }
3886

3887
    /**
3888
     * Read SHEETPROTECTION record (FEATHEADR).
3889
     */
3890
    protected function readSheetProtection(): void
125✔
3891
    {
3892
        $length = self::getUInt2d($this->data, $this->pos + 2);
125✔
3893
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
125✔
3894

3895
        // move stream pointer to next record
3896
        $this->pos += 4 + $length;
125✔
3897

3898
        if ($this->readDataOnly) {
125✔
3899
            return;
2✔
3900
        }
3901

3902
        // offset: 0; size: 2; repeated record header
3903

3904
        // offset: 2; size: 2; FRT cell reference flag (=0 currently)
3905

3906
        // offset: 4; size: 8; Currently not used and set to 0
3907

3908
        // offset: 12; size: 2; Shared feature type index (2=Enhanced Protetion, 4=SmartTag)
3909
        $isf = self::getUInt2d($recordData, 12);
123✔
3910
        if ($isf != 2) {
123✔
3911
            return;
×
3912
        }
3913

3914
        // offset: 14; size: 1; =1 since this is a feat header
3915

3916
        // offset: 15; size: 4; size of rgbHdrSData
3917

3918
        // rgbHdrSData, assume "Enhanced Protection"
3919
        // offset: 19; size: 2; option flags
3920
        $options = self::getUInt2d($recordData, 19);
123✔
3921

3922
        // bit: 0; mask 0x0001; 1 = user may edit objects, 0 = users must not edit objects
3923
        // Note - do not negate $bool
3924
        $bool = (0x0001 & $options) >> 0;
123✔
3925
        $this->phpSheet->getProtection()->setObjects((bool) $bool);
123✔
3926

3927
        // bit: 1; mask 0x0002; edit scenarios
3928
        // Note - do not negate $bool
3929
        $bool = (0x0002 & $options) >> 1;
123✔
3930
        $this->phpSheet->getProtection()->setScenarios((bool) $bool);
123✔
3931

3932
        // bit: 2; mask 0x0004; format cells
3933
        $bool = (0x0004 & $options) >> 2;
123✔
3934
        $this->phpSheet->getProtection()->setFormatCells(!$bool);
123✔
3935

3936
        // bit: 3; mask 0x0008; format columns
3937
        $bool = (0x0008 & $options) >> 3;
123✔
3938
        $this->phpSheet->getProtection()->setFormatColumns(!$bool);
123✔
3939

3940
        // bit: 4; mask 0x0010; format rows
3941
        $bool = (0x0010 & $options) >> 4;
123✔
3942
        $this->phpSheet->getProtection()->setFormatRows(!$bool);
123✔
3943

3944
        // bit: 5; mask 0x0020; insert columns
3945
        $bool = (0x0020 & $options) >> 5;
123✔
3946
        $this->phpSheet->getProtection()->setInsertColumns(!$bool);
123✔
3947

3948
        // bit: 6; mask 0x0040; insert rows
3949
        $bool = (0x0040 & $options) >> 6;
123✔
3950
        $this->phpSheet->getProtection()->setInsertRows(!$bool);
123✔
3951

3952
        // bit: 7; mask 0x0080; insert hyperlinks
3953
        $bool = (0x0080 & $options) >> 7;
123✔
3954
        $this->phpSheet->getProtection()->setInsertHyperlinks(!$bool);
123✔
3955

3956
        // bit: 8; mask 0x0100; delete columns
3957
        $bool = (0x0100 & $options) >> 8;
123✔
3958
        $this->phpSheet->getProtection()->setDeleteColumns(!$bool);
123✔
3959

3960
        // bit: 9; mask 0x0200; delete rows
3961
        $bool = (0x0200 & $options) >> 9;
123✔
3962
        $this->phpSheet->getProtection()->setDeleteRows(!$bool);
123✔
3963

3964
        // bit: 10; mask 0x0400; select locked cells
3965
        // Note that this is opposite of most of above.
3966
        $bool = (0x0400 & $options) >> 10;
123✔
3967
        $this->phpSheet->getProtection()->setSelectLockedCells((bool) $bool);
123✔
3968

3969
        // bit: 11; mask 0x0800; sort cell range
3970
        $bool = (0x0800 & $options) >> 11;
123✔
3971
        $this->phpSheet->getProtection()->setSort(!$bool);
123✔
3972

3973
        // bit: 12; mask 0x1000; auto filter
3974
        $bool = (0x1000 & $options) >> 12;
123✔
3975
        $this->phpSheet->getProtection()->setAutoFilter(!$bool);
123✔
3976

3977
        // bit: 13; mask 0x2000; pivot tables
3978
        $bool = (0x2000 & $options) >> 13;
123✔
3979
        $this->phpSheet->getProtection()->setPivotTables(!$bool);
123✔
3980

3981
        // bit: 14; mask 0x4000; select unlocked cells
3982
        // Note that this is opposite of most of above.
3983
        $bool = (0x4000 & $options) >> 14;
123✔
3984
        $this->phpSheet->getProtection()->setSelectUnlockedCells((bool) $bool);
123✔
3985

3986
        // offset: 21; size: 2; not used
3987
    }
3988

3989
    /**
3990
     * Read RANGEPROTECTION record
3991
     * Reading of this record is based on Microsoft Office Excel 97-2000 Binary File Format Specification,
3992
     * where it is referred to as FEAT record.
3993
     */
3994
    protected function readRangeProtection(): void
2✔
3995
    {
3996
        $length = self::getUInt2d($this->data, $this->pos + 2);
2✔
3997
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2✔
3998

3999
        // move stream pointer to next record
4000
        $this->pos += 4 + $length;
2✔
4001

4002
        // local pointer in record data
4003
        $offset = 0;
2✔
4004

4005
        if (!$this->readDataOnly) {
2✔
4006
            $offset += 12;
2✔
4007

4008
            // offset: 12; size: 2; shared feature type, 2 = enhanced protection, 4 = smart tag
4009
            $isf = self::getUInt2d($recordData, 12);
2✔
4010
            if ($isf != 2) {
2✔
4011
                // we only read FEAT records of type 2
4012
                return;
×
4013
            }
4014
            $offset += 2;
2✔
4015

4016
            $offset += 5;
2✔
4017

4018
            // offset: 19; size: 2; count of ref ranges this feature is on
4019
            $cref = self::getUInt2d($recordData, 19);
2✔
4020
            $offset += 2;
2✔
4021

4022
            $offset += 6;
2✔
4023

4024
            // offset: 27; size: 8 * $cref; list of cell ranges (like in hyperlink record)
4025
            $cellRanges = [];
2✔
4026
            for ($i = 0; $i < $cref; ++$i) {
2✔
4027
                try {
4028
                    $cellRange = Xls\Biff8::readBIFF8CellRangeAddressFixed(substr($recordData, 27 + 8 * $i, 8));
2✔
4029
                } catch (PhpSpreadsheetException) {
×
4030
                    return;
×
4031
                }
4032
                $cellRanges[] = $cellRange;
2✔
4033
                $offset += 8;
2✔
4034
            }
4035

4036
            // offset: var; size: var; variable length of feature specific data
4037
            //$rgbFeat = substr($recordData, $offset);
4038
            $offset += 4;
2✔
4039

4040
            // offset: var; size: 4; the encrypted password (only 16-bit although field is 32-bit)
4041
            $wPassword = self::getInt4d($recordData, $offset);
2✔
4042
            $offset += 4;
2✔
4043

4044
            // Apply range protection to sheet
4045
            if ($cellRanges) {
2✔
4046
                $this->phpSheet->protectCells(implode(' ', $cellRanges), ($wPassword === 0) ? '' : strtoupper(dechex($wPassword)), true);
2✔
4047
            }
4048
        }
4049
    }
4050

4051
    /**
4052
     * Read a free CONTINUE record. Free CONTINUE record may be a camouflaged MSODRAWING record
4053
     * When MSODRAWING data on a sheet exceeds 8224 bytes, CONTINUE records are used instead. Undocumented.
4054
     * In this case, we must treat the CONTINUE record as a MSODRAWING record.
4055
     */
4056
    protected function readContinue(): void
1✔
4057
    {
4058
        $length = self::getUInt2d($this->data, $this->pos + 2);
1✔
4059
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
1✔
4060

4061
        // check if we are reading drawing data
4062
        // this is in case a free CONTINUE record occurs in other circumstances we are unaware of
4063
        if ($this->drawingData == '') {
1✔
4064
            // move stream pointer to next record
4065
            $this->pos += 4 + $length;
1✔
4066

4067
            return;
1✔
4068
        }
4069

4070
        // check if record data is at least 4 bytes long, otherwise there is no chance this is MSODRAWING data
4071
        if ($length < 4) {
×
4072
            // move stream pointer to next record
4073
            $this->pos += 4 + $length;
×
4074

4075
            return;
×
4076
        }
4077

4078
        // dirty check to see if CONTINUE record could be a camouflaged MSODRAWING record
4079
        // look inside CONTINUE record to see if it looks like a part of an Escher stream
4080
        // we know that Escher stream may be split at least at
4081
        //        0xF003 MsofbtSpgrContainer
4082
        //        0xF004 MsofbtSpContainer
4083
        //        0xF00D MsofbtClientTextbox
4084
        $validSplitPoints = [0xF003, 0xF004, 0xF00D]; // add identifiers if we find more
×
4085

4086
        $splitPoint = self::getUInt2d($recordData, 2);
×
4087
        if (in_array($splitPoint, $validSplitPoints)) {
×
4088
            // get spliced record data (and move pointer to next record)
4089
            $splicedRecordData = $this->getSplicedRecordData();
×
4090
            $this->drawingData .= StringHelper::convertToString($splicedRecordData['recordData']);
×
4091

4092
            return;
×
4093
        }
4094

4095
        // move stream pointer to next record
4096
        $this->pos += 4 + $length;
×
4097
    }
4098

4099
    /**
4100
     * Reads a record from current position in data stream and continues reading data as long as CONTINUE
4101
     * records are found. Splices the record data pieces and returns the combined string as if record data
4102
     * is in one piece.
4103
     * Moves to next current position in data stream to start of next record different from a CONtINUE record.
4104
     *
4105
     * @return mixed[]
4106
     */
4107
    private function getSplicedRecordData(): array
135✔
4108
    {
4109
        $data = '';
135✔
4110
        $spliceOffsets = [];
135✔
4111

4112
        $i = 0;
135✔
4113
        $spliceOffsets[0] = 0;
135✔
4114

4115
        do {
4116
            ++$i;
135✔
4117

4118
            // offset: 0; size: 2; identifier
4119
            //$identifier = self::getUInt2d($this->data, $this->pos);
4120
            // offset: 2; size: 2; length
4121
            $length = self::getUInt2d($this->data, $this->pos + 2);
135✔
4122
            $data .= $this->readRecordData($this->data, $this->pos + 4, $length);
135✔
4123

4124
            $spliceOffsets[$i] = $spliceOffsets[$i - 1] + $length;
135✔
4125

4126
            $this->pos += 4 + $length;
135✔
4127
            $nextIdentifier = self::getUInt2d($this->data, $this->pos);
135✔
4128
        } while ($nextIdentifier == self::XLS_TYPE_CONTINUE);
135✔
4129

4130
        return [
135✔
4131
            'recordData' => $data,
135✔
4132
            'spliceOffsets' => $spliceOffsets,
135✔
4133
        ];
135✔
4134
    }
4135

4136
    /**
4137
     * Convert formula structure into human readable Excel formula like 'A3+A5*5'.
4138
     *
4139
     * @param string $formulaStructure The complete binary data for the formula
4140
     * @param string $baseCell Base cell, only needed when formula contains tRefN tokens, e.g. with shared formulas
4141
     *
4142
     * @return string Human readable formula
4143
     */
4144
    protected function getFormulaFromStructure(string $formulaStructure, string $baseCell = 'A1'): string
74✔
4145
    {
4146
        // offset: 0; size: 2; size of the following formula data
4147
        $sz = self::getUInt2d($formulaStructure, 0);
74✔
4148

4149
        // offset: 2; size: sz
4150
        $formulaData = substr($formulaStructure, 2, $sz);
74✔
4151

4152
        // offset: 2 + sz; size: variable (optional)
4153
        if (strlen($formulaStructure) > 2 + $sz) {
74✔
4154
            $additionalData = substr($formulaStructure, 2 + $sz);
1✔
4155
        } else {
4156
            $additionalData = '';
73✔
4157
        }
4158

4159
        return $this->getFormulaFromData($formulaData, $additionalData, $baseCell);
74✔
4160
    }
4161

4162
    /**
4163
     * Take formula data and additional data for formula and return human readable formula.
4164
     *
4165
     * @param string $formulaData The binary data for the formula itself
4166
     * @param string $additionalData Additional binary data going with the formula
4167
     * @param string $baseCell Base cell, only needed when formula contains tRefN tokens, e.g. with shared formulas
4168
     *
4169
     * @return string Human readable formula
4170
     */
4171
    private function getFormulaFromData(string $formulaData, string $additionalData = '', string $baseCell = 'A1'): string
74✔
4172
    {
4173
        // start parsing the formula data
4174
        $tokens = [];
74✔
4175

4176
        while ($formulaData !== '' && $token = $this->getNextToken($formulaData, $baseCell)) {
74✔
4177
            $tokens[] = $token;
74✔
4178
            /** @var int[] $token */
4179
            $formulaData = substr($formulaData, $token['size']);
74✔
4180
        }
4181

4182
        $formulaString = $this->createFormulaFromTokens($tokens, $additionalData);
74✔
4183

4184
        return $formulaString;
74✔
4185
    }
4186

4187
    /**
4188
     * Take array of tokens together with additional data for formula and return human readable formula.
4189
     *
4190
     * @param mixed[][] $tokens
4191
     * @param string $additionalData Additional binary data going with the formula
4192
     *
4193
     * @return string Human readable formula
4194
     */
4195
    private function createFormulaFromTokens(array $tokens, string $additionalData): string
74✔
4196
    {
4197
        // empty formula?
4198
        if (empty($tokens)) {
74✔
4199
            return '';
5✔
4200
        }
4201

4202
        $formulaStrings = [];
74✔
4203
        foreach ($tokens as $token) {
74✔
4204
            // initialize spaces
4205
            $space0 = $space0 ?? ''; // spaces before next token, not tParen
74✔
4206
            $space1 = $space1 ?? ''; // carriage returns before next token, not tParen
74✔
4207
            $space2 = $space2 ?? ''; // spaces before opening parenthesis
74✔
4208
            $space3 = $space3 ?? ''; // carriage returns before opening parenthesis
74✔
4209
            $space4 = $space4 ?? ''; // spaces before closing parenthesis
74✔
4210
            $space5 = $space5 ?? ''; // carriage returns before closing parenthesis
74✔
4211
            /** @var string */
4212
            $tokenData = $token['data'] ?? '';
74✔
4213
            switch ($token['name']) {
74✔
4214
                case 'tAdd': // addition
74✔
4215
                case 'tConcat': // addition
74✔
4216
                case 'tDiv': // division
74✔
4217
                case 'tEQ': // equality
74✔
4218
                case 'tGE': // greater than or equal
74✔
4219
                case 'tGT': // greater than
74✔
4220
                case 'tIsect': // intersection
74✔
4221
                case 'tLE': // less than or equal
74✔
4222
                case 'tList': // less than or equal
74✔
4223
                case 'tLT': // less than
74✔
4224
                case 'tMul': // multiplication
74✔
4225
                case 'tNE': // multiplication
74✔
4226
                case 'tPower': // power
74✔
4227
                case 'tRange': // range
74✔
4228
                case 'tSub': // subtraction
74✔
4229
                    $op2 = array_pop($formulaStrings);
37✔
4230
                    $op1 = array_pop($formulaStrings);
37✔
4231
                    $formulaStrings[] = "$op1$space1$space0{$tokenData}$op2";
37✔
4232
                    unset($space0, $space1);
37✔
4233

4234
                    break;
37✔
4235
                case 'tUplus': // unary plus
74✔
4236
                case 'tUminus': // unary minus
74✔
4237
                    $op = array_pop($formulaStrings);
4✔
4238
                    $formulaStrings[] = "$space1$space0{$tokenData}$op";
4✔
4239
                    unset($space0, $space1);
4✔
4240

4241
                    break;
4✔
4242
                case 'tPercent': // percent sign
74✔
4243
                    $op = array_pop($formulaStrings);
1✔
4244
                    $formulaStrings[] = "$op$space1$space0{$tokenData}";
1✔
4245
                    unset($space0, $space1);
1✔
4246

4247
                    break;
1✔
4248
                case 'tAttrVolatile': // indicates volatile function
74✔
4249
                case 'tAttrIf':
74✔
4250
                case 'tAttrSkip':
74✔
4251
                case 'tAttrChoose':
74✔
4252
                    // token is only important for Excel formula evaluator
4253
                    // do nothing
4254
                    break;
3✔
4255
                case 'tAttrSpace': // space / carriage return
74✔
4256
                    // space will be used when next token arrives, do not alter formulaString stack
4257
                    /** @var string[][] $token */
4258
                    switch ($token['data']['spacetype']) {
×
4259
                        case 'type0':
×
4260
                            $space0 = str_repeat(' ', (int) $token['data']['spacecount']);
×
4261

4262
                            break;
×
4263
                        case 'type1':
×
4264
                            $space1 = str_repeat("\n", (int) $token['data']['spacecount']);
×
4265

4266
                            break;
×
4267
                        case 'type2':
×
4268
                            $space2 = str_repeat(' ', (int) $token['data']['spacecount']);
×
4269

4270
                            break;
×
4271
                        case 'type3':
×
4272
                            $space3 = str_repeat("\n", (int) $token['data']['spacecount']);
×
4273

4274
                            break;
×
4275
                        case 'type4':
×
4276
                            $space4 = str_repeat(' ', (int) $token['data']['spacecount']);
×
4277

4278
                            break;
×
4279
                        case 'type5':
×
4280
                            $space5 = str_repeat("\n", (int) $token['data']['spacecount']);
×
4281

4282
                            break;
×
4283
                    }
4284

4285
                    break;
×
4286
                case 'tAttrSum': // SUM function with one parameter
74✔
4287
                    $op = array_pop($formulaStrings);
15✔
4288
                    $formulaStrings[] = "{$space1}{$space0}SUM($op)";
15✔
4289
                    unset($space0, $space1);
15✔
4290

4291
                    break;
15✔
4292
                case 'tFunc': // function with fixed number of arguments
74✔
4293
                case 'tFuncV': // function with variable number of arguments
74✔
4294
                    /** @var string[] */
4295
                    $temp1 = $token['data'];
39✔
4296
                    $temp2 = $temp1['function'];
39✔
4297
                    if ($temp2 != '') {
39✔
4298
                        // normal function
4299
                        $ops = []; // array of operators
38✔
4300
                        $temp3 = (int) $temp1['args'];
38✔
4301
                        for ($i = 0; $i < $temp3; ++$i) {
38✔
4302
                            $ops[] = array_pop($formulaStrings);
29✔
4303
                        }
4304
                        $ops = array_reverse($ops);
38✔
4305
                        $formulaStrings[] = "$space1$space0{$temp2}(" . implode(',', $ops) . ')';
38✔
4306
                        unset($space0, $space1);
38✔
4307
                    } else {
4308
                        // add-in function
4309
                        $ops = []; // array of operators
1✔
4310
                        /** @var int[] */
4311
                        $temp = $token['data'];
1✔
4312
                        for ($i = 0; $i < $temp['args'] - 1; ++$i) {
1✔
4313
                            $ops[] = array_pop($formulaStrings);
1✔
4314
                        }
4315
                        $ops = array_reverse($ops);
1✔
4316
                        $function = array_pop($formulaStrings);
1✔
4317
                        $formulaStrings[] = "$space1$space0$function(" . implode(',', $ops) . ')';
1✔
4318
                        unset($space0, $space1);
1✔
4319
                    }
4320

4321
                    break;
39✔
4322
                case 'tParen': // parenthesis
74✔
4323
                    $expression = array_pop($formulaStrings);
1✔
4324
                    $formulaStrings[] = "$space3$space2($expression$space5$space4)";
1✔
4325
                    unset($space2, $space3, $space4, $space5);
1✔
4326

4327
                    break;
1✔
4328
                case 'tArray': // array constant
74✔
4329
                    $constantArray = Xls\Biff8::readBIFF8ConstantArray($additionalData);
1✔
4330
                    $formulaStrings[] = $space1 . $space0 . $constantArray['value'];
1✔
4331
                    $additionalData = substr($additionalData, $constantArray['size']); // bite of chunk of additional data
1✔
4332
                    unset($space0, $space1);
1✔
4333

4334
                    break;
1✔
4335
                case 'tMemArea':
74✔
4336
                    // bite off chunk of additional data
4337
                    $cellRangeAddressList = Xls\Biff8::readBIFF8CellRangeAddressList($additionalData);
×
4338
                    $additionalData = substr($additionalData, $cellRangeAddressList['size']);
×
4339
                    $formulaStrings[] = "$space1$space0{$tokenData}";
×
4340
                    unset($space0, $space1);
×
4341

4342
                    break;
×
4343
                case 'tArea': // cell range address
74✔
4344
                case 'tBool': // boolean
72✔
4345
                case 'tErr': // error code
71✔
4346
                case 'tInt': // integer
70✔
4347
                case 'tMemErr':
54✔
4348
                case 'tMemFunc':
54✔
4349
                case 'tMissArg':
54✔
4350
                case 'tName':
54✔
4351
                case 'tNameX':
54✔
4352
                case 'tNum': // number
53✔
4353
                case 'tRef': // single cell reference
51✔
4354
                case 'tRef3d': // 3d cell reference
41✔
4355
                case 'tArea3d': // 3d cell range reference
38✔
4356
                case 'tRefN':
27✔
4357
                case 'tAreaN':
26✔
4358
                case 'tStr': // string
25✔
4359
                    $formulaStrings[] = "$space1$space0{$tokenData}";
74✔
4360
                    unset($space0, $space1);
74✔
4361

4362
                    break;
74✔
4363
            }
4364
        }
4365
        $formulaString = $formulaStrings[0];
74✔
4366

4367
        return $formulaString;
74✔
4368
    }
4369

4370
    /**
4371
     * Fetch next token from binary formula data.
4372
     *
4373
     * @param string $formulaData Formula data
4374
     * @param string $baseCell Base cell, only needed when formula contains tRefN tokens, e.g. with shared formulas
4375
     *
4376
     * @return mixed[]
4377
     */
4378
    private function getNextToken(string $formulaData, string $baseCell = 'A1'): array
74✔
4379
    {
4380
        // offset: 0; size: 1; token id
4381
        $id = ord($formulaData[0]); // token id
74✔
4382
        $name = false; // initialize token name
74✔
4383

4384
        switch ($id) {
4385
            case 0x03:
74✔
4386
                $name = 'tAdd';
16✔
4387
                $size = 1;
16✔
4388
                $data = '+';
16✔
4389

4390
                break;
16✔
4391
            case 0x04:
74✔
4392
                $name = 'tSub';
14✔
4393
                $size = 1;
14✔
4394
                $data = '-';
14✔
4395

4396
                break;
14✔
4397
            case 0x05:
74✔
4398
                $name = 'tMul';
4✔
4399
                $size = 1;
4✔
4400
                $data = '*';
4✔
4401

4402
                break;
4✔
4403
            case 0x06:
74✔
4404
                $name = 'tDiv';
13✔
4405
                $size = 1;
13✔
4406
                $data = '/';
13✔
4407

4408
                break;
13✔
4409
            case 0x07:
74✔
4410
                $name = 'tPower';
1✔
4411
                $size = 1;
1✔
4412
                $data = '^';
1✔
4413

4414
                break;
1✔
4415
            case 0x08:
74✔
4416
                $name = 'tConcat';
4✔
4417
                $size = 1;
4✔
4418
                $data = '&';
4✔
4419

4420
                break;
4✔
4421
            case 0x09:
74✔
4422
                $name = 'tLT';
1✔
4423
                $size = 1;
1✔
4424
                $data = '<';
1✔
4425

4426
                break;
1✔
4427
            case 0x0A:
74✔
4428
                $name = 'tLE';
1✔
4429
                $size = 1;
1✔
4430
                $data = '<=';
1✔
4431

4432
                break;
1✔
4433
            case 0x0B:
74✔
4434
                $name = 'tEQ';
4✔
4435
                $size = 1;
4✔
4436
                $data = '=';
4✔
4437

4438
                break;
4✔
4439
            case 0x0C:
74✔
4440
                $name = 'tGE';
1✔
4441
                $size = 1;
1✔
4442
                $data = '>=';
1✔
4443

4444
                break;
1✔
4445
            case 0x0D:
74✔
4446
                $name = 'tGT';
2✔
4447
                $size = 1;
2✔
4448
                $data = '>';
2✔
4449

4450
                break;
2✔
4451
            case 0x0E:
74✔
4452
                $name = 'tNE';
2✔
4453
                $size = 1;
2✔
4454
                $data = '<>';
2✔
4455

4456
                break;
2✔
4457
            case 0x0F:
74✔
4458
                $name = 'tIsect';
×
4459
                $size = 1;
×
4460
                $data = ' ';
×
4461

4462
                break;
×
4463
            case 0x10:
74✔
4464
                $name = 'tList';
2✔
4465
                $size = 1;
2✔
4466
                $data = ',';
2✔
4467

4468
                break;
2✔
4469
            case 0x11:
74✔
4470
                $name = 'tRange';
×
4471
                $size = 1;
×
4472
                $data = ':';
×
4473

4474
                break;
×
4475
            case 0x12:
74✔
4476
                $name = 'tUplus';
1✔
4477
                $size = 1;
1✔
4478
                $data = '+';
1✔
4479

4480
                break;
1✔
4481
            case 0x13:
74✔
4482
                $name = 'tUminus';
4✔
4483
                $size = 1;
4✔
4484
                $data = '-';
4✔
4485

4486
                break;
4✔
4487
            case 0x14:
74✔
4488
                $name = 'tPercent';
1✔
4489
                $size = 1;
1✔
4490
                $data = '%';
1✔
4491

4492
                break;
1✔
4493
            case 0x15:    //    parenthesis
74✔
4494
                $name = 'tParen';
1✔
4495
                $size = 1;
1✔
4496
                $data = null;
1✔
4497

4498
                break;
1✔
4499
            case 0x16:    //    missing argument
74✔
4500
                $name = 'tMissArg';
×
4501
                $size = 1;
×
4502
                $data = '';
×
4503

4504
                break;
×
4505
            case 0x17:    //    string
74✔
4506
                $name = 'tStr';
25✔
4507
                // offset: 1; size: var; Unicode string, 8-bit string length
4508
                $string = self::readUnicodeStringShort(substr($formulaData, 1));
25✔
4509
                $size = 1 + $string['size'];
25✔
4510
                $data = self::UTF8toExcelDoubleQuoted($string['value']);
25✔
4511

4512
                break;
25✔
4513
            case 0x19:    //    Special attribute
73✔
4514
                // offset: 1; size: 1; attribute type flags:
4515
                switch (ord($formulaData[1])) {
17✔
4516
                    case 0x01:
17✔
4517
                        $name = 'tAttrVolatile';
3✔
4518
                        $size = 4;
3✔
4519
                        $data = null;
3✔
4520

4521
                        break;
3✔
4522
                    case 0x02:
15✔
4523
                        $name = 'tAttrIf';
1✔
4524
                        $size = 4;
1✔
4525
                        $data = null;
1✔
4526

4527
                        break;
1✔
4528
                    case 0x04:
15✔
4529
                        $name = 'tAttrChoose';
1✔
4530
                        // offset: 2; size: 2; number of choices in the CHOOSE function ($nc, number of parameters decreased by 1)
4531
                        $nc = self::getUInt2d($formulaData, 2);
1✔
4532
                        // offset: 4; size: 2 * $nc
4533
                        // offset: 4 + 2 * $nc; size: 2
4534
                        $size = 2 * $nc + 6;
1✔
4535
                        $data = null;
1✔
4536

4537
                        break;
1✔
4538
                    case 0x08:
15✔
4539
                        $name = 'tAttrSkip';
1✔
4540
                        $size = 4;
1✔
4541
                        $data = null;
1✔
4542

4543
                        break;
1✔
4544
                    case 0x10:
15✔
4545
                        $name = 'tAttrSum';
15✔
4546
                        $size = 4;
15✔
4547
                        $data = null;
15✔
4548

4549
                        break;
15✔
4550
                    case 0x40:
×
4551
                    case 0x41:
×
4552
                        $name = 'tAttrSpace';
×
4553
                        $size = 4;
×
4554
                        // offset: 2; size: 2; space type and position
4555
                        $spacetype = match (ord($formulaData[2])) {
×
4556
                            0x00 => 'type0',
×
4557
                            0x01 => 'type1',
×
4558
                            0x02 => 'type2',
×
4559
                            0x03 => 'type3',
×
4560
                            0x04 => 'type4',
×
4561
                            0x05 => 'type5',
×
4562
                            default => throw new Exception('Unrecognized space type in tAttrSpace token'),
×
4563
                        };
×
4564
                        // offset: 3; size: 1; number of inserted spaces/carriage returns
4565
                        $spacecount = ord($formulaData[3]);
×
4566

4567
                        $data = ['spacetype' => $spacetype, 'spacecount' => $spacecount];
×
4568

4569
                        break;
×
4570
                    default:
4571
                        throw new Exception('Unrecognized attribute flag in tAttr token');
×
4572
                }
4573

4574
                break;
17✔
4575
            case 0x1C:    //    error code
73✔
4576
                // offset: 1; size: 1; error code
4577
                $name = 'tErr';
5✔
4578
                $size = 2;
5✔
4579
                $data = Xls\ErrorCode::lookup(ord($formulaData[1]));
5✔
4580

4581
                break;
5✔
4582
            case 0x1D:    //    boolean
72✔
4583
                // offset: 1; size: 1; 0 = false, 1 = true;
4584
                $name = 'tBool';
3✔
4585
                $size = 2;
3✔
4586
                $data = ord($formulaData[1]) ? 'TRUE' : 'FALSE';
3✔
4587

4588
                break;
3✔
4589
            case 0x1E:    //    integer
72✔
4590
                // offset: 1; size: 2; unsigned 16-bit integer
4591
                $name = 'tInt';
46✔
4592
                $size = 3;
46✔
4593
                $data = self::getUInt2d($formulaData, 1);
46✔
4594

4595
                break;
46✔
4596
            case 0x1F:    //    number
65✔
4597
                // offset: 1; size: 8;
4598
                $name = 'tNum';
9✔
4599
                $size = 9;
9✔
4600
                $data = self::extractNumber(substr($formulaData, 1));
9✔
4601
                $data = str_replace(',', '.', (string) $data); // in case non-English locale
9✔
4602

4603
                break;
9✔
4604
            case 0x20:    //    array constant
64✔
4605
            case 0x40:
64✔
4606
            case 0x60:
64✔
4607
                // offset: 1; size: 7; not used
4608
                $name = 'tArray';
1✔
4609
                $size = 8;
1✔
4610
                $data = null;
1✔
4611

4612
                break;
1✔
4613
            case 0x21:    //    function with fixed number of arguments
64✔
4614
            case 0x41:
64✔
4615
            case 0x61:
60✔
4616
                $name = 'tFunc';
22✔
4617
                $size = 3;
22✔
4618
                // offset: 1; size: 2; index to built-in sheet function
4619
                $mapping = Xls\Mappings::TFUNC_MAPPINGS[self::getUInt2d($formulaData, 1)] ?? null;
22✔
4620
                if ($mapping === null) {
22✔
4621
                    throw new Exception('Unrecognized function in formula');
1✔
4622
                }
4623
                $data = ['function' => $mapping[0], 'args' => $mapping[1]];
22✔
4624

4625
                break;
22✔
4626
            case 0x22:    //    function with variable number of arguments
60✔
4627
            case 0x42:
60✔
4628
            case 0x62:
56✔
4629
                $name = 'tFuncV';
23✔
4630
                $size = 4;
23✔
4631
                // offset: 1; size: 1; number of arguments
4632
                $args = ord($formulaData[1]);
23✔
4633
                // offset: 2: size: 2; index to built-in sheet function
4634
                $index = self::getUInt2d($formulaData, 2);
23✔
4635
                $function = Xls\Mappings::TFUNCV_MAPPINGS[$index] ?? null;
23✔
4636
                if ($function === null) {
23✔
4637
                    throw new Exception('Unrecognized function in formula');
×
4638
                }
4639
                $data = ['function' => $function, 'args' => $args];
23✔
4640

4641
                break;
23✔
4642
            case 0x23:    //    index to defined name
56✔
4643
            case 0x43:
56✔
4644
            case 0x63:
56✔
4645
                $name = 'tName';
1✔
4646
                $size = 5;
1✔
4647
                // offset: 1; size: 2; one-based index to definedname record
4648
                $definedNameIndex = self::getUInt2d($formulaData, 1) - 1;
1✔
4649
                // offset: 2; size: 2; not used
4650
                /** @var string[] */
4651
                $data = $this->definedname[$definedNameIndex]['name'] ?? ''; //* @phpstan-ignore-line
1✔
4652

4653
                break;
1✔
4654
            case 0x24:    //    single cell reference e.g. A5
55✔
4655
            case 0x44:
55✔
4656
            case 0x64:
46✔
4657
                $name = 'tRef';
25✔
4658
                $size = 5;
25✔
4659
                $data = Xls\Biff8::readBIFF8CellAddress(substr($formulaData, 1, 4));
25✔
4660

4661
                break;
25✔
4662
            case 0x25:    //    cell range reference to cells in the same sheet (2d)
44✔
4663
            case 0x45:
22✔
4664
            case 0x65:
22✔
4665
                $name = 'tArea';
29✔
4666
                $size = 9;
29✔
4667
                $data = Xls\Biff8::readBIFF8CellRangeAddress(substr($formulaData, 1, 8));
29✔
4668

4669
                break;
29✔
4670
            case 0x26:    //    Constant reference sub-expression
21✔
4671
            case 0x46:
21✔
4672
            case 0x66:
21✔
4673
                $name = 'tMemArea';
×
4674
                // offset: 1; size: 4; not used
4675
                // offset: 5; size: 2; size of the following subexpression
4676
                $subSize = self::getUInt2d($formulaData, 5);
×
4677
                $size = 7 + $subSize;
×
4678
                $data = $this->getFormulaFromData(substr($formulaData, 7, $subSize));
×
4679

4680
                break;
×
4681
            case 0x27:    //    Deleted constant reference sub-expression
21✔
4682
            case 0x47:
21✔
4683
            case 0x67:
21✔
4684
                $name = 'tMemErr';
×
4685
                // offset: 1; size: 4; not used
4686
                // offset: 5; size: 2; size of the following subexpression
4687
                $subSize = self::getUInt2d($formulaData, 5);
×
4688
                $size = 7 + $subSize;
×
4689
                $data = $this->getFormulaFromData(substr($formulaData, 7, $subSize));
×
4690

4691
                break;
×
4692
            case 0x29:    //    Variable reference sub-expression
21✔
4693
            case 0x49:
21✔
4694
            case 0x69:
21✔
4695
                $name = 'tMemFunc';
1✔
4696
                // offset: 1; size: 2; size of the following sub-expression
4697
                $subSize = self::getUInt2d($formulaData, 1);
1✔
4698
                $size = 3 + $subSize;
1✔
4699
                $data = $this->getFormulaFromData(substr($formulaData, 3, $subSize));
1✔
4700

4701
                break;
1✔
4702
            case 0x2C: // Relative 2d cell reference reference, used in shared formulas and some other places
21✔
4703
            case 0x4C:
21✔
4704
            case 0x6C:
20✔
4705
                $name = 'tRefN';
4✔
4706
                $size = 5;
4✔
4707
                $data = Xls\Biff8::readBIFF8CellAddressB(substr($formulaData, 1, 4), $baseCell);
4✔
4708

4709
                break;
4✔
4710
            case 0x2D:    //    Relative 2d range reference
18✔
4711
            case 0x4D:
17✔
4712
            case 0x6D:
17✔
4713
                $name = 'tAreaN';
1✔
4714
                $size = 9;
1✔
4715
                $data = Xls\Biff8::readBIFF8CellRangeAddressB(substr($formulaData, 1, 8), $baseCell);
1✔
4716

4717
                break;
1✔
4718
            case 0x39:    //    External name
17✔
4719
            case 0x59:
16✔
4720
            case 0x79:
16✔
4721
                $name = 'tNameX';
1✔
4722
                $size = 7;
1✔
4723
                // offset: 1; size: 2; index to REF entry in EXTERNSHEET record
4724
                // offset: 3; size: 2; one-based index to DEFINEDNAME or EXTERNNAME record
4725
                $index = self::getUInt2d($formulaData, 3);
1✔
4726
                // assume index is to EXTERNNAME record
4727
                $data = $this->externalNames[$index - 1]['name'] ?? '';
1✔
4728

4729
                // offset: 5; size: 2; not used
4730
                break;
1✔
4731
            case 0x3A:    //    3d reference to cell
16✔
4732
            case 0x5A:
13✔
4733
            case 0x7A:
13✔
4734
                $name = 'tRef3d';
4✔
4735
                $size = 7;
4✔
4736

4737
                try {
4738
                    // offset: 1; size: 2; index to REF entry
4739
                    $sheetRange = $this->readSheetRangeByRefIndex(self::getUInt2d($formulaData, 1));
4✔
4740
                    // offset: 3; size: 4; cell address
4741
                    $cellAddress = Xls\Biff8::readBIFF8CellAddress(substr($formulaData, 3, 4));
4✔
4742

4743
                    $data = "$sheetRange!$cellAddress";
4✔
4744
                } catch (PhpSpreadsheetException) {
×
4745
                    // deleted sheet reference
4746
                    $data = '#REF!';
×
4747
                }
4748

4749
                break;
4✔
4750
            case 0x3B:    //    3d reference to cell range
12✔
4751
            case 0x5B:
1✔
4752
            case 0x7B:
1✔
4753
                $name = 'tArea3d';
11✔
4754
                $size = 11;
11✔
4755

4756
                try {
4757
                    // offset: 1; size: 2; index to REF entry
4758
                    $sheetRange = $this->readSheetRangeByRefIndex(self::getUInt2d($formulaData, 1));
11✔
4759
                    // offset: 3; size: 8; cell address
4760
                    $cellRangeAddress = Xls\Biff8::readBIFF8CellRangeAddress(substr($formulaData, 3, 8));
11✔
4761

4762
                    $data = "$sheetRange!$cellRangeAddress";
11✔
4763
                } catch (PhpSpreadsheetException) {
×
4764
                    // deleted sheet reference
4765
                    $data = '#REF!';
×
4766
                }
4767

4768
                break;
11✔
4769
                // Unknown cases    // don't know how to deal with
4770
            default:
4771
                throw new Exception('Unrecognized token ' . sprintf('%02X', $id) . ' in formula');
1✔
4772
        }
4773

4774
        return [
74✔
4775
            'id' => $id,
74✔
4776
            'name' => $name,
74✔
4777
            'size' => $size,
74✔
4778
            'data' => $data,
74✔
4779
        ];
74✔
4780
    }
4781

4782
    /**
4783
     * Get a sheet range like Sheet1:Sheet3 from REF index
4784
     * Note: If there is only one sheet in the range, one gets e.g Sheet1
4785
     * It can also happen that the REF structure uses the -1 (FFFF) code to indicate deleted sheets,
4786
     * in which case an Exception is thrown.
4787
     */
4788
    protected function readSheetRangeByRefIndex(int $index): string|false
15✔
4789
    {
4790
        if (isset($this->ref[$index])) {
15✔
4791
            $type = $this->externalBooks[$this->ref[$index]['externalBookIndex']]['type'];
15✔
4792

4793
            switch ($type) {
4794
                case 'internal':
15✔
4795
                    // check if we have a deleted 3d reference
4796
                    if ($this->ref[$index]['firstSheetIndex'] == 0xFFFF || $this->ref[$index]['lastSheetIndex'] == 0xFFFF) {
15✔
4797
                        throw new Exception('Deleted sheet reference');
×
4798
                    }
4799

4800
                    // we have normal sheet range (collapsed or uncollapsed)
4801
                    $firstSheetName = $this->sheets[$this->ref[$index]['firstSheetIndex']]['name'];
15✔
4802
                    $lastSheetName = $this->sheets[$this->ref[$index]['lastSheetIndex']]['name'];
15✔
4803

4804
                    if ($firstSheetName == $lastSheetName) {
15✔
4805
                        // collapsed sheet range
4806
                        $sheetRange = $firstSheetName;
15✔
4807
                    } else {
4808
                        $sheetRange = "$firstSheetName:$lastSheetName";
×
4809
                    }
4810

4811
                    // escape the single-quotes
4812
                    $sheetRange = str_replace("'", "''", $sheetRange);
15✔
4813

4814
                    // if there are special characters, we need to enclose the range in single-quotes
4815
                    // todo: check if we have identified the whole set of special characters
4816
                    // it seems that the following characters are not accepted for sheet names
4817
                    // and we may assume that they are not present: []*/:\?
4818
                    // 'u' qualifier makes it risky to use Preg::isMatch here
4819
                    if (preg_match("/[ !\"@#£$%&{()}<>=+'|^,;-]/u", $sheetRange)) {
15✔
4820
                        $sheetRange = "'$sheetRange'";
4✔
4821
                    }
4822

4823
                    return $sheetRange;
15✔
4824
                default:
4825
                    // TODO: external sheet support
4826
                    throw new Exception('Xls reader only supports internal sheets in formulas');
×
4827
            }
4828
        }
4829

4830
        return false;
×
4831
    }
4832

4833
    /**
4834
     * Read byte string (8-bit string length)
4835
     * OpenOffice documentation: 2.5.2.
4836
     *
4837
     * @return array{value: mixed, size: int}
4838
     */
4839
    protected function readByteStringShort(string $subData): array
7✔
4840
    {
4841
        // offset: 0; size: 1; length of the string (character count)
4842
        $ln = ord($subData[0]);
7✔
4843

4844
        // offset: 1: size: var; character array (8-bit characters)
4845
        $value = $this->decodeCodepage(substr($subData, 1, $ln));
7✔
4846

4847
        return [
7✔
4848
            'value' => $value,
7✔
4849
            'size' => 1 + $ln, // size in bytes of data structure
7✔
4850
        ];
7✔
4851
    }
4852

4853
    /**
4854
     * Read byte string (16-bit string length)
4855
     * OpenOffice documentation: 2.5.2.
4856
     *
4857
     * @return array{value: mixed, size: int}
4858
     */
4859
    protected function readByteStringLong(string $subData): array
2✔
4860
    {
4861
        // offset: 0; size: 2; length of the string (character count)
4862
        $ln = self::getUInt2d($subData, 0);
2✔
4863

4864
        // offset: 2: size: var; character array (8-bit characters)
4865
        $value = $this->decodeCodepage(substr($subData, 2));
2✔
4866

4867
        //return $string;
4868
        return [
2✔
4869
            'value' => $value,
2✔
4870
            'size' => 2 + $ln, // size in bytes of data structure
2✔
4871
        ];
2✔
4872
    }
4873

4874
    protected function parseRichText(string $is): RichText
3✔
4875
    {
4876
        $value = new RichText();
3✔
4877
        $value->createText($is);
3✔
4878

4879
        return $value;
3✔
4880
    }
4881

4882
    /**
4883
     * Phpstan 1.4.4 complains that this property is never read.
4884
     * So, we might be able to get rid of it altogether.
4885
     * For now, however, this function makes it readable,
4886
     * which satisfies Phpstan.
4887
     *
4888
     * @return mixed[]
4889
     *
4890
     * @codeCoverageIgnore
4891
     */
4892
    public function getMapCellStyleXfIndex(): array
4893
    {
4894
        return $this->mapCellStyleXfIndex;
4895
    }
4896

4897
    /**
4898
     * Parse conditional formatting blocks.
4899
     *
4900
     * @see https://www.openoffice.org/sc/excelfileformat.pdf Search for CFHEADER followed by CFRULE
4901
     *
4902
     * @return mixed[]
4903
     */
4904
    protected function readCFHeader(): array
24✔
4905
    {
4906
        return (new Xls\ConditionalFormatting())->readCFHeader2($this);
24✔
4907
    }
4908

4909
    /** @param string[] $cellRangeAddresses */
4910
    protected function readCFRule(array $cellRangeAddresses): void
24✔
4911
    {
4912
        (new Xls\ConditionalFormatting())->readCFRule2($cellRangeAddresses, $this);
24✔
4913
    }
4914

4915
    public function getVersion(): int
5✔
4916
    {
4917
        return $this->version;
5✔
4918
    }
4919
}
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