Failed Conditions
Pull Request — master (#3876)
by Abdul Malik
22:45 queued 13:31
created

Xls::readBIFF8CellRangeAddressFixed()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 28
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 5.0144

Importance

Changes 0
Metric Value
eloc 11
c 0
b 0
f 0
dl 0
loc 28
ccs 11
cts 12
cp 0.9167
rs 9.6111
cc 5
nc 3
nop 1
crap 5.0144
1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Reader;
4
5
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
6
use PhpOffice\PhpSpreadsheet\Cell\DataType;
7
use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
8
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
9
use PhpOffice\PhpSpreadsheet\NamedRange;
10
use PhpOffice\PhpSpreadsheet\Reader\Xls\ConditionalFormatting;
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;
0 ignored issues
show
Bug introduced by
The type PhpOffice\PhpSpreadsheet\Shared\Date was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
16
use PhpOffice\PhpSpreadsheet\Shared\Escher;
17
use PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer\SpgrContainer\SpContainer;
18
use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer\BSE;
19
use PhpOffice\PhpSpreadsheet\Shared\File;
20
use PhpOffice\PhpSpreadsheet\Shared\OLE;
21
use PhpOffice\PhpSpreadsheet\Shared\OLERead;
22
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
23
use PhpOffice\PhpSpreadsheet\Shared\Xls as SharedXls;
24
use PhpOffice\PhpSpreadsheet\Spreadsheet;
25
use PhpOffice\PhpSpreadsheet\Style\Alignment;
26
use PhpOffice\PhpSpreadsheet\Style\Borders;
27
use PhpOffice\PhpSpreadsheet\Style\Conditional;
28
use PhpOffice\PhpSpreadsheet\Style\Fill;
29
use PhpOffice\PhpSpreadsheet\Style\Font;
30
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
31
use PhpOffice\PhpSpreadsheet\Style\Protection;
32
use PhpOffice\PhpSpreadsheet\Style\Style;
33
use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing;
34
use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup;
35
use PhpOffice\PhpSpreadsheet\Worksheet\SheetView;
36
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
37
38
// Original file header of ParseXL (used as the base for this class):
39
// --------------------------------------------------------------------------------
40
// Adapted from Excel_Spreadsheet_Reader developed by users bizon153,
41
// trex005, and mmp11 (SourceForge.net)
42
// https://sourceforge.net/projects/phpexcelreader/
43
// Primary changes made by canyoncasa (dvc) for ParseXL 1.00 ...
44
//     Modelled moreso after Perl Excel Parse/Write modules
45
//     Added Parse_Excel_Spreadsheet object
46
//         Reads a whole worksheet or tab as row,column array or as
47
//         associated hash of indexed rows and named column fields
48
//     Added variables for worksheet (tab) indexes and names
49
//     Added an object call for loading individual woorksheets
50
//     Changed default indexing defaults to 0 based arrays
51
//     Fixed date/time and percent formats
52
//     Includes patches found at SourceForge...
53
//         unicode patch by nobody
54
//         unpack("d") machine depedency patch by matchy
55
//         boundsheet utf16 patch by bjaenichen
56
//     Renamed functions for shorter names
57
//     General code cleanup and rigor, including <80 column width
58
//     Included a testcase Excel file and PHP example calls
59
//     Code works for PHP 5.x
60
61
// Primary changes made by canyoncasa (dvc) for ParseXL 1.10 ...
62
// http://sourceforge.net/tracker/index.php?func=detail&aid=1466964&group_id=99160&atid=623334
63
//     Decoding of formula conditions, results, and tokens.
64
//     Support for user-defined named cells added as an array "namedcells"
65
//         Patch code for user-defined named cells supports single cells only.
66
//         NOTE: this patch only works for BIFF8 as BIFF5-7 use a different
67
//         external sheet reference structure
68
class Xls extends BaseReader
69
{
70
    private const HIGH_ORDER_BIT = 0x80 << 24;
71
    private const FC000000 = 0xFC << 24;
72
    private const FE000000 = 0xFE << 24;
73
74
    // ParseXL definitions
75
    public const XLS_BIFF8 = 0x0600;
76
    public const XLS_BIFF7 = 0x0500;
77
    public const XLS_WORKBOOKGLOBALS = 0x0005;
78
    public const XLS_WORKSHEET = 0x0010;
79
80
    // record identifiers
81
    public const XLS_TYPE_FORMULA = 0x0006;
82
    public const XLS_TYPE_EOF = 0x000A;
83
    public const XLS_TYPE_PROTECT = 0x0012;
84
    public const XLS_TYPE_OBJECTPROTECT = 0x0063;
85
    public const XLS_TYPE_SCENPROTECT = 0x00DD;
86
    public const XLS_TYPE_PASSWORD = 0x0013;
87
    public const XLS_TYPE_HEADER = 0x0014;
88
    public const XLS_TYPE_FOOTER = 0x0015;
89
    public const XLS_TYPE_EXTERNSHEET = 0x0017;
90
    public const XLS_TYPE_DEFINEDNAME = 0x0018;
91
    public const XLS_TYPE_VERTICALPAGEBREAKS = 0x001A;
92
    public const XLS_TYPE_HORIZONTALPAGEBREAKS = 0x001B;
93
    public const XLS_TYPE_NOTE = 0x001C;
94
    public const XLS_TYPE_SELECTION = 0x001D;
95
    public const XLS_TYPE_DATEMODE = 0x0022;
96
    public const XLS_TYPE_EXTERNNAME = 0x0023;
97
    public const XLS_TYPE_LEFTMARGIN = 0x0026;
98
    public const XLS_TYPE_RIGHTMARGIN = 0x0027;
99
    public const XLS_TYPE_TOPMARGIN = 0x0028;
100
    public const XLS_TYPE_BOTTOMMARGIN = 0x0029;
101
    public const XLS_TYPE_PRINTGRIDLINES = 0x002B;
102
    public const XLS_TYPE_FILEPASS = 0x002F;
103
    public const XLS_TYPE_FONT = 0x0031;
104
    public const XLS_TYPE_CONTINUE = 0x003C;
105
    public const XLS_TYPE_PANE = 0x0041;
106
    public const XLS_TYPE_CODEPAGE = 0x0042;
107
    public const XLS_TYPE_DEFCOLWIDTH = 0x0055;
108
    public const XLS_TYPE_OBJ = 0x005D;
109
    public const XLS_TYPE_COLINFO = 0x007D;
110
    public const XLS_TYPE_IMDATA = 0x007F;
111
    public const XLS_TYPE_SHEETPR = 0x0081;
112
    public const XLS_TYPE_HCENTER = 0x0083;
113
    public const XLS_TYPE_VCENTER = 0x0084;
114
    public const XLS_TYPE_SHEET = 0x0085;
115
    public const XLS_TYPE_PALETTE = 0x0092;
116
    public const XLS_TYPE_SCL = 0x00A0;
117
    public const XLS_TYPE_PAGESETUP = 0x00A1;
118
    public const XLS_TYPE_MULRK = 0x00BD;
119
    public const XLS_TYPE_MULBLANK = 0x00BE;
120
    public const XLS_TYPE_DBCELL = 0x00D7;
121
    public const XLS_TYPE_XF = 0x00E0;
122
    public const XLS_TYPE_MERGEDCELLS = 0x00E5;
123
    public const XLS_TYPE_MSODRAWINGGROUP = 0x00EB;
124
    public const XLS_TYPE_MSODRAWING = 0x00EC;
125
    public const XLS_TYPE_SST = 0x00FC;
126
    public const XLS_TYPE_LABELSST = 0x00FD;
127
    public const XLS_TYPE_EXTSST = 0x00FF;
128
    public const XLS_TYPE_EXTERNALBOOK = 0x01AE;
129
    public const XLS_TYPE_DATAVALIDATIONS = 0x01B2;
130
    public const XLS_TYPE_TXO = 0x01B6;
131
    public const XLS_TYPE_HYPERLINK = 0x01B8;
132
    public const XLS_TYPE_DATAVALIDATION = 0x01BE;
133
    public const XLS_TYPE_DIMENSION = 0x0200;
134
    public const XLS_TYPE_BLANK = 0x0201;
135
    public const XLS_TYPE_NUMBER = 0x0203;
136
    public const XLS_TYPE_LABEL = 0x0204;
137
    public const XLS_TYPE_BOOLERR = 0x0205;
138
    public const XLS_TYPE_STRING = 0x0207;
139
    public const XLS_TYPE_ROW = 0x0208;
140
    public const XLS_TYPE_INDEX = 0x020B;
141
    public const XLS_TYPE_ARRAY = 0x0221;
142
    public const XLS_TYPE_DEFAULTROWHEIGHT = 0x0225;
143
    public const XLS_TYPE_WINDOW2 = 0x023E;
144
    public const XLS_TYPE_RK = 0x027E;
145
    public const XLS_TYPE_STYLE = 0x0293;
146
    public const XLS_TYPE_FORMAT = 0x041E;
147
    public const XLS_TYPE_SHAREDFMLA = 0x04BC;
148
    public const XLS_TYPE_BOF = 0x0809;
149
    public const XLS_TYPE_SHEETPROTECTION = 0x0867;
150
    public const XLS_TYPE_RANGEPROTECTION = 0x0868;
151
    public const XLS_TYPE_SHEETLAYOUT = 0x0862;
152
    public const XLS_TYPE_XFEXT = 0x087D;
153
    public const XLS_TYPE_PAGELAYOUTVIEW = 0x088B;
154
    public const XLS_TYPE_CFHEADER = 0x01B0;
155
    public const XLS_TYPE_CFRULE = 0x01B1;
156
    public const XLS_TYPE_UNKNOWN = 0xFFFF;
157
158
    // Encryption type
159
    public const MS_BIFF_CRYPTO_NONE = 0;
160
    public const MS_BIFF_CRYPTO_XOR = 1;
161
    public const MS_BIFF_CRYPTO_RC4 = 2;
162
163
    // Size of stream blocks when using RC4 encryption
164
    public const REKEY_BLOCK = 0x400;
165
166
    /**
167
     * Summary Information stream data.
168
     */
169
    private ?string $summaryInformation = null;
170
171
    /**
172
     * Extended Summary Information stream data.
173
     */
174
    private ?string $documentSummaryInformation = null;
175
176
    /**
177
     * Workbook stream data. (Includes workbook globals substream as well as sheet substreams).
178
     */
179
    private string $data;
180
181
    /**
182
     * Size in bytes of $this->data.
183
     */
184
    private int $dataSize;
185
186
    /**
187
     * Current position in stream.
188
     */
189
    private int $pos;
190
191
    /**
192
     * Workbook to be returned by the reader.
193
     */
194
    private Spreadsheet $spreadsheet;
195
196
    /**
197
     * Worksheet that is currently being built by the reader.
198
     */
199
    private Worksheet $phpSheet;
200
201
    /**
202
     * BIFF version.
203
     */
204
    private int $version = 0;
205
206
    /**
207
     * Codepage set in the Excel file being read. Only important for BIFF5 (Excel 5.0 - Excel 95)
208
     * For BIFF8 (Excel 97 - Excel 2003) this will always have the value 'UTF-16LE'.
209
     */
210
    private string $codepage = '';
211
212
    /**
213
     * Shared formats.
214
     */
215
    private array $formats;
216
217
    /**
218
     * Shared fonts.
219
     *
220
     * @var Font[]
221
     */
222
    private array $objFonts;
223
224
    /**
225
     * Color palette.
226
     */
227
    private array $palette;
228
229
    /**
230
     * Worksheets.
231
     */
232
    private array $sheets;
233
234
    /**
235
     * External books.
236
     */
237
    private array $externalBooks;
238
239
    /**
240
     * REF structures. Only applies to BIFF8.
241
     */
242
    private array $ref;
243
244
    /**
245
     * External names.
246
     */
247
    private array $externalNames;
248
249
    /**
250
     * Defined names.
251
     */
252
    private array $definedname;
253
254
    /**
255
     * Shared strings. Only applies to BIFF8.
256
     */
257
    private array $sst;
258
259
    /**
260
     * Panes are frozen? (in sheet currently being read). See WINDOW2 record.
261
     */
262
    private bool $frozen;
263
264
    /**
265
     * Fit printout to number of pages? (in sheet currently being read). See SHEETPR record.
266
     */
267
    private bool $isFitToPages;
268
269
    /**
270
     * Objects. One OBJ record contributes with one entry.
271
     */
272
    private array $objs;
273
274
    /**
275
     * Text Objects. One TXO record corresponds with one entry.
276
     */
277
    private array $textObjects;
278
279
    /**
280
     * Cell Annotations (BIFF8).
281
     */
282
    private array $cellNotes;
283
284
    /**
285
     * The combined MSODRAWINGGROUP data.
286
     */
287
    private string $drawingGroupData;
288
289
    /**
290
     * The combined MSODRAWING data (per sheet).
291
     */
292
    private string $drawingData;
293
294
    /**
295
     * Keep track of XF index.
296
     */
297
    private int $xfIndex;
298
299
    /**
300
     * Mapping of XF index (that is a cell XF) to final index in cellXf collection.
301
     */
302
    private array $mapCellXfIndex;
303
304
    /**
305
     * Mapping of XF index (that is a style XF) to final index in cellStyleXf collection.
306
     */
307
    private array $mapCellStyleXfIndex;
308
309
    /**
310
     * The shared formulas in a sheet. One SHAREDFMLA record contributes with one value.
311
     */
312
    private array $sharedFormulas;
313
314
    /**
315
     * The shared formula parts in a sheet. One FORMULA record contributes with one value if it
316
     * refers to a shared formula.
317
     */
318
    private array $sharedFormulaParts;
319
320
    /**
321
     * The type of encryption in use.
322
     */
323
    private int $encryption = 0;
324
325
    /**
326
     * The position in the stream after which contents are encrypted.
327
     */
328
    private int $encryptionStartPos = 0;
329
330
    /**
331
     * The current RC4 decryption object.
332
     *
333
     * @var ?Xls\RC4
334
     */
335
    private ?Xls\RC4 $rc4Key = null;
336
337
    /**
338
     * The position in the stream that the RC4 decryption object was left at.
339
     */
340
    private int $rc4Pos = 0;
341
342
    /**
343
     * The current MD5 context state.
344
     * It is never set in the program, so code which uses it is suspect.
345
     */
346
    private string $md5Ctxt; // @phpstan-ignore-line
347
348
    private int $textObjRef;
349
350
    private string $baseCell;
351
352
    private bool $activeSheetSet = false;
353
354
    /**
355
     * Create a new Xls Reader instance.
356
     */
357 121
    public function __construct()
358
    {
359 121
        parent::__construct();
360
    }
361
362
    /**
363
     * Can the current IReader read the file?
364
     */
365 20
    public function canRead(string $filename): bool
366
    {
367 20
        if (File::testFileNoThrow($filename) === false) {
368 1
            return false;
369
        }
370
371
        try {
372
            // Use ParseXL for the hard work.
373 19
            $ole = new OLERead();
374
375
            // get excel data
376 19
            $ole->read($filename);
377 12
            if ($ole->wrkbook === null) {
378 3
                throw new Exception('The filename ' . $filename . ' is not recognised as a Spreadsheet file');
379
            }
380
381 9
            return true;
382 10
        } catch (PhpSpreadsheetException) {
383 10
            return false;
384
        }
385
    }
386
387
    public function setCodepage(string $codepage): void
388
    {
389
        if (CodePage::validate($codepage) === false) {
390
            throw new PhpSpreadsheetException('Unknown codepage: ' . $codepage);
391
        }
392
393
        $this->codepage = $codepage;
394
    }
395
396 5
    public function getCodepage(): string
397
    {
398 5
        return $this->codepage;
399
    }
400
401
    /**
402
     * Reads names of the worksheets from a file, without parsing the whole file to a PhpSpreadsheet object.
403
     */
404 6
    public function listWorksheetNames(string $filename): array
405
    {
406 6
        File::assertFile($filename);
407
408 6
        $worksheetNames = [];
409
410
        // Read the OLE file
411 6
        $this->loadOLE($filename);
412
413
        // total byte size of Excel data (workbook global substream + sheet substreams)
414 6
        $this->dataSize = strlen($this->data);
415
416 6
        $this->pos = 0;
417 6
        $this->sheets = [];
418
419
        // Parse Workbook Global Substream
420 6
        while ($this->pos < $this->dataSize) {
421 6
            $code = self::getUInt2d($this->data, $this->pos);
422
423 6
            match ($code) {
424 6
                self::XLS_TYPE_BOF => $this->readBof(),
425 6
                self::XLS_TYPE_SHEET => $this->readSheet(),
426 6
                self::XLS_TYPE_EOF => $this->readDefault(),
427 6
                self::XLS_TYPE_CODEPAGE => $this->readCodepage(),
428 6
                default => $this->readDefault(),
429 6
            };
430
431 6
            if ($code === self::XLS_TYPE_EOF) {
432 6
                break;
433
            }
434
        }
435
436 6
        foreach ($this->sheets as $sheet) {
437 6
            if ($sheet['sheetType'] != 0x00) {
438
                // 0x00: Worksheet, 0x02: Chart, 0x06: Visual Basic module
439
                continue;
440
            }
441
442 6
            $worksheetNames[] = $sheet['name'];
443
        }
444
445 6
        return $worksheetNames;
446
    }
447
448
    /**
449
     * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
450
     */
451 4
    public function listWorksheetInfo(string $filename): array
452
    {
453 4
        File::assertFile($filename);
454
455 4
        $worksheetInfo = [];
456
457
        // Read the OLE file
458 4
        $this->loadOLE($filename);
459
460
        // total byte size of Excel data (workbook global substream + sheet substreams)
461 4
        $this->dataSize = strlen($this->data);
462
463
        // initialize
464 4
        $this->pos = 0;
465 4
        $this->sheets = [];
466
467
        // Parse Workbook Global Substream
468 4
        while ($this->pos < $this->dataSize) {
469 4
            $code = self::getUInt2d($this->data, $this->pos);
470
471 4
            match ($code) {
472 4
                self::XLS_TYPE_BOF => $this->readBof(),
473 4
                self::XLS_TYPE_SHEET => $this->readSheet(),
474 4
                self::XLS_TYPE_EOF => $this->readDefault(),
475 4
                self::XLS_TYPE_CODEPAGE => $this->readCodepage(),
476 4
                default => $this->readDefault(),
477 4
            };
478
479 4
            if ($code === self::XLS_TYPE_EOF) {
480 4
                break;
481
            }
482
        }
483
484
        // Parse the individual sheets
485 4
        foreach ($this->sheets as $sheet) {
486 4
            if ($sheet['sheetType'] != 0x00) {
487
                // 0x00: Worksheet
488
                // 0x02: Chart
489
                // 0x06: Visual Basic module
490
                continue;
491
            }
492
493 4
            $tmpInfo = [];
494 4
            $tmpInfo['worksheetName'] = $sheet['name'];
495 4
            $tmpInfo['lastColumnLetter'] = 'A';
496 4
            $tmpInfo['lastColumnIndex'] = 0;
497 4
            $tmpInfo['totalRows'] = 0;
498 4
            $tmpInfo['totalColumns'] = 0;
499
500 4
            $this->pos = $sheet['offset'];
501
502 4
            while ($this->pos <= $this->dataSize - 4) {
503 4
                $code = self::getUInt2d($this->data, $this->pos);
504
505
                switch ($code) {
506
                    case self::XLS_TYPE_RK:
507
                    case self::XLS_TYPE_LABELSST:
508
                    case self::XLS_TYPE_NUMBER:
509
                    case self::XLS_TYPE_FORMULA:
510
                    case self::XLS_TYPE_BOOLERR:
511
                    case self::XLS_TYPE_LABEL:
512 4
                        $length = self::getUInt2d($this->data, $this->pos + 2);
513 4
                        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
514
515
                        // move stream pointer to next record
516 4
                        $this->pos += 4 + $length;
517
518 4
                        $rowIndex = self::getUInt2d($recordData, 0) + 1;
519 4
                        $columnIndex = self::getUInt2d($recordData, 2);
520
521 4
                        $tmpInfo['totalRows'] = max($tmpInfo['totalRows'], $rowIndex);
522 4
                        $tmpInfo['lastColumnIndex'] = max($tmpInfo['lastColumnIndex'], $columnIndex);
523
524 4
                        break;
525
                    case self::XLS_TYPE_BOF:
526 4
                        $this->readBof();
527
528 4
                        break;
529
                    case self::XLS_TYPE_EOF:
530 4
                        $this->readDefault();
531
532 4
                        break 2;
533
                    default:
534 4
                        $this->readDefault();
535
536 4
                        break;
537
                }
538
            }
539
540 4
            $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1);
541 4
            $tmpInfo['totalColumns'] = $tmpInfo['lastColumnIndex'] + 1;
542
543 4
            $worksheetInfo[] = $tmpInfo;
544
        }
545
546 4
        return $worksheetInfo;
547
    }
548
549
    /**
550
     * Loads PhpSpreadsheet from file.
551
     */
552 96
    protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
553
    {
554
        // Read the OLE file
555 96
        $this->loadOLE($filename);
556
557
        // Initialisations
558 96
        $this->spreadsheet = new Spreadsheet();
559 96
        $this->spreadsheet->removeSheetByIndex(0); // remove 1st sheet
560 96
        if (!$this->readDataOnly) {
561 95
            $this->spreadsheet->removeCellStyleXfByIndex(0); // remove the default style
562 95
            $this->spreadsheet->removeCellXfByIndex(0); // remove the default style
563
        }
564
565
        // Read the summary information stream (containing meta data)
566 96
        $this->readSummaryInformation();
567
568
        // Read the Additional document summary information stream (containing application-specific meta data)
569 96
        $this->readDocumentSummaryInformation();
570
571
        // total byte size of Excel data (workbook global substream + sheet substreams)
572 96
        $this->dataSize = strlen($this->data);
573
574
        // initialize
575 96
        $this->pos = 0;
576 96
        $this->codepage = $this->codepage ?: CodePage::DEFAULT_CODE_PAGE;
577 96
        $this->formats = [];
578 96
        $this->objFonts = [];
579 96
        $this->palette = [];
580 96
        $this->sheets = [];
581 96
        $this->externalBooks = [];
582 96
        $this->ref = [];
583 96
        $this->definedname = [];
584 96
        $this->sst = [];
585 96
        $this->drawingGroupData = '';
586 96
        $this->xfIndex = 0;
587 96
        $this->mapCellXfIndex = [];
588 96
        $this->mapCellStyleXfIndex = [];
589
590
        // Parse Workbook Global Substream
591 96
        while ($this->pos < $this->dataSize) {
592 96
            $code = self::getUInt2d($this->data, $this->pos);
593
594 96
            match ($code) {
595 96
                self::XLS_TYPE_BOF => $this->readBof(),
596 96
                self::XLS_TYPE_FILEPASS => $this->readFilepass(),
597 96
                self::XLS_TYPE_CODEPAGE => $this->readCodepage(),
598 96
                self::XLS_TYPE_DATEMODE => $this->readDateMode(),
599 96
                self::XLS_TYPE_FONT => $this->readFont(),
600 96
                self::XLS_TYPE_FORMAT => $this->readFormat(),
601 96
                self::XLS_TYPE_XF => $this->readXf(),
602 96
                self::XLS_TYPE_XFEXT => $this->readXfExt(),
603 96
                self::XLS_TYPE_STYLE => $this->readStyle(),
604 96
                self::XLS_TYPE_PALETTE => $this->readPalette(),
605 96
                self::XLS_TYPE_SHEET => $this->readSheet(),
606 96
                self::XLS_TYPE_EXTERNALBOOK => $this->readExternalBook(),
607 96
                self::XLS_TYPE_EXTERNNAME => $this->readExternName(),
608 96
                self::XLS_TYPE_EXTERNSHEET => $this->readExternSheet(),
609 96
                self::XLS_TYPE_DEFINEDNAME => $this->readDefinedName(),
610 96
                self::XLS_TYPE_MSODRAWINGGROUP => $this->readMsoDrawingGroup(),
611 96
                self::XLS_TYPE_SST => $this->readSst(),
612 96
                self::XLS_TYPE_EOF => $this->readDefault(),
613 96
                default => $this->readDefault(),
614 96
            };
615
616 96
            if ($code === self::XLS_TYPE_EOF) {
617 96
                break;
618
            }
619
        }
620
621
        // Resolve indexed colors for font, fill, and border colors
622
        // Cannot be resolved already in XF record, because PALETTE record comes afterwards
623 96
        if (!$this->readDataOnly) {
624 95
            foreach ($this->objFonts as $objFont) {
625 94
                if (isset($objFont->colorIndex)) {
626 94
                    $color = Xls\Color::map($objFont->colorIndex, $this->palette, $this->version);
627 94
                    $objFont->getColor()->setRGB($color['rgb']);
628
                }
629
            }
630
631 95
            foreach ($this->spreadsheet->getCellXfCollection() as $objStyle) {
632
                // fill start and end color
633 95
                $fill = $objStyle->getFill();
634
635 95
                if (isset($fill->startcolorIndex)) {
636 95
                    $startColor = Xls\Color::map($fill->startcolorIndex, $this->palette, $this->version);
637 95
                    $fill->getStartColor()->setRGB($startColor['rgb']);
638
                }
639 95
                if (isset($fill->endcolorIndex)) {
640 95
                    $endColor = Xls\Color::map($fill->endcolorIndex, $this->palette, $this->version);
641 95
                    $fill->getEndColor()->setRGB($endColor['rgb']);
642
                }
643
644
                // border colors
645 95
                $top = $objStyle->getBorders()->getTop();
646 95
                $right = $objStyle->getBorders()->getRight();
647 95
                $bottom = $objStyle->getBorders()->getBottom();
648 95
                $left = $objStyle->getBorders()->getLeft();
649 95
                $diagonal = $objStyle->getBorders()->getDiagonal();
650
651 95
                if (isset($top->colorIndex)) {
652 95
                    $borderTopColor = Xls\Color::map($top->colorIndex, $this->palette, $this->version);
653 95
                    $top->getColor()->setRGB($borderTopColor['rgb']);
654
                }
655 95
                if (isset($right->colorIndex)) {
656 95
                    $borderRightColor = Xls\Color::map($right->colorIndex, $this->palette, $this->version);
657 95
                    $right->getColor()->setRGB($borderRightColor['rgb']);
658
                }
659 95
                if (isset($bottom->colorIndex)) {
660 95
                    $borderBottomColor = Xls\Color::map($bottom->colorIndex, $this->palette, $this->version);
661 95
                    $bottom->getColor()->setRGB($borderBottomColor['rgb']);
662
                }
663 95
                if (isset($left->colorIndex)) {
664 95
                    $borderLeftColor = Xls\Color::map($left->colorIndex, $this->palette, $this->version);
665 95
                    $left->getColor()->setRGB($borderLeftColor['rgb']);
666
                }
667 95
                if (isset($diagonal->colorIndex)) {
668 93
                    $borderDiagonalColor = Xls\Color::map($diagonal->colorIndex, $this->palette, $this->version);
669 93
                    $diagonal->getColor()->setRGB($borderDiagonalColor['rgb']);
670
                }
671
            }
672
        }
673
674
        // treat MSODRAWINGGROUP records, workbook-level Escher
675 96
        $escherWorkbook = null;
676 96
        if (!$this->readDataOnly && $this->drawingGroupData) {
677 17
            $escher = new Escher();
678 17
            $reader = new Xls\Escher($escher);
679 17
            $escherWorkbook = $reader->load($this->drawingGroupData);
680
        }
681
682
        // Parse the individual sheets
683 96
        $this->activeSheetSet = false;
684 96
        foreach ($this->sheets as $sheet) {
685 96
            if ($sheet['sheetType'] != 0x00) {
686
                // 0x00: Worksheet, 0x02: Chart, 0x06: Visual Basic module
687
                continue;
688
            }
689
690
            // check if sheet should be skipped
691 96
            if (isset($this->loadSheetsOnly) && !in_array($sheet['name'], $this->loadSheetsOnly)) {
692 8
                continue;
693
            }
694
695
            // add sheet to PhpSpreadsheet object
696 95
            $this->phpSheet = $this->spreadsheet->createSheet();
697
            //    Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in formula
698
            //        cells... during the load, all formulae should be correct, and we're simply bringing the worksheet
699
            //        name in line with the formula, not the reverse
700 95
            $this->phpSheet->setTitle($sheet['name'], false, false);
701 95
            $this->phpSheet->setSheetState($sheet['sheetState']);
702
703 95
            $this->pos = $sheet['offset'];
704
705
            // Initialize isFitToPages. May change after reading SHEETPR record.
706 95
            $this->isFitToPages = false;
707
708
            // Initialize drawingData
709 95
            $this->drawingData = '';
710
711
            // Initialize objs
712 95
            $this->objs = [];
713
714
            // Initialize shared formula parts
715 95
            $this->sharedFormulaParts = [];
716
717
            // Initialize shared formulas
718 95
            $this->sharedFormulas = [];
719
720
            // Initialize text objs
721 95
            $this->textObjects = [];
722
723
            // Initialize cell annotations
724 95
            $this->cellNotes = [];
725 95
            $this->textObjRef = -1;
726
727 95
            while ($this->pos <= $this->dataSize - 4) {
728 95
                $code = self::getUInt2d($this->data, $this->pos);
729
730
                switch ($code) {
731
                    case self::XLS_TYPE_BOF:
732 95
                        $this->readBof();
733
734 95
                        break;
735
                    case self::XLS_TYPE_PRINTGRIDLINES:
736 92
                        $this->readPrintGridlines();
737
738 92
                        break;
739
                    case self::XLS_TYPE_DEFAULTROWHEIGHT:
740 52
                        $this->readDefaultRowHeight();
741
742 52
                        break;
743
                    case self::XLS_TYPE_SHEETPR:
744 94
                        $this->readSheetPr();
745
746 94
                        break;
747
                    case self::XLS_TYPE_HORIZONTALPAGEBREAKS:
748 4
                        $this->readHorizontalPageBreaks();
749
750 4
                        break;
751
                    case self::XLS_TYPE_VERTICALPAGEBREAKS:
752 4
                        $this->readVerticalPageBreaks();
753
754 4
                        break;
755
                    case self::XLS_TYPE_HEADER:
756 92
                        $this->readHeader();
757
758 92
                        break;
759
                    case self::XLS_TYPE_FOOTER:
760 92
                        $this->readFooter();
761
762 92
                        break;
763
                    case self::XLS_TYPE_HCENTER:
764 92
                        $this->readHcenter();
765
766 92
                        break;
767
                    case self::XLS_TYPE_VCENTER:
768 92
                        $this->readVcenter();
769
770 92
                        break;
771
                    case self::XLS_TYPE_LEFTMARGIN:
772 87
                        $this->readLeftMargin();
773
774 87
                        break;
775
                    case self::XLS_TYPE_RIGHTMARGIN:
776 87
                        $this->readRightMargin();
777
778 87
                        break;
779
                    case self::XLS_TYPE_TOPMARGIN:
780 87
                        $this->readTopMargin();
781
782 87
                        break;
783
                    case self::XLS_TYPE_BOTTOMMARGIN:
784 87
                        $this->readBottomMargin();
785
786 87
                        break;
787
                    case self::XLS_TYPE_PAGESETUP:
788 94
                        $this->readPageSetup();
789
790 94
                        break;
791
                    case self::XLS_TYPE_PROTECT:
792 6
                        $this->readProtect();
793
794 6
                        break;
795
                    case self::XLS_TYPE_SCENPROTECT:
796
                        $this->readScenProtect();
797
798
                        break;
799
                    case self::XLS_TYPE_OBJECTPROTECT:
800 1
                        $this->readObjectProtect();
801
802 1
                        break;
803
                    case self::XLS_TYPE_PASSWORD:
804 2
                        $this->readPassword();
805
806 2
                        break;
807
                    case self::XLS_TYPE_DEFCOLWIDTH:
808 93
                        $this->readDefColWidth();
809
810 93
                        break;
811
                    case self::XLS_TYPE_COLINFO:
812 85
                        $this->readColInfo();
813
814 85
                        break;
815
                    case self::XLS_TYPE_DIMENSION:
816 95
                        $this->readDefault();
817
818 95
                        break;
819
                    case self::XLS_TYPE_ROW:
820 59
                        $this->readRow();
821
822 59
                        break;
823
                    case self::XLS_TYPE_DBCELL:
824 46
                        $this->readDefault();
825
826 46
                        break;
827
                    case self::XLS_TYPE_RK:
828 27
                        $this->readRk();
829
830 27
                        break;
831
                    case self::XLS_TYPE_LABELSST:
832 59
                        $this->readLabelSst();
833
834 59
                        break;
835
                    case self::XLS_TYPE_MULRK:
836 21
                        $this->readMulRk();
837
838 21
                        break;
839
                    case self::XLS_TYPE_NUMBER:
840 47
                        $this->readNumber();
841
842 47
                        break;
843
                    case self::XLS_TYPE_FORMULA:
844 29
                        $this->readFormula();
845
846 29
                        break;
847
                    case self::XLS_TYPE_SHAREDFMLA:
848
                        $this->readSharedFmla();
849
850
                        break;
851
                    case self::XLS_TYPE_BOOLERR:
852 10
                        $this->readBoolErr();
853
854 10
                        break;
855
                    case self::XLS_TYPE_MULBLANK:
856 25
                        $this->readMulBlank();
857
858 25
                        break;
859
                    case self::XLS_TYPE_LABEL:
860 4
                        $this->readLabel();
861
862 4
                        break;
863
                    case self::XLS_TYPE_BLANK:
864 24
                        $this->readBlank();
865
866 24
                        break;
867
                    case self::XLS_TYPE_MSODRAWING:
868 16
                        $this->readMsoDrawing();
869
870 16
                        break;
871
                    case self::XLS_TYPE_OBJ:
872 12
                        $this->readObj();
873
874 12
                        break;
875
                    case self::XLS_TYPE_WINDOW2:
876 95
                        $this->readWindow2();
877
878 95
                        break;
879
                    case self::XLS_TYPE_PAGELAYOUTVIEW:
880 82
                        $this->readPageLayoutView();
881
882 82
                        break;
883
                    case self::XLS_TYPE_SCL:
884 5
                        $this->readScl();
885
886 5
                        break;
887
                    case self::XLS_TYPE_PANE:
888 8
                        $this->readPane();
889
890 8
                        break;
891
                    case self::XLS_TYPE_SELECTION:
892 92
                        $this->readSelection();
893
894 92
                        break;
895
                    case self::XLS_TYPE_MERGEDCELLS:
896 18
                        $this->readMergedCells();
897
898 18
                        break;
899
                    case self::XLS_TYPE_HYPERLINK:
900 6
                        $this->readHyperLink();
901
902 6
                        break;
903
                    case self::XLS_TYPE_DATAVALIDATIONS:
904 3
                        $this->readDataValidations();
905
906 3
                        break;
907
                    case self::XLS_TYPE_DATAVALIDATION:
908 3
                        $this->readDataValidation();
909
910 3
                        break;
911
                    case self::XLS_TYPE_CFHEADER:
912 16
                        $cellRangeAddresses = $this->readCFHeader();
913
914 16
                        break;
915
                    case self::XLS_TYPE_CFRULE:
916 16
                        $this->readCFRule($cellRangeAddresses ?? []);
917
918 16
                        break;
919
                    case self::XLS_TYPE_SHEETLAYOUT:
920 5
                        $this->readSheetLayout();
921
922 5
                        break;
923
                    case self::XLS_TYPE_SHEETPROTECTION:
924 86
                        $this->readSheetProtection();
925
926 86
                        break;
927
                    case self::XLS_TYPE_RANGEPROTECTION:
928 1
                        $this->readRangeProtection();
929
930 1
                        break;
931
                    case self::XLS_TYPE_NOTE:
932 3
                        $this->readNote();
933
934 3
                        break;
935
                    case self::XLS_TYPE_TXO:
936 2
                        $this->readTextObject();
937
938 2
                        break;
939
                    case self::XLS_TYPE_CONTINUE:
940 1
                        $this->readContinue();
941
942 1
                        break;
943
                    case self::XLS_TYPE_EOF:
944 95
                        $this->readDefault();
945
946 95
                        break 2;
947
                    default:
948 94
                        $this->readDefault();
949
950 94
                        break;
951
                }
952
            }
953
954
            // treat MSODRAWING records, sheet-level Escher
955 95
            if (!$this->readDataOnly && $this->drawingData) {
956 16
                $escherWorksheet = new Escher();
957 16
                $reader = new Xls\Escher($escherWorksheet);
958 16
                $escherWorksheet = $reader->load($this->drawingData);
959
960
                // get all spContainers in one long array, so they can be mapped to OBJ records
961
                /** @var SpContainer[] $allSpContainers */
962 16
                $allSpContainers = method_exists($escherWorksheet, 'getDgContainer') ? $escherWorksheet->getDgContainer()->getSpgrContainer()->getAllSpContainers() : [];
963
            }
964
965
            // treat OBJ records
966 95
            foreach ($this->objs as $n => $obj) {
967
                // the first shape container never has a corresponding OBJ record, hence $n + 1
968 11
                if (isset($allSpContainers[$n + 1])) {
969 11
                    $spContainer = $allSpContainers[$n + 1];
970
971
                    // we skip all spContainers that are a part of a group shape since we cannot yet handle those
972 11
                    if ($spContainer->getNestingLevel() > 1) {
973
                        continue;
974
                    }
975
976
                    // calculate the width and height of the shape
977
                    /** @var int $startRow */
978 11
                    [$startColumn, $startRow] = Coordinate::coordinateFromString($spContainer->getStartCoordinates());
979
                    /** @var int $endRow */
980 11
                    [$endColumn, $endRow] = Coordinate::coordinateFromString($spContainer->getEndCoordinates());
981
982 11
                    $startOffsetX = $spContainer->getStartOffsetX();
983 11
                    $startOffsetY = $spContainer->getStartOffsetY();
984 11
                    $endOffsetX = $spContainer->getEndOffsetX();
985 11
                    $endOffsetY = $spContainer->getEndOffsetY();
986
987 11
                    $width = SharedXls::getDistanceX($this->phpSheet, $startColumn, $startOffsetX, $endColumn, $endOffsetX);
988 11
                    $height = SharedXls::getDistanceY($this->phpSheet, $startRow, $startOffsetY, $endRow, $endOffsetY);
989
990
                    // calculate offsetX and offsetY of the shape
991 11
                    $offsetX = (int) ($startOffsetX * SharedXls::sizeCol($this->phpSheet, $startColumn) / 1024);
992 11
                    $offsetY = (int) ($startOffsetY * SharedXls::sizeRow($this->phpSheet, $startRow) / 256);
993
994 11
                    switch ($obj['otObjType']) {
995 11
                        case 0x19:
996
                            // Note
997 2
                            if (isset($this->cellNotes[$obj['idObjID']])) {
998
                                //$cellNote = $this->cellNotes[$obj['idObjID']];
999
1000 2
                                if (isset($this->textObjects[$obj['idObjID']])) {
1001 2
                                    $textObject = $this->textObjects[$obj['idObjID']];
1002 2
                                    $this->cellNotes[$obj['idObjID']]['objTextData'] = $textObject;
1003
                                }
1004
                            }
1005
1006 2
                            break;
1007 11
                        case 0x08:
1008
                            // picture
1009
                            // get index to BSE entry (1-based)
1010 11
                            $BSEindex = $spContainer->getOPT(0x0104);
1011
1012
                            // If there is no BSE Index, we will fail here and other fields are not read.
1013
                            // Fix by checking here.
1014
                            // TODO: Why is there no BSE Index? Is this a new Office Version? Password protected field?
1015
                            // More likely : a uncompatible picture
1016 11
                            if (!$BSEindex) {
1017
                                continue 2;
1018
                            }
1019
1020 11
                            if ($escherWorkbook) {
1021 11
                                $BSECollection = method_exists($escherWorkbook, 'getDggContainer') ? $escherWorkbook->getDggContainer()->getBstoreContainer()->getBSECollection() : [];
1022 11
                                $BSE = $BSECollection[$BSEindex - 1];
1023 11
                                $blipType = $BSE->getBlipType();
1024
1025
                                // need check because some blip types are not supported by Escher reader such as EMF
1026 11
                                if ($blip = $BSE->getBlip()) {
1027 11
                                    $ih = imagecreatefromstring($blip->getData());
1028 11
                                    if ($ih !== false) {
1029 11
                                        $drawing = new MemoryDrawing();
1030 11
                                        $drawing->setImageResource($ih);
1031
1032
                                        // width, height, offsetX, offsetY
1033 11
                                        $drawing->setResizeProportional(false);
1034 11
                                        $drawing->setWidth($width);
1035 11
                                        $drawing->setHeight($height);
1036 11
                                        $drawing->setOffsetX($offsetX);
1037 11
                                        $drawing->setOffsetY($offsetY);
1038
1039
                                        switch ($blipType) {
1040 10
                                            case BSE::BLIPTYPE_JPEG:
1041 9
                                                $drawing->setRenderingFunction(MemoryDrawing::RENDERING_JPEG);
1042 9
                                                $drawing->setMimeType(MemoryDrawing::MIMETYPE_JPEG);
1043
1044 9
                                                break;
1045 10
                                            case BSE::BLIPTYPE_PNG:
1046 11
                                                imagealphablending($ih, false);
1047 11
                                                imagesavealpha($ih, true);
1048 11
                                                $drawing->setRenderingFunction(MemoryDrawing::RENDERING_PNG);
1049 11
                                                $drawing->setMimeType(MemoryDrawing::MIMETYPE_PNG);
1050
1051 11
                                                break;
1052
                                        }
1053
1054 11
                                        $drawing->setWorksheet($this->phpSheet);
1055 11
                                        $drawing->setCoordinates($spContainer->getStartCoordinates());
1056
                                    }
1057
                                }
1058
                            }
1059
1060 11
                            break;
1061
                        default:
1062
                            // other object type
1063
                            break;
1064
                    }
1065
                }
1066
            }
1067
1068
            // treat SHAREDFMLA records
1069 95
            if ($this->version == self::XLS_BIFF8) {
1070 93
                foreach ($this->sharedFormulaParts as $cell => $baseCell) {
1071
                    /** @var int $row */
1072
                    [$column, $row] = Coordinate::coordinateFromString($cell);
1073
                    if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($column, $row, $this->phpSheet->getTitle())) {
1074
                        $formula = $this->getFormulaFromStructure($this->sharedFormulas[$baseCell], $cell);
1075
                        $this->phpSheet->getCell($cell)->setValueExplicit('=' . $formula, DataType::TYPE_FORMULA);
1076
                    }
1077
                }
1078
            }
1079
1080 95
            if (!empty($this->cellNotes)) {
1081 2
                foreach ($this->cellNotes as $note => $noteDetails) {
1082 2
                    if (!isset($noteDetails['objTextData'])) {
1083
                        if (isset($this->textObjects[$note])) {
1084
                            $textObject = $this->textObjects[$note];
1085
                            $noteDetails['objTextData'] = $textObject;
1086
                        } else {
1087
                            $noteDetails['objTextData']['text'] = '';
1088
                        }
1089
                    }
1090 2
                    $cellAddress = str_replace('$', '', $noteDetails['cellRef']);
1091 2
                    $this->phpSheet->getComment($cellAddress)->setAuthor($noteDetails['author'])->setText($this->parseRichText($noteDetails['objTextData']['text']));
1092
                }
1093
            }
1094
        }
1095 96
        if ($this->activeSheetSet === false) {
1096 5
            $this->spreadsheet->setActiveSheetIndex(0);
1097
        }
1098
1099
        // add the named ranges (defined names)
1100 95
        foreach ($this->definedname as $definedName) {
1101 15
            if ($definedName['isBuiltInName']) {
1102 5
                switch ($definedName['name']) {
1103 5
                    case pack('C', 0x06):
1104
                        // print area
1105
                        //    in general, formula looks like this: Foo!$C$7:$J$66,Bar!$A$1:$IV$2
1106 5
                        $ranges = explode(',', $definedName['formula']); // FIXME: what if sheetname contains comma?
1107
1108 5
                        $extractedRanges = [];
1109 5
                        $sheetName = '';
1110
                        /** @var non-empty-string $range */
1111 5
                        foreach ($ranges as $range) {
1112
                            // $range should look like one of these
1113
                            //        Foo!$C$7:$J$66
1114
                            //        Bar!$A$1:$IV$2
1115 5
                            $explodes = Worksheet::extractSheetTitle($range, true);
1116 5
                            $sheetName = trim($explodes[0], "'");
1117 5
                            if (!str_contains($explodes[1], ':')) {
1118
                                $explodes[1] = $explodes[1] . ':' . $explodes[1];
1119
                            }
1120 5
                            $extractedRanges[] = str_replace('$', '', $explodes[1]); // C7:J66
1121
                        }
1122 5
                        if ($docSheet = $this->spreadsheet->getSheetByName($sheetName)) {
1123 5
                            $docSheet->getPageSetup()->setPrintArea(implode(',', $extractedRanges)); // C7:J66,A1:IV2
1124
                        }
1125
1126 5
                        break;
1127
                    case pack('C', 0x07):
1128
                        // print titles (repeating rows)
1129
                        // Assuming BIFF8, there are 3 cases
1130
                        // 1. repeating rows
1131
                        //        formula looks like this: Sheet!$A$1:$IV$2
1132
                        //        rows 1-2 repeat
1133
                        // 2. repeating columns
1134
                        //        formula looks like this: Sheet!$A$1:$B$65536
1135
                        //        columns A-B repeat
1136
                        // 3. both repeating rows and repeating columns
1137
                        //        formula looks like this: Sheet!$A$1:$B$65536,Sheet!$A$1:$IV$2
1138
                        $ranges = explode(',', $definedName['formula']); // FIXME: what if sheetname contains comma?
1139
                        foreach ($ranges as $range) {
1140
                            // $range should look like this one of these
1141
                            //        Sheet!$A$1:$B$65536
1142
                            //        Sheet!$A$1:$IV$2
1143
                            if (str_contains($range, '!')) {
1144
                                $explodes = Worksheet::extractSheetTitle($range, true);
1145
                                if ($docSheet = $this->spreadsheet->getSheetByName($explodes[0])) {
1146
                                    $extractedRange = $explodes[1];
1147
                                    $extractedRange = str_replace('$', '', $extractedRange);
1148
1149
                                    $coordinateStrings = explode(':', $extractedRange);
1150
                                    if (count($coordinateStrings) == 2) {
1151
                                        [$firstColumn, $firstRow] = Coordinate::coordinateFromString($coordinateStrings[0]);
1152
                                        [$lastColumn, $lastRow] = Coordinate::coordinateFromString($coordinateStrings[1]);
1153
1154
                                        if ($firstColumn == 'A' && $lastColumn == 'IV') {
1155
                                            // then we have repeating rows
1156
                                            $docSheet->getPageSetup()->setRowsToRepeatAtTop([$firstRow, $lastRow]);
1157
                                        } elseif ($firstRow == 1 && $lastRow == 65536) {
1158
                                            // then we have repeating columns
1159
                                            $docSheet->getPageSetup()->setColumnsToRepeatAtLeft([$firstColumn, $lastColumn]);
1160
                                        }
1161
                                    }
1162
                                }
1163
                            }
1164
                        }
1165
1166 5
                        break;
1167
                }
1168
            } else {
1169
                // Extract range
1170
                /** @var non-empty-string $formula */
1171 10
                $formula = $definedName['formula'];
1172 10
                if (str_contains($formula, '!')) {
1173 5
                    $explodes = Worksheet::extractSheetTitle($formula, true);
1174
                    if (
1175 5
                        ($docSheet = $this->spreadsheet->getSheetByName($explodes[0]))
1176 5
                        || ($docSheet = $this->spreadsheet->getSheetByName(trim($explodes[0], "'")))
1177
                    ) {
1178 5
                        $extractedRange = $explodes[1];
1179
1180 5
                        $localOnly = ($definedName['scope'] === 0) ? false : true;
1181
1182 5
                        $scope = ($definedName['scope'] === 0) ? null : $this->spreadsheet->getSheetByName($this->sheets[$definedName['scope'] - 1]['name']);
1183
1184 5
                        $this->spreadsheet->addNamedRange(new NamedRange((string) $definedName['name'], $docSheet, $extractedRange, $localOnly, $scope));
1185
                    }
1186
                }
1187
                //    Named Value
1188
                //    TODO Provide support for named values
1189
            }
1190
        }
1191 95
        $this->data = '';
1192
1193 95
        return $this->spreadsheet;
1194
    }
1195
1196
    /**
1197
     * Read record data from stream, decrypting as required.
1198
     *
1199
     * @param string $data Data stream to read from
1200
     * @param int $pos Position to start reading from
1201
     * @param int $len Record data length
1202
     *
1203
     * @return string Record data
1204
     */
1205 105
    private function readRecordData(string $data, int $pos, int $len): string
1206
    {
1207 105
        $data = substr($data, $pos, $len);
1208
1209
        // File not encrypted, or record before encryption start point
1210 105
        if ($this->encryption == self::MS_BIFF_CRYPTO_NONE || $pos < $this->encryptionStartPos) {
1211 105
            return $data;
1212
        }
1213
1214
        $recordData = '';
1215
        if ($this->encryption == self::MS_BIFF_CRYPTO_RC4) {
1216
            $oldBlock = floor($this->rc4Pos / self::REKEY_BLOCK);
1217
            $block = (int) floor($pos / self::REKEY_BLOCK);
1218
            $endBlock = (int) floor(($pos + $len) / self::REKEY_BLOCK);
1219
1220
            // Spin an RC4 decryptor to the right spot. If we have a decryptor sitting
1221
            // at a point earlier in the current block, re-use it as we can save some time.
1222
            if ($block != $oldBlock || $pos < $this->rc4Pos || !$this->rc4Key) {
1223
                $this->rc4Key = $this->makeKey($block, $this->md5Ctxt);
1224
                $step = $pos % self::REKEY_BLOCK;
1225
            } else {
1226
                $step = $pos - $this->rc4Pos;
1227
            }
1228
            $this->rc4Key->RC4(str_repeat("\0", $step));
1229
1230
            // Decrypt record data (re-keying at the end of every block)
1231
            while ($block != $endBlock) {
1232
                $step = self::REKEY_BLOCK - ($pos % self::REKEY_BLOCK);
1233
                $recordData .= $this->rc4Key->RC4(substr($data, 0, $step));
1234
                $data = substr($data, $step);
1235
                $pos += $step;
1236
                $len -= $step;
1237
                ++$block;
1238
                $this->rc4Key = $this->makeKey($block, $this->md5Ctxt);
1239
            }
1240
            $recordData .= $this->rc4Key->RC4(substr($data, 0, $len));
1241
1242
            // Keep track of the position of this decryptor.
1243
            // We'll try and re-use it later if we can to speed things up
1244
            $this->rc4Pos = $pos + $len;
1245
        } elseif ($this->encryption == self::MS_BIFF_CRYPTO_XOR) {
1246
            throw new Exception('XOr encryption not supported');
1247
        }
1248
1249
        return $recordData;
1250
    }
1251
1252
    /**
1253
     * Use OLE reader to extract the relevant data streams from the OLE file.
1254
     */
1255 105
    private function loadOLE(string $filename): void
1256
    {
1257
        // OLE reader
1258 105
        $ole = new OLERead();
1259
        // get excel data,
1260 105
        $ole->read($filename);
1261
        // Get workbook data: workbook stream + sheet streams
1262 105
        $this->data = $ole->getStream($ole->wrkbook); // @phpstan-ignore-line
1263
        // Get summary information data
1264 105
        $this->summaryInformation = $ole->getStream($ole->summaryInformation);
1265
        // Get additional document summary information data
1266 105
        $this->documentSummaryInformation = $ole->getStream($ole->documentSummaryInformation);
1267
    }
1268
1269
    /**
1270
     * Read summary information.
1271
     */
1272 96
    private function readSummaryInformation(): void
1273
    {
1274 96
        if (!isset($this->summaryInformation)) {
1275 3
            return;
1276
        }
1277
1278
        // offset: 0; size: 2; must be 0xFE 0xFF (UTF-16 LE byte order mark)
1279
        // offset: 2; size: 2;
1280
        // offset: 4; size: 2; OS version
1281
        // offset: 6; size: 2; OS indicator
1282
        // offset: 8; size: 16
1283
        // offset: 24; size: 4; section count
1284
        //$secCount = self::getInt4d($this->summaryInformation, 24);
1285
1286
        // 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
1287
        // offset: 44; size: 4
1288 93
        $secOffset = self::getInt4d($this->summaryInformation, 44);
1289
1290
        // section header
1291
        // offset: $secOffset; size: 4; section length
1292
        //$secLength = self::getInt4d($this->summaryInformation, $secOffset);
1293
1294
        // offset: $secOffset+4; size: 4; property count
1295 93
        $countProperties = self::getInt4d($this->summaryInformation, $secOffset + 4);
1296
1297
        // initialize code page (used to resolve string values)
1298 93
        $codePage = 'CP1252';
1299
1300
        // offset: ($secOffset+8); size: var
1301
        // loop through property decarations and properties
1302 93
        for ($i = 0; $i < $countProperties; ++$i) {
1303
            // offset: ($secOffset+8) + (8 * $i); size: 4; property ID
1304 93
            $id = self::getInt4d($this->summaryInformation, ($secOffset + 8) + (8 * $i));
1305
1306
            // Use value of property id as appropriate
1307
            // offset: ($secOffset+12) + (8 * $i); size: 4; offset from beginning of section (48)
1308 93
            $offset = self::getInt4d($this->summaryInformation, ($secOffset + 12) + (8 * $i));
1309
1310 93
            $type = self::getInt4d($this->summaryInformation, $secOffset + $offset);
1311
1312
            // initialize property value
1313 93
            $value = null;
1314
1315
            // extract property value based on property type
1316
            switch ($type) {
1317 93
                case 0x02: // 2 byte signed integer
1318 93
                    $value = self::getUInt2d($this->summaryInformation, $secOffset + 4 + $offset);
1319
1320 93
                    break;
1321 93
                case 0x03: // 4 byte signed integer
1322 89
                    $value = self::getInt4d($this->summaryInformation, $secOffset + 4 + $offset);
1323
1324 89
                    break;
1325 93
                case 0x13: // 4 byte unsigned integer
1326
                    // not needed yet, fix later if necessary
1327 1
                    break;
1328 93
                case 0x1E: // null-terminated string prepended by dword string length
1329 91
                    $byteLength = self::getInt4d($this->summaryInformation, $secOffset + 4 + $offset);
1330 91
                    $value = substr($this->summaryInformation, $secOffset + 8 + $offset, $byteLength);
1331 91
                    $value = StringHelper::convertEncoding($value, 'UTF-8', $codePage);
1332 91
                    $value = rtrim($value);
1333
1334 91
                    break;
1335 93
                case 0x40: // Filetime (64-bit value representing the number of 100-nanosecond intervals since January 1, 1601)
1336
                    // PHP-time
1337 93
                    $value = OLE::OLE2LocalDate(substr($this->summaryInformation, $secOffset + 4 + $offset, 8));
1338
1339 93
                    break;
1340 2
                case 0x47: // Clipboard format
1341
                    // not needed yet, fix later if necessary
1342
                    break;
1343
            }
1344
1345
            switch ($id) {
1346 93
                case 0x01:    //    Code Page
1347 93
                    $codePage = CodePage::numberToName((int) $value);
1348
1349 93
                    break;
1350 93
                case 0x02:    //    Title
1351 52
                    $this->spreadsheet->getProperties()->setTitle("$value");
1352
1353 52
                    break;
1354 93
                case 0x03:    //    Subject
1355 15
                    $this->spreadsheet->getProperties()->setSubject("$value");
1356
1357 15
                    break;
1358 93
                case 0x04:    //    Author (Creator)
1359 80
                    $this->spreadsheet->getProperties()->setCreator("$value");
1360
1361 80
                    break;
1362 93
                case 0x05:    //    Keywords
1363 15
                    $this->spreadsheet->getProperties()->setKeywords("$value");
1364
1365 15
                    break;
1366 93
                case 0x06:    //    Comments (Description)
1367 15
                    $this->spreadsheet->getProperties()->setDescription("$value");
1368
1369 15
                    break;
1370 93
                case 0x07:    //    Template
1371
                    //    Not supported by PhpSpreadsheet
1372
                    break;
1373 93
                case 0x08:    //    Last Saved By (LastModifiedBy)
1374 91
                    $this->spreadsheet->getProperties()->setLastModifiedBy("$value");
1375
1376 91
                    break;
1377 93
                case 0x09:    //    Revision
1378
                    //    Not supported by PhpSpreadsheet
1379 3
                    break;
1380 93
                case 0x0A:    //    Total Editing Time
1381
                    //    Not supported by PhpSpreadsheet
1382 3
                    break;
1383 93
                case 0x0B:    //    Last Printed
1384
                    //    Not supported by PhpSpreadsheet
1385 7
                    break;
1386 93
                case 0x0C:    //    Created Date/Time
1387 87
                    $this->spreadsheet->getProperties()->setCreated($value);
1388
1389 87
                    break;
1390 93
                case 0x0D:    //    Modified Date/Time
1391 92
                    $this->spreadsheet->getProperties()->setModified($value);
1392
1393 92
                    break;
1394 90
                case 0x0E:    //    Number of Pages
1395
                    //    Not supported by PhpSpreadsheet
1396
                    break;
1397 90
                case 0x0F:    //    Number of Words
1398
                    //    Not supported by PhpSpreadsheet
1399
                    break;
1400 90
                case 0x10:    //    Number of Characters
1401
                    //    Not supported by PhpSpreadsheet
1402
                    break;
1403 90
                case 0x11:    //    Thumbnail
1404
                    //    Not supported by PhpSpreadsheet
1405
                    break;
1406 90
                case 0x12:    //    Name of creating application
1407
                    //    Not supported by PhpSpreadsheet
1408 34
                    break;
1409 90
                case 0x13:    //    Security
1410
                    //    Not supported by PhpSpreadsheet
1411 89
                    break;
1412
            }
1413
        }
1414
    }
1415
1416
    /**
1417
     * Read additional document summary information.
1418
     */
1419 96
    private function readDocumentSummaryInformation(): void
1420
    {
1421 96
        if (!isset($this->documentSummaryInformation)) {
1422 4
            return;
1423
        }
1424
1425
        //    offset: 0;    size: 2;    must be 0xFE 0xFF (UTF-16 LE byte order mark)
1426
        //    offset: 2;    size: 2;
1427
        //    offset: 4;    size: 2;    OS version
1428
        //    offset: 6;    size: 2;    OS indicator
1429
        //    offset: 8;    size: 16
1430
        //    offset: 24;    size: 4;    section count
1431
        //$secCount = self::getInt4d($this->documentSummaryInformation, 24);
1432
1433
        // 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
1434
        // offset: 44;    size: 4;    first section offset
1435 92
        $secOffset = self::getInt4d($this->documentSummaryInformation, 44);
1436
1437
        //    section header
1438
        //    offset: $secOffset;    size: 4;    section length
1439
        //$secLength = self::getInt4d($this->documentSummaryInformation, $secOffset);
1440
1441
        //    offset: $secOffset+4;    size: 4;    property count
1442 92
        $countProperties = self::getInt4d($this->documentSummaryInformation, $secOffset + 4);
1443
1444
        // initialize code page (used to resolve string values)
1445 92
        $codePage = 'CP1252';
1446
1447
        //    offset: ($secOffset+8);    size: var
1448
        //    loop through property decarations and properties
1449 92
        for ($i = 0; $i < $countProperties; ++$i) {
1450
            //    offset: ($secOffset+8) + (8 * $i);    size: 4;    property ID
1451 92
            $id = self::getInt4d($this->documentSummaryInformation, ($secOffset + 8) + (8 * $i));
1452
1453
            // Use value of property id as appropriate
1454
            // offset: 60 + 8 * $i;    size: 4;    offset from beginning of section (48)
1455 92
            $offset = self::getInt4d($this->documentSummaryInformation, ($secOffset + 12) + (8 * $i));
1456
1457 92
            $type = self::getInt4d($this->documentSummaryInformation, $secOffset + $offset);
1458
1459
            // initialize property value
1460 92
            $value = null;
1461
1462
            // extract property value based on property type
1463
            switch ($type) {
1464 92
                case 0x02:    //    2 byte signed integer
1465 92
                    $value = self::getUInt2d($this->documentSummaryInformation, $secOffset + 4 + $offset);
1466
1467 92
                    break;
1468 89
                case 0x03:    //    4 byte signed integer
1469 86
                    $value = self::getInt4d($this->documentSummaryInformation, $secOffset + 4 + $offset);
1470
1471 86
                    break;
1472 89
                case 0x0B:  // Boolean
1473 89
                    $value = self::getUInt2d($this->documentSummaryInformation, $secOffset + 4 + $offset);
1474 89
                    $value = ($value == 0 ? false : true);
1475
1476 89
                    break;
1477 88
                case 0x13:    //    4 byte unsigned integer
1478
                    // not needed yet, fix later if necessary
1479 1
                    break;
1480 87
                case 0x1E:    //    null-terminated string prepended by dword string length
1481 36
                    $byteLength = self::getInt4d($this->documentSummaryInformation, $secOffset + 4 + $offset);
1482 36
                    $value = substr($this->documentSummaryInformation, $secOffset + 8 + $offset, $byteLength);
1483 36
                    $value = StringHelper::convertEncoding($value, 'UTF-8', $codePage);
1484 36
                    $value = rtrim($value);
1485
1486 36
                    break;
1487 87
                case 0x40:    //    Filetime (64-bit value representing the number of 100-nanosecond intervals since January 1, 1601)
1488
                    // PHP-Time
1489
                    $value = OLE::OLE2LocalDate(substr($this->documentSummaryInformation, $secOffset + 4 + $offset, 8));
1490
1491
                    break;
1492 87
                case 0x47:    //    Clipboard format
1493
                    // not needed yet, fix later if necessary
1494
                    break;
1495
            }
1496
1497
            switch ($id) {
1498 92
                case 0x01:    //    Code Page
1499 92
                    $codePage = CodePage::numberToName((int) $value);
1500
1501 92
                    break;
1502 89
                case 0x02:    //    Category
1503 15
                    $this->spreadsheet->getProperties()->setCategory("$value");
1504
1505 15
                    break;
1506 89
                case 0x03:    //    Presentation Target
1507
                    //    Not supported by PhpSpreadsheet
1508
                    break;
1509 89
                case 0x04:    //    Bytes
1510
                    //    Not supported by PhpSpreadsheet
1511
                    break;
1512 89
                case 0x05:    //    Lines
1513
                    //    Not supported by PhpSpreadsheet
1514
                    break;
1515 89
                case 0x06:    //    Paragraphs
1516
                    //    Not supported by PhpSpreadsheet
1517
                    break;
1518 89
                case 0x07:    //    Slides
1519
                    //    Not supported by PhpSpreadsheet
1520
                    break;
1521 89
                case 0x08:    //    Notes
1522
                    //    Not supported by PhpSpreadsheet
1523
                    break;
1524 89
                case 0x09:    //    Hidden Slides
1525
                    //    Not supported by PhpSpreadsheet
1526
                    break;
1527 89
                case 0x0A:    //    MM Clips
1528
                    //    Not supported by PhpSpreadsheet
1529
                    break;
1530 89
                case 0x0B:    //    Scale Crop
1531
                    //    Not supported by PhpSpreadsheet
1532 89
                    break;
1533 89
                case 0x0C:    //    Heading Pairs
1534
                    //    Not supported by PhpSpreadsheet
1535 87
                    break;
1536 89
                case 0x0D:    //    Titles of Parts
1537
                    //    Not supported by PhpSpreadsheet
1538 87
                    break;
1539 89
                case 0x0E:    //    Manager
1540 2
                    $this->spreadsheet->getProperties()->setManager("$value");
1541
1542 2
                    break;
1543 89
                case 0x0F:    //    Company
1544 26
                    $this->spreadsheet->getProperties()->setCompany("$value");
1545
1546 26
                    break;
1547 89
                case 0x10:    //    Links up-to-date
1548
                    //    Not supported by PhpSpreadsheet
1549 89
                    break;
1550
            }
1551
        }
1552
    }
1553
1554
    /**
1555
     * Reads a general type of BIFF record. Does nothing except for moving stream pointer forward to next record.
1556
     */
1557 105
    private function readDefault(): void
1558
    {
1559 105
        $length = self::getUInt2d($this->data, $this->pos + 2);
1560
1561
        // move stream pointer to next record
1562 105
        $this->pos += 4 + $length;
1563
    }
1564
1565
    /**
1566
     *    The NOTE record specifies a comment associated with a particular cell. In Excel 95 (BIFF7) and earlier versions,
1567
     *        this record stores a note (cell note). This feature was significantly enhanced in Excel 97.
1568
     */
1569 3
    private function readNote(): void
1570
    {
1571 3
        $length = self::getUInt2d($this->data, $this->pos + 2);
1572 3
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
1573
1574
        // move stream pointer to next record
1575 3
        $this->pos += 4 + $length;
1576
1577 3
        if ($this->readDataOnly) {
1578
            return;
1579
        }
1580
1581 3
        $cellAddress = $this->readBIFF8CellAddress(substr($recordData, 0, 4));
1582 3
        if ($this->version == self::XLS_BIFF8) {
1583 2
            $noteObjID = self::getUInt2d($recordData, 6);
1584 2
            $noteAuthor = self::readUnicodeStringLong(substr($recordData, 8));
1585 2
            $noteAuthor = $noteAuthor['value'];
1586 2
            $this->cellNotes[$noteObjID] = [
1587 2
                'cellRef' => $cellAddress,
1588 2
                'objectID' => $noteObjID,
1589 2
                'author' => $noteAuthor,
1590 2
            ];
1591
        } else {
1592 1
            $extension = false;
1593 1
            if ($cellAddress == '$B$65536') {
1594
                //    If the address row is -1 and the column is 0, (which translates as $B$65536) then this is a continuation
1595
                //        note from the previous cell annotation. We're not yet handling this, so annotations longer than the
1596
                //        max 2048 bytes will probably throw a wobbly.
1597
                //$row = self::getUInt2d($recordData, 0);
1598
                $extension = true;
1599
                $arrayKeys = array_keys($this->phpSheet->getComments());
1600
                $cellAddress = array_pop($arrayKeys);
1601
            }
1602
1603 1
            $cellAddress = str_replace('$', '', (string) $cellAddress);
1604
            //$noteLength = self::getUInt2d($recordData, 4);
1605 1
            $noteText = trim(substr($recordData, 6));
1606
1607 1
            if ($extension) {
1608
                //    Concatenate this extension with the currently set comment for the cell
1609
                $comment = $this->phpSheet->getComment($cellAddress);
1610
                $commentText = $comment->getText()->getPlainText();
1611
                $comment->setText($this->parseRichText($commentText . $noteText));
1612
            } else {
1613
                //    Set comment for the cell
1614 1
                $this->phpSheet->getComment($cellAddress)->setText($this->parseRichText($noteText));
1615
//                                                    ->setAuthor($author)
1616
            }
1617
        }
1618
    }
1619
1620
    /**
1621
     * The TEXT Object record contains the text associated with a cell annotation.
1622
     */
1623 2
    private function readTextObject(): void
1624
    {
1625 2
        $length = self::getUInt2d($this->data, $this->pos + 2);
1626 2
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
1627
1628
        // move stream pointer to next record
1629 2
        $this->pos += 4 + $length;
1630
1631 2
        if ($this->readDataOnly) {
1632
            return;
1633
        }
1634
1635
        // recordData consists of an array of subrecords looking like this:
1636
        //    grbit: 2 bytes; Option Flags
1637
        //    rot: 2 bytes; rotation
1638
        //    cchText: 2 bytes; length of the text (in the first continue record)
1639
        //    cbRuns: 2 bytes; length of the formatting (in the second continue record)
1640
        // followed by the continuation records containing the actual text and formatting
1641 2
        $grbitOpts = self::getUInt2d($recordData, 0);
1642 2
        $rot = self::getUInt2d($recordData, 2);
1643
        //$cchText = self::getUInt2d($recordData, 10);
1644 2
        $cbRuns = self::getUInt2d($recordData, 12);
1645 2
        $text = $this->getSplicedRecordData();
1646
1647 2
        $textByte = $text['spliceOffsets'][1] - $text['spliceOffsets'][0] - 1;
1648 2
        $textStr = substr($text['recordData'], $text['spliceOffsets'][0] + 1, $textByte);
1649
        // get 1 byte
1650 2
        $is16Bit = ord($text['recordData'][0]);
1651
        // it is possible to use a compressed format,
1652
        // which omits the high bytes of all characters, if they are all zero
1653 2
        if (($is16Bit & 0x01) === 0) {
1654 2
            $textStr = StringHelper::ConvertEncoding($textStr, 'UTF-8', 'ISO-8859-1');
1655
        } else {
1656
            $textStr = $this->decodeCodepage($textStr);
1657
        }
1658
1659 2
        $this->textObjects[$this->textObjRef] = [
1660 2
            'text' => $textStr,
1661 2
            'format' => substr($text['recordData'], $text['spliceOffsets'][1], $cbRuns),
1662 2
            'alignment' => $grbitOpts,
1663 2
            'rotation' => $rot,
1664 2
        ];
1665
    }
1666
1667
    /**
1668
     * Read BOF.
1669
     */
1670 105
    private function readBof(): void
1671
    {
1672 105
        $length = self::getUInt2d($this->data, $this->pos + 2);
1673 105
        $recordData = substr($this->data, $this->pos + 4, $length);
1674
1675
        // move stream pointer to next record
1676 105
        $this->pos += 4 + $length;
1677
1678
        // offset: 2; size: 2; type of the following data
1679 105
        $substreamType = self::getUInt2d($recordData, 2);
1680
1681
        switch ($substreamType) {
1682
            case self::XLS_WORKBOOKGLOBALS:
1683 105
                $version = self::getUInt2d($recordData, 0);
1684 105
                if (($version != self::XLS_BIFF8) && ($version != self::XLS_BIFF7)) {
1685
                    throw new Exception('Cannot read this Excel file. Version is too old.');
1686
                }
1687 105
                $this->version = $version;
1688
1689 105
                break;
1690
            case self::XLS_WORKSHEET:
1691
                // do not use this version information for anything
1692
                // it is unreliable (OpenOffice doc, 5.8), use only version information from the global stream
1693 99
                break;
1694
            default:
1695
                // substream, e.g. chart
1696
                // just skip the entire substream
1697
                do {
1698
                    $code = self::getUInt2d($this->data, $this->pos);
1699
                    $this->readDefault();
1700
                } while ($code != self::XLS_TYPE_EOF && $this->pos < $this->dataSize);
1701
1702
                break;
1703
        }
1704
    }
1705
1706
    /**
1707
     * FILEPASS.
1708
     *
1709
     * This record is part of the File Protection Block. It
1710
     * contains information about the read/write password of the
1711
     * file. All record contents following this record will be
1712
     * encrypted.
1713
     *
1714
     * --    "OpenOffice.org's Documentation of the Microsoft
1715
     *         Excel File Format"
1716
     *
1717
     * The decryption functions and objects used from here on in
1718
     * are based on the source of Spreadsheet-ParseExcel:
1719
     * https://metacpan.org/release/Spreadsheet-ParseExcel
1720
     */
1721
    private function readFilepass(): void
1722
    {
1723
        $length = self::getUInt2d($this->data, $this->pos + 2);
1724
1725
        if ($length != 54) {
1726
            throw new Exception('Unexpected file pass record length');
1727
        }
1728
1729
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
1730
1731
        // move stream pointer to next record
1732
        $this->pos += 4 + $length;
1733
1734
        if (!$this->verifyPassword('VelvetSweatshop', substr($recordData, 6, 16), substr($recordData, 22, 16), substr($recordData, 38, 16), $this->md5Ctxt)) {
1735
            throw new Exception('Decryption password incorrect');
1736
        }
1737
1738
        $this->encryption = self::MS_BIFF_CRYPTO_RC4;
1739
1740
        // Decryption required from the record after next onwards
1741
        $this->encryptionStartPos = $this->pos + self::getUInt2d($this->data, $this->pos + 2);
1742
    }
1743
1744
    /**
1745
     * Make an RC4 decryptor for the given block.
1746
     *
1747
     * @param int $block Block for which to create decrypto
1748
     * @param string $valContext MD5 context state
1749
     */
1750
    private function makeKey(int $block, string $valContext): Xls\RC4
1751
    {
1752
        $pwarray = str_repeat("\0", 64);
1753
1754
        for ($i = 0; $i < 5; ++$i) {
1755
            $pwarray[$i] = $valContext[$i];
1756
        }
1757
1758
        $pwarray[5] = chr($block & 0xFF);
1759
        $pwarray[6] = chr(($block >> 8) & 0xFF);
1760
        $pwarray[7] = chr(($block >> 16) & 0xFF);
1761
        $pwarray[8] = chr(($block >> 24) & 0xFF);
1762
1763
        $pwarray[9] = "\x80";
1764
        $pwarray[56] = "\x48";
1765
1766
        $md5 = new Xls\MD5();
1767
        $md5->add($pwarray);
1768
1769
        $s = $md5->getContext();
1770
1771
        return new Xls\RC4($s);
1772
    }
1773
1774
    /**
1775
     * Verify RC4 file password.
1776
     *
1777
     * @param string $password Password to check
1778
     * @param string $docid Document id
1779
     * @param string $salt_data Salt data
1780
     * @param string $hashedsalt_data Hashed salt data
1781
     * @param string $valContext Set to the MD5 context of the value
1782
     *
1783
     * @return bool Success
1784
     */
1785
    private function verifyPassword(string $password, string $docid, string $salt_data, string $hashedsalt_data, string &$valContext): bool
1786
    {
1787
        $pwarray = str_repeat("\0", 64);
1788
1789
        $iMax = strlen($password);
1790
        for ($i = 0; $i < $iMax; ++$i) {
1791
            $o = ord(substr($password, $i, 1));
1792
            $pwarray[2 * $i] = chr($o & 0xFF);
1793
            $pwarray[2 * $i + 1] = chr(($o >> 8) & 0xFF);
1794
        }
1795
        $pwarray[2 * $i] = chr(0x80);
1796
        $pwarray[56] = chr(($i << 4) & 0xFF);
1797
1798
        $md5 = new Xls\MD5();
1799
        $md5->add($pwarray);
1800
1801
        $mdContext1 = $md5->getContext();
1802
1803
        $offset = 0;
1804
        $keyoffset = 0;
1805
        $tocopy = 5;
1806
1807
        $md5->reset();
1808
1809
        while ($offset != 16) {
1810
            if ((64 - $offset) < 5) {
1811
                $tocopy = 64 - $offset;
1812
            }
1813
            for ($i = 0; $i <= $tocopy; ++$i) {
1814
                $pwarray[$offset + $i] = $mdContext1[$keyoffset + $i];
1815
            }
1816
            $offset += $tocopy;
1817
1818
            if ($offset == 64) {
1819
                $md5->add($pwarray);
1820
                $keyoffset = $tocopy;
1821
                $tocopy = 5 - $tocopy;
1822
                $offset = 0;
1823
1824
                continue;
1825
            }
1826
1827
            $keyoffset = 0;
1828
            $tocopy = 5;
1829
            for ($i = 0; $i < 16; ++$i) {
1830
                $pwarray[$offset + $i] = $docid[$i];
1831
            }
1832
            $offset += 16;
1833
        }
1834
1835
        $pwarray[16] = "\x80";
1836
        for ($i = 0; $i < 47; ++$i) {
1837
            $pwarray[17 + $i] = "\0";
1838
        }
1839
        $pwarray[56] = "\x80";
1840
        $pwarray[57] = "\x0a";
1841
1842
        $md5->add($pwarray);
1843
        $valContext = $md5->getContext();
1844
1845
        $key = $this->makeKey(0, $valContext);
1846
1847
        $salt = $key->RC4($salt_data);
1848
        $hashedsalt = $key->RC4($hashedsalt_data);
1849
1850
        $salt .= "\x80" . str_repeat("\0", 47);
1851
        $salt[56] = "\x80";
1852
1853
        $md5->reset();
1854
        $md5->add($salt);
1855
        $mdContext2 = $md5->getContext();
1856
1857
        return $mdContext2 == $hashedsalt;
1858
    }
1859
1860
    /**
1861
     * CODEPAGE.
1862
     *
1863
     * This record stores the text encoding used to write byte
1864
     * strings, stored as MS Windows code page identifier.
1865
     *
1866
     * --    "OpenOffice.org's Documentation of the Microsoft
1867
     *         Excel File Format"
1868
     */
1869 102
    private function readCodepage(): void
1870
    {
1871 102
        $length = self::getUInt2d($this->data, $this->pos + 2);
1872 102
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
1873
1874
        // move stream pointer to next record
1875 102
        $this->pos += 4 + $length;
1876
1877
        // offset: 0; size: 2; code page identifier
1878 102
        $codepage = self::getUInt2d($recordData, 0);
1879
1880 102
        $this->codepage = CodePage::numberToName($codepage);
1881
    }
1882
1883
    /**
1884
     * DATEMODE.
1885
     *
1886
     * This record specifies the base date for displaying date
1887
     * values. All dates are stored as count of days past this
1888
     * base date. In BIFF2-BIFF4 this record is part of the
1889
     * Calculation Settings Block. In BIFF5-BIFF8 it is
1890
     * stored in the Workbook Globals Substream.
1891
     *
1892
     * --    "OpenOffice.org's Documentation of the Microsoft
1893
     *         Excel File Format"
1894
     */
1895 95
    private function readDateMode(): void
1896
    {
1897 95
        $length = self::getUInt2d($this->data, $this->pos + 2);
1898 95
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
1899
1900
        // move stream pointer to next record
1901 95
        $this->pos += 4 + $length;
1902
1903
        // offset: 0; size: 2; 0 = base 1900, 1 = base 1904
1904 95
        Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900);
1905 95
        if (ord($recordData[0]) == 1) {
1906
            Date::setExcelCalendar(Date::CALENDAR_MAC_1904);
1907
        }
1908
    }
1909
1910
    /**
1911
     * Read a FONT record.
1912
     */
1913 95
    private function readFont(): void
1914
    {
1915 95
        $length = self::getUInt2d($this->data, $this->pos + 2);
1916 95
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
1917
1918
        // move stream pointer to next record
1919 95
        $this->pos += 4 + $length;
1920
1921 95
        if (!$this->readDataOnly) {
1922 94
            $objFont = new Font();
1923
1924
            // offset: 0; size: 2; height of the font (in twips = 1/20 of a point)
1925 94
            $size = self::getUInt2d($recordData, 0);
1926 94
            $objFont->setSize($size / 20);
1927
1928
            // offset: 2; size: 2; option flags
1929
            // bit: 0; mask 0x0001; bold (redundant in BIFF5-BIFF8)
1930
            // bit: 1; mask 0x0002; italic
1931 94
            $isItalic = (0x0002 & self::getUInt2d($recordData, 2)) >> 1;
1932 94
            if ($isItalic) {
1933 43
                $objFont->setItalic(true);
1934
            }
1935
1936
            // bit: 2; mask 0x0004; underlined (redundant in BIFF5-BIFF8)
1937
            // bit: 3; mask 0x0008; strikethrough
1938 94
            $isStrike = (0x0008 & self::getUInt2d($recordData, 2)) >> 3;
1939 94
            if ($isStrike) {
1940
                $objFont->setStrikethrough(true);
1941
            }
1942
1943
            // offset: 4; size: 2; colour index
1944 94
            $colorIndex = self::getUInt2d($recordData, 4);
1945 94
            $objFont->colorIndex = $colorIndex;
1946
1947
            // offset: 6; size: 2; font weight
1948 94
            $weight = self::getUInt2d($recordData, 6);
1949
            switch ($weight) {
1950 94
                case 0x02BC:
1951 54
                    $objFont->setBold(true);
1952
1953 54
                    break;
1954
            }
1955
1956
            // offset: 8; size: 2; escapement type
1957 94
            $escapement = self::getUInt2d($recordData, 8);
1958 94
            CellFont::escapement($objFont, $escapement);
1959
1960
            // offset: 10; size: 1; underline type
1961 94
            $underlineType = ord($recordData[10]);
1962 94
            CellFont::underline($objFont, $underlineType);
1963
1964
            // offset: 11; size: 1; font family
1965
            // offset: 12; size: 1; character set
1966
            // offset: 13; size: 1; not used
1967
            // offset: 14; size: var; font name
1968 94
            if ($this->version == self::XLS_BIFF8) {
1969 92
                $string = self::readUnicodeStringShort(substr($recordData, 14));
1970
            } else {
1971 2
                $string = $this->readByteStringShort(substr($recordData, 14));
1972
            }
1973 94
            $objFont->setName($string['value']);
1974
1975 94
            $this->objFonts[] = $objFont;
1976
        }
1977
    }
1978
1979
    /**
1980
     * FORMAT.
1981
     *
1982
     * This record contains information about a number format.
1983
     * All FORMAT records occur together in a sequential list.
1984
     *
1985
     * In BIFF2-BIFF4 other records referencing a FORMAT record
1986
     * contain a zero-based index into this list. From BIFF5 on
1987
     * the FORMAT record contains the index itself that will be
1988
     * used by other records.
1989
     *
1990
     * --    "OpenOffice.org's Documentation of the Microsoft
1991
     *         Excel File Format"
1992
     */
1993 53
    private function readFormat(): void
1994
    {
1995 53
        $length = self::getUInt2d($this->data, $this->pos + 2);
1996 53
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
1997
1998
        // move stream pointer to next record
1999 53
        $this->pos += 4 + $length;
2000
2001 53
        if (!$this->readDataOnly) {
2002 52
            $indexCode = self::getUInt2d($recordData, 0);
2003
2004 52
            if ($this->version == self::XLS_BIFF8) {
2005 50
                $string = self::readUnicodeStringLong(substr($recordData, 2));
2006
            } else {
2007
                // BIFF7
2008 2
                $string = $this->readByteStringShort(substr($recordData, 2));
2009
            }
2010
2011 52
            $formatString = $string['value'];
2012
            // Apache Open Office sets wrong case writing to xls - issue 2239
2013 52
            if ($formatString === 'GENERAL') {
2014 1
                $formatString = NumberFormat::FORMAT_GENERAL;
2015
            }
2016 52
            $this->formats[$indexCode] = $formatString;
2017
        }
2018
    }
2019
2020
    /**
2021
     * XF - Extended Format.
2022
     *
2023
     * This record contains formatting information for cells, rows, columns or styles.
2024
     * According to https://support.microsoft.com/en-us/help/147732 there are always at least 15 cell style XF
2025
     * and 1 cell XF.
2026
     * Inspection of Excel files generated by MS Office Excel shows that XF records 0-14 are cell style XF
2027
     * and XF record 15 is a cell XF
2028
     * We only read the first cell style XF and skip the remaining cell style XF records
2029
     * We read all cell XF records.
2030
     *
2031
     * --    "OpenOffice.org's Documentation of the Microsoft
2032
     *         Excel File Format"
2033
     */
2034 96
    private function readXf(): void
2035
    {
2036 96
        $length = self::getUInt2d($this->data, $this->pos + 2);
2037 96
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2038
2039
        // move stream pointer to next record
2040 96
        $this->pos += 4 + $length;
2041
2042 96
        $objStyle = new Style();
2043
2044 96
        if (!$this->readDataOnly) {
2045
            // offset:  0; size: 2; Index to FONT record
2046 95
            if (self::getUInt2d($recordData, 0) < 4) {
2047 95
                $fontIndex = self::getUInt2d($recordData, 0);
2048
            } else {
2049
                // this has to do with that index 4 is omitted in all BIFF versions for some strange reason
2050
                // check the OpenOffice documentation of the FONT record
2051 50
                $fontIndex = self::getUInt2d($recordData, 0) - 1;
2052
            }
2053 95
            if (isset($this->objFonts[$fontIndex])) {
2054 94
                $objStyle->setFont($this->objFonts[$fontIndex]);
2055
            }
2056
2057
            // offset:  2; size: 2; Index to FORMAT record
2058 95
            $numberFormatIndex = self::getUInt2d($recordData, 2);
2059 95
            if (isset($this->formats[$numberFormatIndex])) {
2060
                // then we have user-defined format code
2061 47
                $numberFormat = ['formatCode' => $this->formats[$numberFormatIndex]];
2062 95
            } elseif (($code = NumberFormat::builtInFormatCode($numberFormatIndex)) !== '') {
2063
                // then we have built-in format code
2064 95
                $numberFormat = ['formatCode' => $code];
2065
            } else {
2066
                // we set the general format code
2067 4
                $numberFormat = ['formatCode' => NumberFormat::FORMAT_GENERAL];
2068
            }
2069 95
            $objStyle->getNumberFormat()->setFormatCode($numberFormat['formatCode']);
2070
2071
            // offset:  4; size: 2; XF type, cell protection, and parent style XF
2072
            // bit 2-0; mask 0x0007; XF_TYPE_PROT
2073 95
            $xfTypeProt = self::getUInt2d($recordData, 4);
2074
            // bit 0; mask 0x01; 1 = cell is locked
2075 95
            $isLocked = (0x01 & $xfTypeProt) >> 0;
2076 95
            $objStyle->getProtection()->setLocked($isLocked ? Protection::PROTECTION_INHERIT : Protection::PROTECTION_UNPROTECTED);
2077
2078
            // bit 1; mask 0x02; 1 = Formula is hidden
2079 95
            $isHidden = (0x02 & $xfTypeProt) >> 1;
2080 95
            $objStyle->getProtection()->setHidden($isHidden ? Protection::PROTECTION_PROTECTED : Protection::PROTECTION_UNPROTECTED);
2081
2082
            // bit 2; mask 0x04; 0 = Cell XF, 1 = Cell Style XF
2083 95
            $isCellStyleXf = (0x04 & $xfTypeProt) >> 2;
2084
2085
            // offset:  6; size: 1; Alignment and text break
2086
            // bit 2-0, mask 0x07; horizontal alignment
2087 95
            $horAlign = (0x07 & ord($recordData[6])) >> 0;
2088 95
            Xls\Style\CellAlignment::horizontal($objStyle->getAlignment(), $horAlign);
2089
2090
            // bit 3, mask 0x08; wrap text
2091 95
            $wrapText = (0x08 & ord($recordData[6])) >> 3;
2092 95
            Xls\Style\CellAlignment::wrap($objStyle->getAlignment(), $wrapText);
2093
2094
            // bit 6-4, mask 0x70; vertical alignment
2095 95
            $vertAlign = (0x70 & ord($recordData[6])) >> 4;
2096 95
            Xls\Style\CellAlignment::vertical($objStyle->getAlignment(), $vertAlign);
2097
2098 95
            if ($this->version == self::XLS_BIFF8) {
2099
                // offset:  7; size: 1; XF_ROTATION: Text rotation angle
2100 93
                $angle = ord($recordData[7]);
2101 93
                $rotation = 0;
2102 93
                if ($angle <= 90) {
2103 93
                    $rotation = $angle;
2104 2
                } elseif ($angle <= 180) {
2105
                    $rotation = 90 - $angle;
2106 2
                } elseif ($angle == Alignment::TEXTROTATION_STACK_EXCEL) {
2107 2
                    $rotation = Alignment::TEXTROTATION_STACK_PHPSPREADSHEET;
2108
                }
2109 93
                $objStyle->getAlignment()->setTextRotation($rotation);
2110
2111
                // offset:  8; size: 1; Indentation, shrink to cell size, and text direction
2112
                // bit: 3-0; mask: 0x0F; indent level
2113 93
                $indent = (0x0F & ord($recordData[8])) >> 0;
2114 93
                $objStyle->getAlignment()->setIndent($indent);
2115
2116
                // bit: 4; mask: 0x10; 1 = shrink content to fit into cell
2117 93
                $shrinkToFit = (0x10 & ord($recordData[8])) >> 4;
2118
                switch ($shrinkToFit) {
2119 93
                    case 0:
2120 93
                        $objStyle->getAlignment()->setShrinkToFit(false);
2121
2122 93
                        break;
2123 1
                    case 1:
2124 1
                        $objStyle->getAlignment()->setShrinkToFit(true);
2125
2126 1
                        break;
2127
                }
2128
2129
                // offset:  9; size: 1; Flags used for attribute groups
2130
2131
                // offset: 10; size: 4; Cell border lines and background area
2132
                // bit: 3-0; mask: 0x0000000F; left style
2133 93
                if ($bordersLeftStyle = Xls\Style\Border::lookup((0x0000000F & self::getInt4d($recordData, 10)) >> 0)) {
2134 93
                    $objStyle->getBorders()->getLeft()->setBorderStyle($bordersLeftStyle);
2135
                }
2136
                // bit: 7-4; mask: 0x000000F0; right style
2137 93
                if ($bordersRightStyle = Xls\Style\Border::lookup((0x000000F0 & self::getInt4d($recordData, 10)) >> 4)) {
2138 93
                    $objStyle->getBorders()->getRight()->setBorderStyle($bordersRightStyle);
2139
                }
2140
                // bit: 11-8; mask: 0x00000F00; top style
2141 93
                if ($bordersTopStyle = Xls\Style\Border::lookup((0x00000F00 & self::getInt4d($recordData, 10)) >> 8)) {
2142 93
                    $objStyle->getBorders()->getTop()->setBorderStyle($bordersTopStyle);
2143
                }
2144
                // bit: 15-12; mask: 0x0000F000; bottom style
2145 93
                if ($bordersBottomStyle = Xls\Style\Border::lookup((0x0000F000 & self::getInt4d($recordData, 10)) >> 12)) {
2146 93
                    $objStyle->getBorders()->getBottom()->setBorderStyle($bordersBottomStyle);
2147
                }
2148
                // bit: 22-16; mask: 0x007F0000; left color
2149 93
                $objStyle->getBorders()->getLeft()->colorIndex = (0x007F0000 & self::getInt4d($recordData, 10)) >> 16;
2150
2151
                // bit: 29-23; mask: 0x3F800000; right color
2152 93
                $objStyle->getBorders()->getRight()->colorIndex = (0x3F800000 & self::getInt4d($recordData, 10)) >> 23;
2153
2154
                // bit: 30; mask: 0x40000000; 1 = diagonal line from top left to right bottom
2155 93
                $diagonalDown = (0x40000000 & self::getInt4d($recordData, 10)) >> 30 ? true : false;
2156
2157
                // bit: 31; mask: 0x800000; 1 = diagonal line from bottom left to top right
2158 93
                $diagonalUp = (self::HIGH_ORDER_BIT & self::getInt4d($recordData, 10)) >> 31 ? true : false;
2159
2160 93
                if ($diagonalUp === false) {
2161 93
                    if ($diagonalDown === false) {
2162 93
                        $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_NONE);
2163
                    } else {
2164 93
                        $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_DOWN);
2165
                    }
2166 1
                } elseif ($diagonalDown === false) {
2167 1
                    $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_UP);
2168
                } else {
2169 1
                    $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_BOTH);
2170
                }
2171
2172
                // offset: 14; size: 4;
2173
                // bit: 6-0; mask: 0x0000007F; top color
2174 93
                $objStyle->getBorders()->getTop()->colorIndex = (0x0000007F & self::getInt4d($recordData, 14)) >> 0;
2175
2176
                // bit: 13-7; mask: 0x00003F80; bottom color
2177 93
                $objStyle->getBorders()->getBottom()->colorIndex = (0x00003F80 & self::getInt4d($recordData, 14)) >> 7;
2178
2179
                // bit: 20-14; mask: 0x001FC000; diagonal color
2180 93
                $objStyle->getBorders()->getDiagonal()->colorIndex = (0x001FC000 & self::getInt4d($recordData, 14)) >> 14;
2181
2182
                // bit: 24-21; mask: 0x01E00000; diagonal style
2183 93
                if ($bordersDiagonalStyle = Xls\Style\Border::lookup((0x01E00000 & self::getInt4d($recordData, 14)) >> 21)) {
2184 93
                    $objStyle->getBorders()->getDiagonal()->setBorderStyle($bordersDiagonalStyle);
2185
                }
2186
2187
                // bit: 31-26; mask: 0xFC000000 fill pattern
2188 93
                if ($fillType = Xls\Style\FillPattern::lookup((self::FC000000 & self::getInt4d($recordData, 14)) >> 26)) {
2189 93
                    $objStyle->getFill()->setFillType($fillType);
2190
                }
2191
                // offset: 18; size: 2; pattern and background colour
2192
                // bit: 6-0; mask: 0x007F; color index for pattern color
2193 93
                $objStyle->getFill()->startcolorIndex = (0x007F & self::getUInt2d($recordData, 18)) >> 0;
2194
2195
                // bit: 13-7; mask: 0x3F80; color index for pattern background
2196 93
                $objStyle->getFill()->endcolorIndex = (0x3F80 & self::getUInt2d($recordData, 18)) >> 7;
2197
            } else {
2198
                // BIFF5
2199
2200
                // offset: 7; size: 1; Text orientation and flags
2201 2
                $orientationAndFlags = ord($recordData[7]);
2202
2203
                // bit: 1-0; mask: 0x03; XF_ORIENTATION: Text orientation
2204 2
                $xfOrientation = (0x03 & $orientationAndFlags) >> 0;
2205
                switch ($xfOrientation) {
2206 2
                    case 0:
2207 2
                        $objStyle->getAlignment()->setTextRotation(0);
2208
2209 2
                        break;
2210 1
                    case 1:
2211 1
                        $objStyle->getAlignment()->setTextRotation(Alignment::TEXTROTATION_STACK_PHPSPREADSHEET);
2212
2213 1
                        break;
2214
                    case 2:
2215
                        $objStyle->getAlignment()->setTextRotation(90);
2216
2217
                        break;
2218
                    case 3:
2219
                        $objStyle->getAlignment()->setTextRotation(-90);
2220
2221
                        break;
2222
                }
2223
2224
                // offset: 8; size: 4; cell border lines and background area
2225 2
                $borderAndBackground = self::getInt4d($recordData, 8);
2226
2227
                // bit: 6-0; mask: 0x0000007F; color index for pattern color
2228 2
                $objStyle->getFill()->startcolorIndex = (0x0000007F & $borderAndBackground) >> 0;
2229
2230
                // bit: 13-7; mask: 0x00003F80; color index for pattern background
2231 2
                $objStyle->getFill()->endcolorIndex = (0x00003F80 & $borderAndBackground) >> 7;
2232
2233
                // bit: 21-16; mask: 0x003F0000; fill pattern
2234 2
                $objStyle->getFill()->setFillType(Xls\Style\FillPattern::lookup((0x003F0000 & $borderAndBackground) >> 16));
2235
2236
                // bit: 24-22; mask: 0x01C00000; bottom line style
2237 2
                $objStyle->getBorders()->getBottom()->setBorderStyle(Xls\Style\Border::lookup((0x01C00000 & $borderAndBackground) >> 22));
2238
2239
                // bit: 31-25; mask: 0xFE000000; bottom line color
2240 2
                $objStyle->getBorders()->getBottom()->colorIndex = (self::FE000000 & $borderAndBackground) >> 25;
2241
2242
                // offset: 12; size: 4; cell border lines
2243 2
                $borderLines = self::getInt4d($recordData, 12);
2244
2245
                // bit: 2-0; mask: 0x00000007; top line style
2246 2
                $objStyle->getBorders()->getTop()->setBorderStyle(Xls\Style\Border::lookup((0x00000007 & $borderLines) >> 0));
2247
2248
                // bit: 5-3; mask: 0x00000038; left line style
2249 2
                $objStyle->getBorders()->getLeft()->setBorderStyle(Xls\Style\Border::lookup((0x00000038 & $borderLines) >> 3));
2250
2251
                // bit: 8-6; mask: 0x000001C0; right line style
2252 2
                $objStyle->getBorders()->getRight()->setBorderStyle(Xls\Style\Border::lookup((0x000001C0 & $borderLines) >> 6));
2253
2254
                // bit: 15-9; mask: 0x0000FE00; top line color index
2255 2
                $objStyle->getBorders()->getTop()->colorIndex = (0x0000FE00 & $borderLines) >> 9;
2256
2257
                // bit: 22-16; mask: 0x007F0000; left line color index
2258 2
                $objStyle->getBorders()->getLeft()->colorIndex = (0x007F0000 & $borderLines) >> 16;
2259
2260
                // bit: 29-23; mask: 0x3F800000; right line color index
2261 2
                $objStyle->getBorders()->getRight()->colorIndex = (0x3F800000 & $borderLines) >> 23;
2262
            }
2263
2264
            // add cellStyleXf or cellXf and update mapping
2265 95
            if ($isCellStyleXf) {
2266
                // we only read one style XF record which is always the first
2267 95
                if ($this->xfIndex == 0) {
2268 95
                    $this->spreadsheet->addCellStyleXf($objStyle);
2269 95
                    $this->mapCellStyleXfIndex[$this->xfIndex] = 0;
2270
                }
2271
            } else {
2272
                // we read all cell XF records
2273 95
                $this->spreadsheet->addCellXf($objStyle);
2274 95
                $this->mapCellXfIndex[$this->xfIndex] = count($this->spreadsheet->getCellXfCollection()) - 1;
2275
            }
2276
2277
            // update XF index for when we read next record
2278 95
            ++$this->xfIndex;
2279
        }
2280
    }
2281
2282 40
    private function readXfExt(): void
2283
    {
2284 40
        $length = self::getUInt2d($this->data, $this->pos + 2);
2285 40
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2286
2287
        // move stream pointer to next record
2288 40
        $this->pos += 4 + $length;
2289
2290 40
        if (!$this->readDataOnly) {
2291
            // offset: 0; size: 2; 0x087D = repeated header
2292
2293
            // offset: 2; size: 2
2294
2295
            // offset: 4; size: 8; not used
2296
2297
            // offset: 12; size: 2; record version
2298
2299
            // offset: 14; size: 2; index to XF record which this record modifies
2300 39
            $ixfe = self::getUInt2d($recordData, 14);
2301
2302
            // offset: 16; size: 2; not used
2303
2304
            // offset: 18; size: 2; number of extension properties that follow
2305
            //$cexts = self::getUInt2d($recordData, 18);
2306
2307
            // start reading the actual extension data
2308 39
            $offset = 20;
2309 39
            while ($offset < $length) {
2310
                // extension type
2311 39
                $extType = self::getUInt2d($recordData, $offset);
2312
2313
                // extension length
2314 39
                $cb = self::getUInt2d($recordData, $offset + 2);
2315
2316
                // extension data
2317 39
                $extData = substr($recordData, $offset + 4, $cb);
2318
2319
                switch ($extType) {
2320 39
                    case 4:        // fill start color
2321 39
                        $xclfType = self::getUInt2d($extData, 0); // color type
2322 39
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2323
2324 39
                        if ($xclfType == 2) {
2325 37
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2326
2327
                            // modify the relevant style property
2328 37
                            if (isset($this->mapCellXfIndex[$ixfe])) {
2329 5
                                $fill = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getFill();
2330 5
                                $fill->getStartColor()->setRGB($rgb);
2331 5
                                $fill->startcolorIndex = null; // normal color index does not apply, discard
2332
                            }
2333
                        }
2334
2335 39
                        break;
2336 37
                    case 5:        // fill end color
2337 3
                        $xclfType = self::getUInt2d($extData, 0); // color type
2338 3
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2339
2340 3
                        if ($xclfType == 2) {
2341 3
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2342
2343
                            // modify the relevant style property
2344 3
                            if (isset($this->mapCellXfIndex[$ixfe])) {
2345 3
                                $fill = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getFill();
2346 3
                                $fill->getEndColor()->setRGB($rgb);
2347 3
                                $fill->endcolorIndex = null; // normal color index does not apply, discard
2348
                            }
2349
                        }
2350
2351 3
                        break;
2352 37
                    case 7:        // border color top
2353 37
                        $xclfType = self::getUInt2d($extData, 0); // color type
2354 37
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2355
2356 37
                        if ($xclfType == 2) {
2357 37
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2358
2359
                            // modify the relevant style property
2360 37
                            if (isset($this->mapCellXfIndex[$ixfe])) {
2361 2
                                $top = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getTop();
2362 2
                                $top->getColor()->setRGB($rgb);
2363 2
                                $top->colorIndex = null; // normal color index does not apply, discard
2364
                            }
2365
                        }
2366
2367 37
                        break;
2368 37
                    case 8:        // border color bottom
2369 37
                        $xclfType = self::getUInt2d($extData, 0); // color type
2370 37
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2371
2372 37
                        if ($xclfType == 2) {
2373 37
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2374
2375
                            // modify the relevant style property
2376 37
                            if (isset($this->mapCellXfIndex[$ixfe])) {
2377 3
                                $bottom = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getBottom();
2378 3
                                $bottom->getColor()->setRGB($rgb);
2379 3
                                $bottom->colorIndex = null; // normal color index does not apply, discard
2380
                            }
2381
                        }
2382
2383 37
                        break;
2384 37
                    case 9:        // border color left
2385 37
                        $xclfType = self::getUInt2d($extData, 0); // color type
2386 37
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2387
2388 37
                        if ($xclfType == 2) {
2389 37
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2390
2391
                            // modify the relevant style property
2392 37
                            if (isset($this->mapCellXfIndex[$ixfe])) {
2393 2
                                $left = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getLeft();
2394 2
                                $left->getColor()->setRGB($rgb);
2395 2
                                $left->colorIndex = null; // normal color index does not apply, discard
2396
                            }
2397
                        }
2398
2399 37
                        break;
2400 37
                    case 10:        // border color right
2401 37
                        $xclfType = self::getUInt2d($extData, 0); // color type
2402 37
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2403
2404 37
                        if ($xclfType == 2) {
2405 37
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2406
2407
                            // modify the relevant style property
2408 37
                            if (isset($this->mapCellXfIndex[$ixfe])) {
2409 2
                                $right = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getRight();
2410 2
                                $right->getColor()->setRGB($rgb);
2411 2
                                $right->colorIndex = null; // normal color index does not apply, discard
2412
                            }
2413
                        }
2414
2415 37
                        break;
2416 37
                    case 11:        // border color diagonal
2417
                        $xclfType = self::getUInt2d($extData, 0); // color type
2418
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2419
2420
                        if ($xclfType == 2) {
2421
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2422
2423
                            // modify the relevant style property
2424
                            if (isset($this->mapCellXfIndex[$ixfe])) {
2425
                                $diagonal = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getDiagonal();
2426
                                $diagonal->getColor()->setRGB($rgb);
2427
                                $diagonal->colorIndex = null; // normal color index does not apply, discard
2428
                            }
2429
                        }
2430
2431
                        break;
2432 37
                    case 13:    // font color
2433 37
                        $xclfType = self::getUInt2d($extData, 0); // color type
2434 37
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2435
2436 37
                        if ($xclfType == 2) {
2437 37
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2438
2439
                            // modify the relevant style property
2440 37
                            if (isset($this->mapCellXfIndex[$ixfe])) {
2441 7
                                $font = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getFont();
2442 7
                                $font->getColor()->setRGB($rgb);
2443 7
                                $font->colorIndex = null; // normal color index does not apply, discard
2444
                            }
2445
                        }
2446
2447 37
                        break;
2448
                }
2449
2450 39
                $offset += $cb;
2451
            }
2452
        }
2453
    }
2454
2455
    /**
2456
     * Read STYLE record.
2457
     */
2458 96
    private function readStyle(): void
2459
    {
2460 96
        $length = self::getUInt2d($this->data, $this->pos + 2);
2461 96
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2462
2463
        // move stream pointer to next record
2464 96
        $this->pos += 4 + $length;
2465
2466 96
        if (!$this->readDataOnly) {
2467
            // offset: 0; size: 2; index to XF record and flag for built-in style
2468 95
            $ixfe = self::getUInt2d($recordData, 0);
2469
2470
            // bit: 11-0; mask 0x0FFF; index to XF record
2471
            //$xfIndex = (0x0FFF & $ixfe) >> 0;
2472
2473
            // bit: 15; mask 0x8000; 0 = user-defined style, 1 = built-in style
2474 95
            $isBuiltIn = (bool) ((0x8000 & $ixfe) >> 15);
2475
2476 95
            if ($isBuiltIn) {
2477
                // offset: 2; size: 1; identifier for built-in style
2478 95
                $builtInId = ord($recordData[2]);
2479
2480
                switch ($builtInId) {
2481 95
                    case 0x00:
2482
                        // currently, we are not using this for anything
2483 95
                        break;
2484
                    default:
2485 46
                        break;
2486
                }
2487
            }
2488
            // user-defined; not supported by PhpSpreadsheet
2489
        }
2490
    }
2491
2492
    /**
2493
     * Read PALETTE record.
2494
     */
2495 64
    private function readPalette(): void
2496
    {
2497 64
        $length = self::getUInt2d($this->data, $this->pos + 2);
2498 64
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2499
2500
        // move stream pointer to next record
2501 64
        $this->pos += 4 + $length;
2502
2503 64
        if (!$this->readDataOnly) {
2504
            // offset: 0; size: 2; number of following colors
2505 64
            $nm = self::getUInt2d($recordData, 0);
2506
2507
            // list of RGB colors
2508 64
            for ($i = 0; $i < $nm; ++$i) {
2509 64
                $rgb = substr($recordData, 2 + 4 * $i, 4);
2510 64
                $this->palette[] = self::readRGB($rgb);
2511
            }
2512
        }
2513
    }
2514
2515
    /**
2516
     * SHEET.
2517
     *
2518
     * This record is  located in the  Workbook Globals
2519
     * Substream  and represents a sheet inside the workbook.
2520
     * One SHEET record is written for each sheet. It stores the
2521
     * sheet name and a stream offset to the BOF record of the
2522
     * respective Sheet Substream within the Workbook Stream.
2523
     *
2524
     * --    "OpenOffice.org's Documentation of the Microsoft
2525
     *         Excel File Format"
2526
     */
2527 105
    private function readSheet(): void
2528
    {
2529 105
        $length = self::getUInt2d($this->data, $this->pos + 2);
2530 105
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2531
2532
        // offset: 0; size: 4; absolute stream position of the BOF record of the sheet
2533
        // NOTE: not encrypted
2534 105
        $rec_offset = self::getInt4d($this->data, $this->pos + 4);
2535
2536
        // move stream pointer to next record
2537 105
        $this->pos += 4 + $length;
2538
2539
        // offset: 4; size: 1; sheet state
2540 105
        $sheetState = match (ord($recordData[4])) {
2541 105
            0x00 => Worksheet::SHEETSTATE_VISIBLE,
2542 105
            0x01 => Worksheet::SHEETSTATE_HIDDEN,
2543 105
            0x02 => Worksheet::SHEETSTATE_VERYHIDDEN,
2544 105
            default => Worksheet::SHEETSTATE_VISIBLE,
2545 105
        };
2546
2547
        // offset: 5; size: 1; sheet type
2548 105
        $sheetType = ord($recordData[5]);
2549
2550
        // offset: 6; size: var; sheet name
2551 105
        $rec_name = null;
2552 105
        if ($this->version == self::XLS_BIFF8) {
2553 99
            $string = self::readUnicodeStringShort(substr($recordData, 6));
2554 99
            $rec_name = $string['value'];
2555 6
        } elseif ($this->version == self::XLS_BIFF7) {
2556 6
            $string = $this->readByteStringShort(substr($recordData, 6));
2557 6
            $rec_name = $string['value'];
2558
        }
2559
2560 105
        $this->sheets[] = [
2561 105
            'name' => $rec_name,
2562 105
            'offset' => $rec_offset,
2563 105
            'sheetState' => $sheetState,
2564 105
            'sheetType' => $sheetType,
2565 105
        ];
2566
    }
2567
2568
    /**
2569
     * Read EXTERNALBOOK record.
2570
     */
2571 74
    private function readExternalBook(): void
2572
    {
2573 74
        $length = self::getUInt2d($this->data, $this->pos + 2);
2574 74
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2575
2576
        // move stream pointer to next record
2577 74
        $this->pos += 4 + $length;
2578
2579
        // offset within record data
2580 74
        $offset = 0;
2581
2582
        // there are 4 types of records
2583 74
        if (strlen($recordData) > 4) {
2584
            // external reference
2585
            // offset: 0; size: 2; number of sheet names ($nm)
2586
            $nm = self::getUInt2d($recordData, 0);
2587
            $offset += 2;
2588
2589
            // offset: 2; size: var; encoded URL without sheet name (Unicode string, 16-bit length)
2590
            $encodedUrlString = self::readUnicodeStringLong(substr($recordData, 2));
2591
            $offset += $encodedUrlString['size'];
2592
2593
            // offset: var; size: var; list of $nm sheet names (Unicode strings, 16-bit length)
2594
            $externalSheetNames = [];
2595
            for ($i = 0; $i < $nm; ++$i) {
2596
                $externalSheetNameString = self::readUnicodeStringLong(substr($recordData, $offset));
2597
                $externalSheetNames[] = $externalSheetNameString['value'];
2598
                $offset += $externalSheetNameString['size'];
2599
            }
2600
2601
            // store the record data
2602
            $this->externalBooks[] = [
2603
                'type' => 'external',
2604
                'encodedUrl' => $encodedUrlString['value'],
2605
                'externalSheetNames' => $externalSheetNames,
2606
            ];
2607 74
        } elseif (substr($recordData, 2, 2) == pack('CC', 0x01, 0x04)) {
2608
            // internal reference
2609
            // offset: 0; size: 2; number of sheet in this document
2610
            // offset: 2; size: 2; 0x01 0x04
2611 74
            $this->externalBooks[] = [
2612 74
                'type' => 'internal',
2613 74
            ];
2614
        } elseif (substr($recordData, 0, 4) == pack('vCC', 0x0001, 0x01, 0x3A)) {
2615
            // add-in function
2616
            // offset: 0; size: 2; 0x0001
2617
            $this->externalBooks[] = [
2618
                'type' => 'addInFunction',
2619
            ];
2620
        } elseif (substr($recordData, 0, 2) == pack('v', 0x0000)) {
2621
            // DDE links, OLE links
2622
            // offset: 0; size: 2; 0x0000
2623
            // offset: 2; size: var; encoded source document name
2624
            $this->externalBooks[] = [
2625
                'type' => 'DDEorOLE',
2626
            ];
2627
        }
2628
    }
2629
2630
    /**
2631
     * Read EXTERNNAME record.
2632
     */
2633
    private function readExternName(): void
2634
    {
2635
        $length = self::getUInt2d($this->data, $this->pos + 2);
2636
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2637
2638
        // move stream pointer to next record
2639
        $this->pos += 4 + $length;
2640
2641
        // external sheet references provided for named cells
2642
        if ($this->version == self::XLS_BIFF8) {
2643
            // offset: 0; size: 2; options
2644
            //$options = self::getUInt2d($recordData, 0);
2645
2646
            // offset: 2; size: 2;
2647
2648
            // offset: 4; size: 2; not used
2649
2650
            // offset: 6; size: var
2651
            $nameString = self::readUnicodeStringShort(substr($recordData, 6));
2652
2653
            // offset: var; size: var; formula data
2654
            $offset = 6 + $nameString['size'];
2655
            $formula = $this->getFormulaFromStructure(substr($recordData, $offset));
2656
2657
            $this->externalNames[] = [
2658
                'name' => $nameString['value'],
2659
                'formula' => $formula,
2660
            ];
2661
        }
2662
    }
2663
2664
    /**
2665
     * Read EXTERNSHEET record.
2666
     */
2667 75
    private function readExternSheet(): void
2668
    {
2669 75
        $length = self::getUInt2d($this->data, $this->pos + 2);
2670 75
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2671
2672
        // move stream pointer to next record
2673 75
        $this->pos += 4 + $length;
2674
2675
        // external sheet references provided for named cells
2676 75
        if ($this->version == self::XLS_BIFF8) {
2677
            // offset: 0; size: 2; number of following ref structures
2678 74
            $nm = self::getUInt2d($recordData, 0);
2679 74
            for ($i = 0; $i < $nm; ++$i) {
2680 72
                $this->ref[] = [
2681
                    // offset: 2 + 6 * $i; index to EXTERNALBOOK record
2682 72
                    'externalBookIndex' => self::getUInt2d($recordData, 2 + 6 * $i),
2683
                    // offset: 4 + 6 * $i; index to first sheet in EXTERNALBOOK record
2684 72
                    'firstSheetIndex' => self::getUInt2d($recordData, 4 + 6 * $i),
2685
                    // offset: 6 + 6 * $i; index to last sheet in EXTERNALBOOK record
2686 72
                    'lastSheetIndex' => self::getUInt2d($recordData, 6 + 6 * $i),
2687 72
                ];
2688
            }
2689
        }
2690
    }
2691
2692
    /**
2693
     * DEFINEDNAME.
2694
     *
2695
     * This record is part of a Link Table. It contains the name
2696
     * and the token array of an internal defined name. Token
2697
     * arrays of defined names contain tokens with aberrant
2698
     * token classes.
2699
     *
2700
     * --    "OpenOffice.org's Documentation of the Microsoft
2701
     *         Excel File Format"
2702
     */
2703 16
    private function readDefinedName(): void
2704
    {
2705 16
        $length = self::getUInt2d($this->data, $this->pos + 2);
2706 16
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2707
2708
        // move stream pointer to next record
2709 16
        $this->pos += 4 + $length;
2710
2711 16
        if ($this->version == self::XLS_BIFF8) {
2712
            // retrieves named cells
2713
2714
            // offset: 0; size: 2; option flags
2715 15
            $opts = self::getUInt2d($recordData, 0);
2716
2717
            // bit: 5; mask: 0x0020; 0 = user-defined name, 1 = built-in-name
2718 15
            $isBuiltInName = (0x0020 & $opts) >> 5;
2719
2720
            // offset: 2; size: 1; keyboard shortcut
2721
2722
            // offset: 3; size: 1; length of the name (character count)
2723 15
            $nlen = ord($recordData[3]);
2724
2725
            // offset: 4; size: 2; size of the formula data (it can happen that this is zero)
2726
            // note: there can also be additional data, this is not included in $flen
2727 15
            $flen = self::getUInt2d($recordData, 4);
2728
2729
            // offset: 8; size: 2; 0=Global name, otherwise index to sheet (1-based)
2730 15
            $scope = self::getUInt2d($recordData, 8);
2731
2732
            // offset: 14; size: var; Name (Unicode string without length field)
2733 15
            $string = self::readUnicodeString(substr($recordData, 14), $nlen);
2734
2735
            // offset: var; size: $flen; formula data
2736 15
            $offset = 14 + $string['size'];
2737 15
            $formulaStructure = pack('v', $flen) . substr($recordData, $offset);
2738
2739
            try {
2740 15
                $formula = $this->getFormulaFromStructure($formulaStructure);
2741 1
            } catch (PhpSpreadsheetException) {
2742 1
                $formula = '';
2743
            }
2744
2745 15
            $this->definedname[] = [
2746 15
                'isBuiltInName' => $isBuiltInName,
2747 15
                'name' => $string['value'],
2748 15
                'formula' => $formula,
2749 15
                'scope' => $scope,
2750 15
            ];
2751
        }
2752
    }
2753
2754
    /**
2755
     * Read MSODRAWINGGROUP record.
2756
     */
2757 17
    private function readMsoDrawingGroup(): void
2758
    {
2759
        //$length = self::getUInt2d($this->data, $this->pos + 2);
2760
2761
        // get spliced record data
2762 17
        $splicedRecordData = $this->getSplicedRecordData();
2763 17
        $recordData = $splicedRecordData['recordData'];
2764
2765 17
        $this->drawingGroupData .= $recordData;
2766
    }
2767
2768
    /**
2769
     * SST - Shared String Table.
2770
     *
2771
     * This record contains a list of all strings used anywhere
2772
     * in the workbook. Each string occurs only once. The
2773
     * workbook uses indexes into the list to reference the
2774
     * strings.
2775
     *
2776
     * --    "OpenOffice.org's Documentation of the Microsoft
2777
     *         Excel File Format"
2778
     */
2779 90
    private function readSst(): void
2780
    {
2781
        // offset within (spliced) record data
2782 90
        $pos = 0;
2783
2784
        // Limit global SST position, further control for bad SST Length in BIFF8 data
2785 90
        $limitposSST = 0;
2786
2787
        // get spliced record data
2788 90
        $splicedRecordData = $this->getSplicedRecordData();
2789
2790 90
        $recordData = $splicedRecordData['recordData'];
2791 90
        $spliceOffsets = $splicedRecordData['spliceOffsets'];
2792
2793
        // offset: 0; size: 4; total number of strings in the workbook
2794 90
        $pos += 4;
2795
2796
        // offset: 4; size: 4; number of following strings ($nm)
2797 90
        $nm = self::getInt4d($recordData, 4);
2798 90
        $pos += 4;
2799
2800
        // look up limit position
2801 90
        foreach ($spliceOffsets as $spliceOffset) {
2802
            // it can happen that the string is empty, therefore we need
2803
            // <= and not just <
2804 90
            if ($pos <= $spliceOffset) {
2805 90
                $limitposSST = $spliceOffset;
2806
            }
2807
        }
2808
2809
        // loop through the Unicode strings (16-bit length)
2810 90
        for ($i = 0; $i < $nm && $pos < $limitposSST; ++$i) {
2811
            // number of characters in the Unicode string
2812 60
            $numChars = self::getUInt2d($recordData, $pos);
2813 60
            $pos += 2;
2814
2815
            // option flags
2816 60
            $optionFlags = ord($recordData[$pos]);
2817 60
            ++$pos;
2818
2819
            // bit: 0; mask: 0x01; 0 = compressed; 1 = uncompressed
2820 60
            $isCompressed = (($optionFlags & 0x01) == 0);
2821
2822
            // bit: 2; mask: 0x02; 0 = ordinary; 1 = Asian phonetic
2823 60
            $hasAsian = (($optionFlags & 0x04) != 0);
2824
2825
            // bit: 3; mask: 0x03; 0 = ordinary; 1 = Rich-Text
2826 60
            $hasRichText = (($optionFlags & 0x08) != 0);
2827
2828 60
            $formattingRuns = 0;
2829 60
            if ($hasRichText) {
2830
                // number of Rich-Text formatting runs
2831 5
                $formattingRuns = self::getUInt2d($recordData, $pos);
2832 5
                $pos += 2;
2833
            }
2834
2835 60
            $extendedRunLength = 0;
2836 60
            if ($hasAsian) {
2837
                // size of Asian phonetic setting
2838
                $extendedRunLength = self::getInt4d($recordData, $pos);
2839
                $pos += 4;
2840
            }
2841
2842
            // expected byte length of character array if not split
2843 60
            $len = ($isCompressed) ? $numChars : $numChars * 2;
2844
2845
            // look up limit position - Check it again to be sure that no error occurs when parsing SST structure
2846 60
            $limitpos = null;
2847 60
            foreach ($spliceOffsets as $spliceOffset) {
2848
                // it can happen that the string is empty, therefore we need
2849
                // <= and not just <
2850 60
                if ($pos <= $spliceOffset) {
2851 60
                    $limitpos = $spliceOffset;
2852
2853 60
                    break;
2854
                }
2855
            }
2856
2857 60
            if ($pos + $len <= $limitpos) {
2858
                // character array is not split between records
2859
2860 60
                $retstr = substr($recordData, $pos, $len);
2861 60
                $pos += $len;
2862
            } else {
2863
                // character array is split between records
2864
2865
                // first part of character array
2866 1
                $retstr = substr($recordData, $pos, $limitpos - $pos);
2867
2868 1
                $bytesRead = $limitpos - $pos;
2869
2870
                // remaining characters in Unicode string
2871 1
                $charsLeft = $numChars - (($isCompressed) ? $bytesRead : ($bytesRead / 2));
2872
2873 1
                $pos = $limitpos;
2874
2875
                // keep reading the characters
2876 1
                while ($charsLeft > 0) {
2877
                    // look up next limit position, in case the string span more than one continue record
2878 1
                    foreach ($spliceOffsets as $spliceOffset) {
2879 1
                        if ($pos < $spliceOffset) {
2880 1
                            $limitpos = $spliceOffset;
2881
2882 1
                            break;
2883
                        }
2884
                    }
2885
2886
                    // repeated option flags
2887
                    // OpenOffice.org documentation 5.21
2888 1
                    $option = ord($recordData[$pos]);
2889 1
                    ++$pos;
2890
2891 1
                    if ($isCompressed && ($option == 0)) {
2892
                        // 1st fragment compressed
2893
                        // this fragment compressed
2894
                        $len = min($charsLeft, $limitpos - $pos);
2895
                        $retstr .= substr($recordData, $pos, $len);
2896
                        $charsLeft -= $len;
2897
                        $isCompressed = true;
2898 1
                    } elseif (!$isCompressed && ($option != 0)) {
2899
                        // 1st fragment uncompressed
2900
                        // this fragment uncompressed
2901 1
                        $len = min($charsLeft * 2, $limitpos - $pos);
2902 1
                        $retstr .= substr($recordData, $pos, $len);
2903 1
                        $charsLeft -= $len / 2;
2904 1
                        $isCompressed = false;
2905
                    } elseif (!$isCompressed && ($option == 0)) {
2906
                        // 1st fragment uncompressed
2907
                        // this fragment compressed
2908
                        $len = min($charsLeft, $limitpos - $pos);
2909
                        for ($j = 0; $j < $len; ++$j) {
2910
                            $retstr .= $recordData[$pos + $j]
2911
                                . chr(0);
2912
                        }
2913
                        $charsLeft -= $len;
2914
                        $isCompressed = false;
2915
                    } else {
2916
                        // 1st fragment compressed
2917
                        // this fragment uncompressed
2918
                        $newstr = '';
2919
                        $jMax = strlen($retstr);
2920
                        for ($j = 0; $j < $jMax; ++$j) {
2921
                            $newstr .= $retstr[$j] . chr(0);
2922
                        }
2923
                        $retstr = $newstr;
2924
                        $len = min($charsLeft * 2, $limitpos - $pos);
2925
                        $retstr .= substr($recordData, $pos, $len);
2926
                        $charsLeft -= $len / 2;
2927
                        $isCompressed = false;
2928
                    }
2929
2930 1
                    $pos += $len;
2931
                }
2932
            }
2933
2934
            // convert to UTF-8
2935 60
            $retstr = self::encodeUTF16($retstr, $isCompressed);
2936
2937
            // read additional Rich-Text information, if any
2938 60
            $fmtRuns = [];
2939 60
            if ($hasRichText) {
2940
                // list of formatting runs
2941 5
                for ($j = 0; $j < $formattingRuns; ++$j) {
2942
                    // first formatted character; zero-based
2943 5
                    $charPos = self::getUInt2d($recordData, $pos + $j * 4);
2944
2945
                    // index to font record
2946 5
                    $fontIndex = self::getUInt2d($recordData, $pos + 2 + $j * 4);
2947
2948 5
                    $fmtRuns[] = [
2949 5
                        'charPos' => $charPos,
2950 5
                        'fontIndex' => $fontIndex,
2951 5
                    ];
2952
                }
2953 5
                $pos += 4 * $formattingRuns;
2954
            }
2955
2956
            // read additional Asian phonetics information, if any
2957 60
            if ($hasAsian) {
2958
                // For Asian phonetic settings, we skip the extended string data
2959
                $pos += $extendedRunLength;
2960
            }
2961
2962
            // store the shared sting
2963 60
            $this->sst[] = [
2964 60
                'value' => $retstr,
2965 60
                'fmtRuns' => $fmtRuns,
2966 60
            ];
2967
        }
2968
2969
        // getSplicedRecordData() takes care of moving current position in data stream
2970
    }
2971
2972
    /**
2973
     * Read PRINTGRIDLINES record.
2974
     */
2975 92
    private function readPrintGridlines(): void
2976
    {
2977 92
        $length = self::getUInt2d($this->data, $this->pos + 2);
2978 92
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2979
2980
        // move stream pointer to next record
2981 92
        $this->pos += 4 + $length;
2982
2983 92
        if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) {
2984
            // offset: 0; size: 2; 0 = do not print sheet grid lines; 1 = print sheet gridlines
2985 89
            $printGridlines = (bool) self::getUInt2d($recordData, 0);
2986 89
            $this->phpSheet->setPrintGridlines($printGridlines);
2987
        }
2988
    }
2989
2990
    /**
2991
     * Read DEFAULTROWHEIGHT record.
2992
     */
2993 52
    private function readDefaultRowHeight(): void
2994
    {
2995 52
        $length = self::getUInt2d($this->data, $this->pos + 2);
2996 52
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2997
2998
        // move stream pointer to next record
2999 52
        $this->pos += 4 + $length;
3000
3001
        // offset: 0; size: 2; option flags
3002
        // offset: 2; size: 2; default height for unused rows, (twips 1/20 point)
3003 52
        $height = self::getUInt2d($recordData, 2);
3004 52
        $this->phpSheet->getDefaultRowDimension()->setRowHeight($height / 20);
3005
    }
3006
3007
    /**
3008
     * Read SHEETPR record.
3009
     */
3010 94
    private function readSheetPr(): void
3011
    {
3012 94
        $length = self::getUInt2d($this->data, $this->pos + 2);
3013 94
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3014
3015
        // move stream pointer to next record
3016 94
        $this->pos += 4 + $length;
3017
3018
        // offset: 0; size: 2
3019
3020
        // bit: 6; mask: 0x0040; 0 = outline buttons above outline group
3021 94
        $isSummaryBelow = (0x0040 & self::getUInt2d($recordData, 0)) >> 6;
3022 94
        $this->phpSheet->setShowSummaryBelow((bool) $isSummaryBelow);
3023
3024
        // bit: 7; mask: 0x0080; 0 = outline buttons left of outline group
3025 94
        $isSummaryRight = (0x0080 & self::getUInt2d($recordData, 0)) >> 7;
3026 94
        $this->phpSheet->setShowSummaryRight((bool) $isSummaryRight);
3027
3028
        // bit: 8; mask: 0x100; 0 = scale printout in percent, 1 = fit printout to number of pages
3029
        // this corresponds to radio button setting in page setup dialog in Excel
3030 94
        $this->isFitToPages = (bool) ((0x0100 & self::getUInt2d($recordData, 0)) >> 8);
3031
    }
3032
3033
    /**
3034
     * Read HORIZONTALPAGEBREAKS record.
3035
     */
3036 4
    private function readHorizontalPageBreaks(): void
3037
    {
3038 4
        $length = self::getUInt2d($this->data, $this->pos + 2);
3039 4
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3040
3041
        // move stream pointer to next record
3042 4
        $this->pos += 4 + $length;
3043
3044 4
        if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) {
3045
            // offset: 0; size: 2; number of the following row index structures
3046 4
            $nm = self::getUInt2d($recordData, 0);
3047
3048
            // offset: 2; size: 6 * $nm; list of $nm row index structures
3049 4
            for ($i = 0; $i < $nm; ++$i) {
3050 2
                $r = self::getUInt2d($recordData, 2 + 6 * $i);
3051 2
                $cf = self::getUInt2d($recordData, 2 + 6 * $i + 2);
3052
                //$cl = self::getUInt2d($recordData, 2 + 6 * $i + 4);
3053
3054
                // not sure why two column indexes are necessary?
3055 2
                $this->phpSheet->setBreak([$cf + 1, $r], Worksheet::BREAK_ROW);
3056
            }
3057
        }
3058
    }
3059
3060
    /**
3061
     * Read VERTICALPAGEBREAKS record.
3062
     */
3063 4
    private function readVerticalPageBreaks(): void
3064
    {
3065 4
        $length = self::getUInt2d($this->data, $this->pos + 2);
3066 4
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3067
3068
        // move stream pointer to next record
3069 4
        $this->pos += 4 + $length;
3070
3071 4
        if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) {
3072
            // offset: 0; size: 2; number of the following column index structures
3073 4
            $nm = self::getUInt2d($recordData, 0);
3074
3075
            // offset: 2; size: 6 * $nm; list of $nm row index structures
3076 4
            for ($i = 0; $i < $nm; ++$i) {
3077 2
                $c = self::getUInt2d($recordData, 2 + 6 * $i);
3078 2
                $rf = self::getUInt2d($recordData, 2 + 6 * $i + 2);
3079
                //$rl = self::getUInt2d($recordData, 2 + 6 * $i + 4);
3080
3081
                // not sure why two row indexes are necessary?
3082 2
                $this->phpSheet->setBreak([$c + 1, ($rf > 0) ? $rf : 1], Worksheet::BREAK_COLUMN);
3083
            }
3084
        }
3085
    }
3086
3087
    /**
3088
     * Read HEADER record.
3089
     */
3090 92
    private function readHeader(): void
3091
    {
3092 92
        $length = self::getUInt2d($this->data, $this->pos + 2);
3093 92
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3094
3095
        // move stream pointer to next record
3096 92
        $this->pos += 4 + $length;
3097
3098 92
        if (!$this->readDataOnly) {
3099
            // offset: 0; size: var
3100
            // realized that $recordData can be empty even when record exists
3101 91
            if ($recordData) {
3102 56
                if ($this->version == self::XLS_BIFF8) {
3103 55
                    $string = self::readUnicodeStringLong($recordData);
3104
                } else {
3105 1
                    $string = $this->readByteStringShort($recordData);
3106
                }
3107
3108 56
                $this->phpSheet->getHeaderFooter()->setOddHeader($string['value']);
3109 56
                $this->phpSheet->getHeaderFooter()->setEvenHeader($string['value']);
3110
            }
3111
        }
3112
    }
3113
3114
    /**
3115
     * Read FOOTER record.
3116
     */
3117 92
    private function readFooter(): void
3118
    {
3119 92
        $length = self::getUInt2d($this->data, $this->pos + 2);
3120 92
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3121
3122
        // move stream pointer to next record
3123 92
        $this->pos += 4 + $length;
3124
3125 92
        if (!$this->readDataOnly) {
3126
            // offset: 0; size: var
3127
            // realized that $recordData can be empty even when record exists
3128 91
            if ($recordData) {
3129 58
                if ($this->version == self::XLS_BIFF8) {
3130 56
                    $string = self::readUnicodeStringLong($recordData);
3131
                } else {
3132 2
                    $string = $this->readByteStringShort($recordData);
3133
                }
3134 58
                $this->phpSheet->getHeaderFooter()->setOddFooter($string['value']);
3135 58
                $this->phpSheet->getHeaderFooter()->setEvenFooter($string['value']);
3136
            }
3137
        }
3138
    }
3139
3140
    /**
3141
     * Read HCENTER record.
3142
     */
3143 92
    private function readHcenter(): void
3144
    {
3145 92
        $length = self::getUInt2d($this->data, $this->pos + 2);
3146 92
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3147
3148
        // move stream pointer to next record
3149 92
        $this->pos += 4 + $length;
3150
3151 92
        if (!$this->readDataOnly) {
3152
            // offset: 0; size: 2; 0 = print sheet left aligned, 1 = print sheet centered horizontally
3153 91
            $isHorizontalCentered = (bool) self::getUInt2d($recordData, 0);
3154
3155 91
            $this->phpSheet->getPageSetup()->setHorizontalCentered($isHorizontalCentered);
3156
        }
3157
    }
3158
3159
    /**
3160
     * Read VCENTER record.
3161
     */
3162 92
    private function readVcenter(): void
3163
    {
3164 92
        $length = self::getUInt2d($this->data, $this->pos + 2);
3165 92
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3166
3167
        // move stream pointer to next record
3168 92
        $this->pos += 4 + $length;
3169
3170 92
        if (!$this->readDataOnly) {
3171
            // offset: 0; size: 2; 0 = print sheet aligned at top page border, 1 = print sheet vertically centered
3172 91
            $isVerticalCentered = (bool) self::getUInt2d($recordData, 0);
3173
3174 91
            $this->phpSheet->getPageSetup()->setVerticalCentered($isVerticalCentered);
3175
        }
3176
    }
3177
3178
    /**
3179
     * Read LEFTMARGIN record.
3180
     */
3181 87
    private function readLeftMargin(): void
3182
    {
3183 87
        $length = self::getUInt2d($this->data, $this->pos + 2);
3184 87
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3185
3186
        // move stream pointer to next record
3187 87
        $this->pos += 4 + $length;
3188
3189 87
        if (!$this->readDataOnly) {
3190
            // offset: 0; size: 8
3191 86
            $this->phpSheet->getPageMargins()->setLeft(self::extractNumber($recordData));
3192
        }
3193
    }
3194
3195
    /**
3196
     * Read RIGHTMARGIN record.
3197
     */
3198 87
    private function readRightMargin(): void
3199
    {
3200 87
        $length = self::getUInt2d($this->data, $this->pos + 2);
3201 87
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3202
3203
        // move stream pointer to next record
3204 87
        $this->pos += 4 + $length;
3205
3206 87
        if (!$this->readDataOnly) {
3207
            // offset: 0; size: 8
3208 86
            $this->phpSheet->getPageMargins()->setRight(self::extractNumber($recordData));
3209
        }
3210
    }
3211
3212
    /**
3213
     * Read TOPMARGIN record.
3214
     */
3215 87
    private function readTopMargin(): void
3216
    {
3217 87
        $length = self::getUInt2d($this->data, $this->pos + 2);
3218 87
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3219
3220
        // move stream pointer to next record
3221 87
        $this->pos += 4 + $length;
3222
3223 87
        if (!$this->readDataOnly) {
3224
            // offset: 0; size: 8
3225 86
            $this->phpSheet->getPageMargins()->setTop(self::extractNumber($recordData));
3226
        }
3227
    }
3228
3229
    /**
3230
     * Read BOTTOMMARGIN record.
3231
     */
3232 87
    private function readBottomMargin(): void
3233
    {
3234 87
        $length = self::getUInt2d($this->data, $this->pos + 2);
3235 87
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3236
3237
        // move stream pointer to next record
3238 87
        $this->pos += 4 + $length;
3239
3240 87
        if (!$this->readDataOnly) {
3241
            // offset: 0; size: 8
3242 86
            $this->phpSheet->getPageMargins()->setBottom(self::extractNumber($recordData));
3243
        }
3244
    }
3245
3246
    /**
3247
     * Read PAGESETUP record.
3248
     */
3249 94
    private function readPageSetup(): void
3250
    {
3251 94
        $length = self::getUInt2d($this->data, $this->pos + 2);
3252 94
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3253
3254
        // move stream pointer to next record
3255 94
        $this->pos += 4 + $length;
3256
3257 94
        if (!$this->readDataOnly) {
3258
            // offset: 0; size: 2; paper size
3259 93
            $paperSize = self::getUInt2d($recordData, 0);
3260
3261
            // offset: 2; size: 2; scaling factor
3262 93
            $scale = self::getUInt2d($recordData, 2);
3263
3264
            // offset: 6; size: 2; fit worksheet width to this number of pages, 0 = use as many as needed
3265 93
            $fitToWidth = self::getUInt2d($recordData, 6);
3266
3267
            // offset: 8; size: 2; fit worksheet height to this number of pages, 0 = use as many as needed
3268 93
            $fitToHeight = self::getUInt2d($recordData, 8);
3269
3270
            // offset: 10; size: 2; option flags
3271
3272
            // bit: 0; mask: 0x0001; 0=down then over, 1=over then down
3273 93
            $isOverThenDown = (0x0001 & self::getUInt2d($recordData, 10));
3274
3275
            // bit: 1; mask: 0x0002; 0=landscape, 1=portrait
3276 93
            $isPortrait = (0x0002 & self::getUInt2d($recordData, 10)) >> 1;
3277
3278
            // bit: 2; mask: 0x0004; 1= paper size, scaling factor, paper orient. not init
3279
            // when this bit is set, do not use flags for those properties
3280 93
            $isNotInit = (0x0004 & self::getUInt2d($recordData, 10)) >> 2;
3281
3282 93
            if (!$isNotInit) {
3283 85
                $this->phpSheet->getPageSetup()->setPaperSize($paperSize);
3284 85
                $this->phpSheet->getPageSetup()->setPageOrder(((bool) $isOverThenDown) ? PageSetup::PAGEORDER_OVER_THEN_DOWN : PageSetup::PAGEORDER_DOWN_THEN_OVER);
3285 85
                $this->phpSheet->getPageSetup()->setOrientation(((bool) $isPortrait) ? PageSetup::ORIENTATION_PORTRAIT : PageSetup::ORIENTATION_LANDSCAPE);
3286
3287 85
                $this->phpSheet->getPageSetup()->setScale($scale, false);
3288 85
                $this->phpSheet->getPageSetup()->setFitToPage((bool) $this->isFitToPages);
3289 85
                $this->phpSheet->getPageSetup()->setFitToWidth($fitToWidth, false);
3290 85
                $this->phpSheet->getPageSetup()->setFitToHeight($fitToHeight, false);
3291
            }
3292
3293
            // offset: 16; size: 8; header margin (IEEE 754 floating-point value)
3294 93
            $marginHeader = self::extractNumber(substr($recordData, 16, 8));
3295 93
            $this->phpSheet->getPageMargins()->setHeader($marginHeader);
3296
3297
            // offset: 24; size: 8; footer margin (IEEE 754 floating-point value)
3298 93
            $marginFooter = self::extractNumber(substr($recordData, 24, 8));
3299 93
            $this->phpSheet->getPageMargins()->setFooter($marginFooter);
3300
        }
3301
    }
3302
3303
    /**
3304
     * PROTECT - Sheet protection (BIFF2 through BIFF8)
3305
     *   if this record is omitted, then it also means no sheet protection.
3306
     */
3307 6
    private function readProtect(): void
3308
    {
3309 6
        $length = self::getUInt2d($this->data, $this->pos + 2);
3310 6
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3311
3312
        // move stream pointer to next record
3313 6
        $this->pos += 4 + $length;
3314
3315 6
        if ($this->readDataOnly) {
3316
            return;
3317
        }
3318
3319
        // offset: 0; size: 2;
3320
3321
        // bit 0, mask 0x01; 1 = sheet is protected
3322 6
        $bool = (0x01 & self::getUInt2d($recordData, 0)) >> 0;
3323 6
        $this->phpSheet->getProtection()->setSheet((bool) $bool);
3324
    }
3325
3326
    /**
3327
     * SCENPROTECT.
3328
     */
3329
    private function readScenProtect(): void
3330
    {
3331
        $length = self::getUInt2d($this->data, $this->pos + 2);
3332
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3333
3334
        // move stream pointer to next record
3335
        $this->pos += 4 + $length;
3336
3337
        if ($this->readDataOnly) {
3338
            return;
3339
        }
3340
3341
        // offset: 0; size: 2;
3342
3343
        // bit: 0, mask 0x01; 1 = scenarios are protected
3344
        $bool = (0x01 & self::getUInt2d($recordData, 0)) >> 0;
3345
3346
        $this->phpSheet->getProtection()->setScenarios((bool) $bool);
3347
    }
3348
3349
    /**
3350
     * OBJECTPROTECT.
3351
     */
3352 1
    private function readObjectProtect(): void
3353
    {
3354 1
        $length = self::getUInt2d($this->data, $this->pos + 2);
3355 1
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3356
3357
        // move stream pointer to next record
3358 1
        $this->pos += 4 + $length;
3359
3360 1
        if ($this->readDataOnly) {
3361
            return;
3362
        }
3363
3364
        // offset: 0; size: 2;
3365
3366
        // bit: 0, mask 0x01; 1 = objects are protected
3367 1
        $bool = (0x01 & self::getUInt2d($recordData, 0)) >> 0;
3368
3369 1
        $this->phpSheet->getProtection()->setObjects((bool) $bool);
3370
    }
3371
3372
    /**
3373
     * PASSWORD - Sheet protection (hashed) password (BIFF2 through BIFF8).
3374
     */
3375 2
    private function readPassword(): void
3376
    {
3377 2
        $length = self::getUInt2d($this->data, $this->pos + 2);
3378 2
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3379
3380
        // move stream pointer to next record
3381 2
        $this->pos += 4 + $length;
3382
3383 2
        if (!$this->readDataOnly) {
3384
            // offset: 0; size: 2; 16-bit hash value of password
3385 2
            $password = strtoupper(dechex(self::getUInt2d($recordData, 0))); // the hashed password
3386 2
            $this->phpSheet->getProtection()->setPassword($password, true);
3387
        }
3388
    }
3389
3390
    /**
3391
     * Read DEFCOLWIDTH record.
3392
     */
3393 93
    private function readDefColWidth(): void
3394
    {
3395 93
        $length = self::getUInt2d($this->data, $this->pos + 2);
3396 93
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3397
3398
        // move stream pointer to next record
3399 93
        $this->pos += 4 + $length;
3400
3401
        // offset: 0; size: 2; default column width
3402 93
        $width = self::getUInt2d($recordData, 0);
3403 93
        if ($width != 8) {
3404 4
            $this->phpSheet->getDefaultColumnDimension()->setWidth($width);
3405
        }
3406
    }
3407
3408
    /**
3409
     * Read COLINFO record.
3410
     */
3411 85
    private function readColInfo(): void
3412
    {
3413 85
        $length = self::getUInt2d($this->data, $this->pos + 2);
3414 85
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3415
3416
        // move stream pointer to next record
3417 85
        $this->pos += 4 + $length;
3418
3419 85
        if (!$this->readDataOnly) {
3420
            // offset: 0; size: 2; index to first column in range
3421 84
            $firstColumnIndex = self::getUInt2d($recordData, 0);
3422
3423
            // offset: 2; size: 2; index to last column in range
3424 84
            $lastColumnIndex = self::getUInt2d($recordData, 2);
3425
3426
            // offset: 4; size: 2; width of the column in 1/256 of the width of the zero character
3427 84
            $width = self::getUInt2d($recordData, 4);
3428
3429
            // offset: 6; size: 2; index to XF record for default column formatting
3430 84
            $xfIndex = self::getUInt2d($recordData, 6);
3431
3432
            // offset: 8; size: 2; option flags
3433
            // bit: 0; mask: 0x0001; 1= columns are hidden
3434 84
            $isHidden = (0x0001 & self::getUInt2d($recordData, 8)) >> 0;
3435
3436
            // bit: 10-8; mask: 0x0700; outline level of the columns (0 = no outline)
3437 84
            $level = (0x0700 & self::getUInt2d($recordData, 8)) >> 8;
3438
3439
            // bit: 12; mask: 0x1000; 1 = collapsed
3440 84
            $isCollapsed = (bool) ((0x1000 & self::getUInt2d($recordData, 8)) >> 12);
3441
3442
            // offset: 10; size: 2; not used
3443
3444 84
            for ($i = $firstColumnIndex + 1; $i <= $lastColumnIndex + 1; ++$i) {
3445 84
                if ($lastColumnIndex == 255 || $lastColumnIndex == 256) {
3446 13
                    $this->phpSheet->getDefaultColumnDimension()->setWidth($width / 256);
3447
3448 13
                    break;
3449
                }
3450 76
                $this->phpSheet->getColumnDimensionByColumn($i)->setWidth($width / 256);
3451 76
                $this->phpSheet->getColumnDimensionByColumn($i)->setVisible(!$isHidden);
3452 76
                $this->phpSheet->getColumnDimensionByColumn($i)->setOutlineLevel($level);
3453 76
                $this->phpSheet->getColumnDimensionByColumn($i)->setCollapsed($isCollapsed);
3454 76
                if (isset($this->mapCellXfIndex[$xfIndex])) {
3455 74
                    $this->phpSheet->getColumnDimensionByColumn($i)->setXfIndex($this->mapCellXfIndex[$xfIndex]);
3456
                }
3457
            }
3458
        }
3459
    }
3460
3461
    /**
3462
     * ROW.
3463
     *
3464
     * This record contains the properties of a single row in a
3465
     * sheet. Rows and cells in a sheet are divided into blocks
3466
     * of 32 rows.
3467
     *
3468
     * --    "OpenOffice.org's Documentation of the Microsoft
3469
     *         Excel File Format"
3470
     */
3471 59
    private function readRow(): void
3472
    {
3473 59
        $length = self::getUInt2d($this->data, $this->pos + 2);
3474 59
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3475
3476
        // move stream pointer to next record
3477 59
        $this->pos += 4 + $length;
3478
3479 59
        if (!$this->readDataOnly) {
3480
            // offset: 0; size: 2; index of this row
3481 58
            $r = self::getUInt2d($recordData, 0);
3482
3483
            // offset: 2; size: 2; index to column of the first cell which is described by a cell record
3484
3485
            // offset: 4; size: 2; index to column of the last cell which is described by a cell record, increased by 1
3486
3487
            // offset: 6; size: 2;
3488
3489
            // bit: 14-0; mask: 0x7FFF; height of the row, in twips = 1/20 of a point
3490 58
            $height = (0x7FFF & self::getUInt2d($recordData, 6)) >> 0;
3491
3492
            // bit: 15: mask: 0x8000; 0 = row has custom height; 1= row has default height
3493 58
            $useDefaultHeight = (0x8000 & self::getUInt2d($recordData, 6)) >> 15;
3494
3495 58
            if (!$useDefaultHeight) {
3496 56
                $this->phpSheet->getRowDimension($r + 1)->setRowHeight($height / 20);
3497
            }
3498
3499
            // offset: 8; size: 2; not used
3500
3501
            // offset: 10; size: 2; not used in BIFF5-BIFF8
3502
3503
            // offset: 12; size: 4; option flags and default row formatting
3504
3505
            // bit: 2-0: mask: 0x00000007; outline level of the row
3506 58
            $level = (0x00000007 & self::getInt4d($recordData, 12)) >> 0;
3507 58
            $this->phpSheet->getRowDimension($r + 1)->setOutlineLevel($level);
3508
3509
            // bit: 4; mask: 0x00000010; 1 = outline group start or ends here... and is collapsed
3510 58
            $isCollapsed = (bool) ((0x00000010 & self::getInt4d($recordData, 12)) >> 4);
3511 58
            $this->phpSheet->getRowDimension($r + 1)->setCollapsed($isCollapsed);
3512
3513
            // bit: 5; mask: 0x00000020; 1 = row is hidden
3514 58
            $isHidden = (0x00000020 & self::getInt4d($recordData, 12)) >> 5;
3515 58
            $this->phpSheet->getRowDimension($r + 1)->setVisible(!$isHidden);
3516
3517
            // bit: 7; mask: 0x00000080; 1 = row has explicit format
3518 58
            $hasExplicitFormat = (0x00000080 & self::getInt4d($recordData, 12)) >> 7;
3519
3520
            // bit: 27-16; mask: 0x0FFF0000; only applies when hasExplicitFormat = 1; index to XF record
3521 58
            $xfIndex = (0x0FFF0000 & self::getInt4d($recordData, 12)) >> 16;
3522
3523 58
            if ($hasExplicitFormat && isset($this->mapCellXfIndex[$xfIndex])) {
3524 6
                $this->phpSheet->getRowDimension($r + 1)->setXfIndex($this->mapCellXfIndex[$xfIndex]);
3525
            }
3526
        }
3527
    }
3528
3529
    /**
3530
     * Read RK record
3531
     * This record represents a cell that contains an RK value
3532
     * (encoded integer or floating-point value). If a
3533
     * floating-point value cannot be encoded to an RK value,
3534
     * a NUMBER record will be written. This record replaces the
3535
     * record INTEGER written in BIFF2.
3536
     *
3537
     * --    "OpenOffice.org's Documentation of the Microsoft
3538
     *         Excel File Format"
3539
     */
3540 27
    private function readRk(): void
3541
    {
3542 27
        $length = self::getUInt2d($this->data, $this->pos + 2);
3543 27
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3544
3545
        // move stream pointer to next record
3546 27
        $this->pos += 4 + $length;
3547
3548
        // offset: 0; size: 2; index to row
3549 27
        $row = self::getUInt2d($recordData, 0);
3550
3551
        // offset: 2; size: 2; index to column
3552 27
        $column = self::getUInt2d($recordData, 2);
3553 27
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
3554
3555
        // Read cell?
3556 27
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
3557
            // offset: 4; size: 2; index to XF record
3558 27
            $xfIndex = self::getUInt2d($recordData, 4);
3559
3560
            // offset: 6; size: 4; RK value
3561 27
            $rknum = self::getInt4d($recordData, 6);
3562 27
            $numValue = self::getIEEE754($rknum);
3563
3564 27
            $cell = $this->phpSheet->getCell($columnString . ($row + 1));
3565 27
            if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
3566
                // add style information
3567 24
                $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
3568
            }
3569
3570
            // add cell
3571 27
            $cell->setValueExplicit($numValue, DataType::TYPE_NUMERIC);
3572
        }
3573
    }
3574
3575
    /**
3576
     * Read LABELSST record
3577
     * This record represents a cell that contains a string. It
3578
     * replaces the LABEL record and RSTRING record used in
3579
     * BIFF2-BIFF5.
3580
     *
3581
     * --    "OpenOffice.org's Documentation of the Microsoft
3582
     *         Excel File Format"
3583
     */
3584 59
    private function readLabelSst(): void
3585
    {
3586 59
        $length = self::getUInt2d($this->data, $this->pos + 2);
3587 59
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3588
3589
        // move stream pointer to next record
3590 59
        $this->pos += 4 + $length;
3591
3592
        // offset: 0; size: 2; index to row
3593 59
        $row = self::getUInt2d($recordData, 0);
3594
3595
        // offset: 2; size: 2; index to column
3596 59
        $column = self::getUInt2d($recordData, 2);
3597 59
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
3598
3599 59
        $cell = null;
3600
        // Read cell?
3601 59
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
3602
            // offset: 4; size: 2; index to XF record
3603 59
            $xfIndex = self::getUInt2d($recordData, 4);
3604
3605
            // offset: 6; size: 4; index to SST record
3606 59
            $index = self::getInt4d($recordData, 6);
3607
3608
            // add cell
3609 59
            if (($fmtRuns = $this->sst[$index]['fmtRuns']) && !$this->readDataOnly) {
3610
                // then we should treat as rich text
3611 5
                $richText = new RichText();
3612 5
                $charPos = 0;
3613 5
                $sstCount = count($this->sst[$index]['fmtRuns']);
3614 5
                for ($i = 0; $i <= $sstCount; ++$i) {
3615 5
                    if (isset($fmtRuns[$i])) {
3616 5
                        $text = StringHelper::substring($this->sst[$index]['value'], $charPos, $fmtRuns[$i]['charPos'] - $charPos);
3617 5
                        $charPos = $fmtRuns[$i]['charPos'];
3618
                    } else {
3619 5
                        $text = StringHelper::substring($this->sst[$index]['value'], $charPos, StringHelper::countCharacters($this->sst[$index]['value']));
3620
                    }
3621
3622 5
                    if (StringHelper::countCharacters($text) > 0) {
3623 5
                        if ($i == 0) { // first text run, no style
3624 3
                            $richText->createText($text);
3625
                        } else {
3626 5
                            $textRun = $richText->createTextRun($text);
3627 5
                            if (isset($fmtRuns[$i - 1])) {
3628 5
                                if ($fmtRuns[$i - 1]['fontIndex'] < 4) {
3629 4
                                    $fontIndex = $fmtRuns[$i - 1]['fontIndex'];
3630
                                } else {
3631
                                    // this has to do with that index 4 is omitted in all BIFF versions for some stra          nge reason
3632
                                    // check the OpenOffice documentation of the FONT record
3633 4
                                    $fontIndex = $fmtRuns[$i - 1]['fontIndex'] - 1;
3634
                                }
3635 5
                                if (array_key_exists($fontIndex, $this->objFonts) === false) {
3636 1
                                    $fontIndex = count($this->objFonts) - 1;
3637
                                }
3638 5
                                $textRun->setFont(clone $this->objFonts[$fontIndex]);
3639
                            }
3640
                        }
3641
                    }
3642
                }
3643 5
                if ($this->readEmptyCells || trim($richText->getPlainText()) !== '') {
3644 5
                    $cell = $this->phpSheet->getCell($columnString . ($row + 1));
3645 5
                    $cell->setValueExplicit($richText, DataType::TYPE_STRING);
3646
                }
3647
            } else {
3648 59
                if ($this->readEmptyCells || trim($this->sst[$index]['value']) !== '') {
3649 59
                    $cell = $this->phpSheet->getCell($columnString . ($row + 1));
3650 59
                    $cell->setValueExplicit($this->sst[$index]['value'], DataType::TYPE_STRING);
3651
                }
3652
            }
3653
3654 59
            if (!$this->readDataOnly && $cell !== null && isset($this->mapCellXfIndex[$xfIndex])) {
3655
                // add style information
3656 58
                $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
3657
            }
3658
        }
3659
    }
3660
3661
    /**
3662
     * Read MULRK record
3663
     * This record represents a cell range containing RK value
3664
     * cells. All cells are located in the same row.
3665
     *
3666
     * --    "OpenOffice.org's Documentation of the Microsoft
3667
     *         Excel File Format"
3668
     */
3669 21
    private function readMulRk(): void
3670
    {
3671 21
        $length = self::getUInt2d($this->data, $this->pos + 2);
3672 21
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3673
3674
        // move stream pointer to next record
3675 21
        $this->pos += 4 + $length;
3676
3677
        // offset: 0; size: 2; index to row
3678 21
        $row = self::getUInt2d($recordData, 0);
3679
3680
        // offset: 2; size: 2; index to first column
3681 21
        $colFirst = self::getUInt2d($recordData, 2);
3682
3683
        // offset: var; size: 2; index to last column
3684 21
        $colLast = self::getUInt2d($recordData, $length - 2);
3685 21
        $columns = $colLast - $colFirst + 1;
3686
3687
        // offset within record data
3688 21
        $offset = 4;
3689
3690 21
        for ($i = 1; $i <= $columns; ++$i) {
3691 21
            $columnString = Coordinate::stringFromColumnIndex($colFirst + $i);
3692
3693
            // Read cell?
3694 21
            if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
3695
                // offset: var; size: 2; index to XF record
3696 21
                $xfIndex = self::getUInt2d($recordData, $offset);
3697
3698
                // offset: var; size: 4; RK value
3699 21
                $numValue = self::getIEEE754(self::getInt4d($recordData, $offset + 2));
3700 21
                $cell = $this->phpSheet->getCell($columnString . ($row + 1));
3701 21
                if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
3702
                    // add style
3703 20
                    $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
3704
                }
3705
3706
                // add cell value
3707 21
                $cell->setValueExplicit($numValue, DataType::TYPE_NUMERIC);
3708
            }
3709
3710 21
            $offset += 6;
3711
        }
3712
    }
3713
3714
    /**
3715
     * Read NUMBER record
3716
     * This record represents a cell that contains a
3717
     * floating-point value.
3718
     *
3719
     * --    "OpenOffice.org's Documentation of the Microsoft
3720
     *         Excel File Format"
3721
     */
3722 47
    private function readNumber(): void
3723
    {
3724 47
        $length = self::getUInt2d($this->data, $this->pos + 2);
3725 47
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3726
3727
        // move stream pointer to next record
3728 47
        $this->pos += 4 + $length;
3729
3730
        // offset: 0; size: 2; index to row
3731 47
        $row = self::getUInt2d($recordData, 0);
3732
3733
        // offset: 2; size 2; index to column
3734 47
        $column = self::getUInt2d($recordData, 2);
3735 47
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
3736
3737
        // Read cell?
3738 47
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
3739
            // offset 4; size: 2; index to XF record
3740 47
            $xfIndex = self::getUInt2d($recordData, 4);
3741
3742 47
            $numValue = self::extractNumber(substr($recordData, 6, 8));
3743
3744 47
            $cell = $this->phpSheet->getCell($columnString . ($row + 1));
3745 47
            if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
3746
                // add cell style
3747 46
                $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
3748
            }
3749
3750
            // add cell value
3751 47
            $cell->setValueExplicit($numValue, DataType::TYPE_NUMERIC);
3752
        }
3753
    }
3754
3755
    /**
3756
     * Read FORMULA record + perhaps a following STRING record if formula result is a string
3757
     * This record contains the token array and the result of a
3758
     * formula cell.
3759
     *
3760
     * --    "OpenOffice.org's Documentation of the Microsoft
3761
     *         Excel File Format"
3762
     */
3763 29
    private function readFormula(): void
3764
    {
3765 29
        $length = self::getUInt2d($this->data, $this->pos + 2);
3766 29
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3767
3768
        // move stream pointer to next record
3769 29
        $this->pos += 4 + $length;
3770
3771
        // offset: 0; size: 2; row index
3772 29
        $row = self::getUInt2d($recordData, 0);
3773
3774
        // offset: 2; size: 2; col index
3775 29
        $column = self::getUInt2d($recordData, 2);
3776 29
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
3777
3778
        // offset: 20: size: variable; formula structure
3779 29
        $formulaStructure = substr($recordData, 20);
3780
3781
        // offset: 14: size: 2; option flags, recalculate always, recalculate on open etc.
3782 29
        $options = self::getUInt2d($recordData, 14);
3783
3784
        // bit: 0; mask: 0x0001; 1 = recalculate always
3785
        // bit: 1; mask: 0x0002; 1 = calculate on open
3786
        // bit: 2; mask: 0x0008; 1 = part of a shared formula
3787 29
        $isPartOfSharedFormula = (bool) (0x0008 & $options);
3788
3789
        // WARNING:
3790
        // We can apparently not rely on $isPartOfSharedFormula. Even when $isPartOfSharedFormula = true
3791
        // the formula data may be ordinary formula data, therefore we need to check
3792
        // explicitly for the tExp token (0x01)
3793 29
        $isPartOfSharedFormula = $isPartOfSharedFormula && ord($formulaStructure[2]) == 0x01;
3794
3795 29
        if ($isPartOfSharedFormula) {
3796
            // part of shared formula which means there will be a formula with a tExp token and nothing else
3797
            // get the base cell, grab tExp token
3798
            $baseRow = self::getUInt2d($formulaStructure, 3);
3799
            $baseCol = self::getUInt2d($formulaStructure, 5);
3800
            $this->baseCell = Coordinate::stringFromColumnIndex($baseCol + 1) . ($baseRow + 1);
3801
        }
3802
3803
        // Read cell?
3804 29
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
3805 29
            if ($isPartOfSharedFormula) {
3806
                // formula is added to this cell after the sheet has been read
3807
                $this->sharedFormulaParts[$columnString . ($row + 1)] = $this->baseCell;
3808
            }
3809
3810
            // offset: 16: size: 4; not used
3811
3812
            // offset: 4; size: 2; XF index
3813 29
            $xfIndex = self::getUInt2d($recordData, 4);
3814
3815
            // offset: 6; size: 8; result of the formula
3816 29
            if ((ord($recordData[6]) == 0) && (ord($recordData[12]) == 255) && (ord($recordData[13]) == 255)) {
3817
                // String formula. Result follows in appended STRING record
3818 7
                $dataType = DataType::TYPE_STRING;
3819
3820
                // read possible SHAREDFMLA record
3821 7
                $code = self::getUInt2d($this->data, $this->pos);
3822 7
                if ($code == self::XLS_TYPE_SHAREDFMLA) {
3823
                    $this->readSharedFmla();
3824
                }
3825
3826
                // read STRING record
3827 7
                $value = $this->readString();
3828
            } elseif (
3829 26
                (ord($recordData[6]) == 1)
3830 26
                && (ord($recordData[12]) == 255)
3831 26
                && (ord($recordData[13]) == 255)
3832
            ) {
3833
                // Boolean formula. Result is in +2; 0=false, 1=true
3834 2
                $dataType = DataType::TYPE_BOOL;
3835 2
                $value = (bool) ord($recordData[8]);
3836
            } elseif (
3837 25
                (ord($recordData[6]) == 2)
3838 25
                && (ord($recordData[12]) == 255)
3839 25
                && (ord($recordData[13]) == 255)
3840
            ) {
3841
                // Error formula. Error code is in +2
3842 10
                $dataType = DataType::TYPE_ERROR;
3843 10
                $value = Xls\ErrorCode::lookup(ord($recordData[8]));
3844
            } elseif (
3845 25
                (ord($recordData[6]) == 3)
3846 25
                && (ord($recordData[12]) == 255)
3847 25
                && (ord($recordData[13]) == 255)
3848
            ) {
3849
                // Formula result is a null string
3850 2
                $dataType = DataType::TYPE_NULL;
3851 2
                $value = '';
3852
            } else {
3853
                // forumla result is a number, first 14 bytes like _NUMBER record
3854 25
                $dataType = DataType::TYPE_NUMERIC;
3855 25
                $value = self::extractNumber(substr($recordData, 6, 8));
3856
            }
3857
3858 29
            $cell = $this->phpSheet->getCell($columnString . ($row + 1));
3859 29
            if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
3860
                // add cell style
3861 28
                $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
3862
            }
3863
3864
            // store the formula
3865 29
            if (!$isPartOfSharedFormula) {
3866
                // not part of shared formula
3867
                // add cell value. If we can read formula, populate with formula, otherwise just used cached value
3868
                try {
3869 29
                    if ($this->version != self::XLS_BIFF8) {
3870 1
                        throw new Exception('Not BIFF8. Can only read BIFF8 formulas');
3871
                    }
3872 28
                    $formula = $this->getFormulaFromStructure($formulaStructure); // get formula in human language
3873 28
                    $cell->setValueExplicit('=' . $formula, DataType::TYPE_FORMULA);
3874 2
                } catch (PhpSpreadsheetException) {
3875 29
                    $cell->setValueExplicit($value, $dataType);
3876
                }
3877
            } else {
3878
                if ($this->version == self::XLS_BIFF8) {
3879
                    // do nothing at this point, formula id added later in the code
3880
                } else {
3881
                    $cell->setValueExplicit($value, $dataType);
3882
                }
3883
            }
3884
3885
            // store the cached calculated value
3886 29
            $cell->setCalculatedValue($value, $dataType === DataType::TYPE_NUMERIC);
3887
        }
3888
    }
3889
3890
    /**
3891
     * Read a SHAREDFMLA record. This function just stores the binary shared formula in the reader,
3892
     * which usually contains relative references.
3893
     * These will be used to construct the formula in each shared formula part after the sheet is read.
3894
     */
3895
    private function readSharedFmla(): void
3896
    {
3897
        $length = self::getUInt2d($this->data, $this->pos + 2);
3898
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3899
3900
        // move stream pointer to next record
3901
        $this->pos += 4 + $length;
3902
3903
        // offset: 0, size: 6; cell range address of the area used by the shared formula, not used for anything
3904
        //$cellRange = substr($recordData, 0, 6);
3905
        //$cellRange = $this->readBIFF5CellRangeAddressFixed($cellRange); // note: even BIFF8 uses BIFF5 syntax
3906
3907
        // offset: 6, size: 1; not used
3908
3909
        // offset: 7, size: 1; number of existing FORMULA records for this shared formula
3910
        //$no = ord($recordData[7]);
3911
3912
        // offset: 8, size: var; Binary token array of the shared formula
3913
        $formula = substr($recordData, 8);
3914
3915
        // at this point we only store the shared formula for later use
3916
        $this->sharedFormulas[$this->baseCell] = $formula;
3917
    }
3918
3919
    /**
3920
     * Read a STRING record from current stream position and advance the stream pointer to next record
3921
     * This record is used for storing result from FORMULA record when it is a string, and
3922
     * it occurs directly after the FORMULA record.
3923
     *
3924
     * @return string The string contents as UTF-8
3925
     */
3926 7
    private function readString(): string
3927
    {
3928 7
        $length = self::getUInt2d($this->data, $this->pos + 2);
3929 7
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3930
3931
        // move stream pointer to next record
3932 7
        $this->pos += 4 + $length;
3933
3934 7
        if ($this->version == self::XLS_BIFF8) {
3935 7
            $string = self::readUnicodeStringLong($recordData);
3936 7
            $value = $string['value'];
3937
        } else {
3938
            $string = $this->readByteStringLong($recordData);
3939
            $value = $string['value'];
3940
        }
3941
3942 7
        return $value;
3943
    }
3944
3945
    /**
3946
     * Read BOOLERR record
3947
     * This record represents a Boolean value or error value
3948
     * cell.
3949
     *
3950
     * --    "OpenOffice.org's Documentation of the Microsoft
3951
     *         Excel File Format"
3952
     */
3953 10
    private function readBoolErr(): void
3954
    {
3955 10
        $length = self::getUInt2d($this->data, $this->pos + 2);
3956 10
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3957
3958
        // move stream pointer to next record
3959 10
        $this->pos += 4 + $length;
3960
3961
        // offset: 0; size: 2; row index
3962 10
        $row = self::getUInt2d($recordData, 0);
3963
3964
        // offset: 2; size: 2; column index
3965 10
        $column = self::getUInt2d($recordData, 2);
3966 10
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
3967
3968
        // Read cell?
3969 10
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
3970
            // offset: 4; size: 2; index to XF record
3971 10
            $xfIndex = self::getUInt2d($recordData, 4);
3972
3973
            // offset: 6; size: 1; the boolean value or error value
3974 10
            $boolErr = ord($recordData[6]);
3975
3976
            // offset: 7; size: 1; 0=boolean; 1=error
3977 10
            $isError = ord($recordData[7]);
3978
3979 10
            $cell = $this->phpSheet->getCell($columnString . ($row + 1));
3980
            switch ($isError) {
3981 10
                case 0: // boolean
3982 10
                    $value = (bool) $boolErr;
3983
3984
                    // add cell value
3985 10
                    $cell->setValueExplicit($value, DataType::TYPE_BOOL);
3986
3987 10
                    break;
3988
                case 1: // error type
3989
                    $value = Xls\ErrorCode::lookup($boolErr);
3990
3991
                    // add cell value
3992
                    $cell->setValueExplicit($value, DataType::TYPE_ERROR);
3993
3994
                    break;
3995
            }
3996
3997 10
            if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
3998
                // add cell style
3999 9
                $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
4000
            }
4001
        }
4002
    }
4003
4004
    /**
4005
     * Read MULBLANK record
4006
     * This record represents a cell range of empty cells. All
4007
     * cells are located in the same row.
4008
     *
4009
     * --    "OpenOffice.org's Documentation of the Microsoft
4010
     *         Excel File Format"
4011
     */
4012 25
    private function readMulBlank(): void
4013
    {
4014 25
        $length = self::getUInt2d($this->data, $this->pos + 2);
4015 25
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4016
4017
        // move stream pointer to next record
4018 25
        $this->pos += 4 + $length;
4019
4020
        // offset: 0; size: 2; index to row
4021 25
        $row = self::getUInt2d($recordData, 0);
4022
4023
        // offset: 2; size: 2; index to first column
4024 25
        $fc = self::getUInt2d($recordData, 2);
4025
4026
        // offset: 4; size: 2 x nc; list of indexes to XF records
4027
        // add style information
4028 25
        if (!$this->readDataOnly && $this->readEmptyCells) {
4029 24
            for ($i = 0; $i < $length / 2 - 3; ++$i) {
4030 24
                $columnString = Coordinate::stringFromColumnIndex($fc + $i + 1);
4031
4032
                // Read cell?
4033 24
                if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
4034 24
                    $xfIndex = self::getUInt2d($recordData, 4 + 2 * $i);
4035 24
                    if (isset($this->mapCellXfIndex[$xfIndex])) {
4036 24
                        $this->phpSheet->getCell($columnString . ($row + 1))->setXfIndex($this->mapCellXfIndex[$xfIndex]);
4037
                    }
4038
                }
4039
            }
4040
        }
4041
4042
        // offset: 6; size 2; index to last column (not needed)
4043
    }
4044
4045
    /**
4046
     * Read LABEL record
4047
     * This record represents a cell that contains a string. In
4048
     * BIFF8 it is usually replaced by the LABELSST record.
4049
     * Excel still uses this record, if it copies unformatted
4050
     * text cells to the clipboard.
4051
     *
4052
     * --    "OpenOffice.org's Documentation of the Microsoft
4053
     *         Excel File Format"
4054
     */
4055 4
    private function readLabel(): void
4056
    {
4057 4
        $length = self::getUInt2d($this->data, $this->pos + 2);
4058 4
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4059
4060
        // move stream pointer to next record
4061 4
        $this->pos += 4 + $length;
4062
4063
        // offset: 0; size: 2; index to row
4064 4
        $row = self::getUInt2d($recordData, 0);
4065
4066
        // offset: 2; size: 2; index to column
4067 4
        $column = self::getUInt2d($recordData, 2);
4068 4
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
4069
4070
        // Read cell?
4071 4
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
4072
            // offset: 4; size: 2; XF index
4073 4
            $xfIndex = self::getUInt2d($recordData, 4);
4074
4075
            // add cell value
4076
            // todo: what if string is very long? continue record
4077 4
            if ($this->version == self::XLS_BIFF8) {
4078 2
                $string = self::readUnicodeStringLong(substr($recordData, 6));
4079 2
                $value = $string['value'];
4080
            } else {
4081 2
                $string = $this->readByteStringLong(substr($recordData, 6));
4082 2
                $value = $string['value'];
4083
            }
4084 4
            if ($this->readEmptyCells || trim($value) !== '') {
4085 4
                $cell = $this->phpSheet->getCell($columnString . ($row + 1));
4086 4
                $cell->setValueExplicit($value, DataType::TYPE_STRING);
4087
4088 4
                if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
4089
                    // add cell style
4090 4
                    $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
4091
                }
4092
            }
4093
        }
4094
    }
4095
4096
    /**
4097
     * Read BLANK record.
4098
     */
4099 24
    private function readBlank(): void
4100
    {
4101 24
        $length = self::getUInt2d($this->data, $this->pos + 2);
4102 24
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4103
4104
        // move stream pointer to next record
4105 24
        $this->pos += 4 + $length;
4106
4107
        // offset: 0; size: 2; row index
4108 24
        $row = self::getUInt2d($recordData, 0);
4109
4110
        // offset: 2; size: 2; col index
4111 24
        $col = self::getUInt2d($recordData, 2);
4112 24
        $columnString = Coordinate::stringFromColumnIndex($col + 1);
4113
4114
        // Read cell?
4115 24
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
4116
            // offset: 4; size: 2; XF index
4117 24
            $xfIndex = self::getUInt2d($recordData, 4);
4118
4119
            // add style information
4120 24
            if (!$this->readDataOnly && $this->readEmptyCells && isset($this->mapCellXfIndex[$xfIndex])) {
4121 24
                $this->phpSheet->getCell($columnString . ($row + 1))->setXfIndex($this->mapCellXfIndex[$xfIndex]);
4122
            }
4123
        }
4124
    }
4125
4126
    /**
4127
     * Read MSODRAWING record.
4128
     */
4129 16
    private function readMsoDrawing(): void
4130
    {
4131
        //$length = self::getUInt2d($this->data, $this->pos + 2);
4132
4133
        // get spliced record data
4134 16
        $splicedRecordData = $this->getSplicedRecordData();
4135 16
        $recordData = $splicedRecordData['recordData'];
4136
4137 16
        $this->drawingData .= $recordData;
4138
    }
4139
4140
    /**
4141
     * Read OBJ record.
4142
     */
4143 12
    private function readObj(): void
4144
    {
4145 12
        $length = self::getUInt2d($this->data, $this->pos + 2);
4146 12
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4147
4148
        // move stream pointer to next record
4149 12
        $this->pos += 4 + $length;
4150
4151 12
        if ($this->readDataOnly || $this->version != self::XLS_BIFF8) {
4152 1
            return;
4153
        }
4154
4155
        // recordData consists of an array of subrecords looking like this:
4156
        //    ft: 2 bytes; ftCmo type (0x15)
4157
        //    cb: 2 bytes; size in bytes of ftCmo data
4158
        //    ot: 2 bytes; Object Type
4159
        //    id: 2 bytes; Object id number
4160
        //    grbit: 2 bytes; Option Flags
4161
        //    data: var; subrecord data
4162
4163
        // for now, we are just interested in the second subrecord containing the object type
4164 11
        $ftCmoType = self::getUInt2d($recordData, 0);
4165 11
        $cbCmoSize = self::getUInt2d($recordData, 2);
4166 11
        $otObjType = self::getUInt2d($recordData, 4);
4167 11
        $idObjID = self::getUInt2d($recordData, 6);
4168 11
        $grbitOpts = self::getUInt2d($recordData, 6);
4169
4170 11
        $this->objs[] = [
4171 11
            'ftCmoType' => $ftCmoType,
4172 11
            'cbCmoSize' => $cbCmoSize,
4173 11
            'otObjType' => $otObjType,
4174 11
            'idObjID' => $idObjID,
4175 11
            'grbitOpts' => $grbitOpts,
4176 11
        ];
4177 11
        $this->textObjRef = $idObjID;
4178
    }
4179
4180
    /**
4181
     * Read WINDOW2 record.
4182
     */
4183 95
    private function readWindow2(): void
4184
    {
4185 95
        $length = self::getUInt2d($this->data, $this->pos + 2);
4186 95
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4187
4188
        // move stream pointer to next record
4189 95
        $this->pos += 4 + $length;
4190
4191
        // offset: 0; size: 2; option flags
4192 95
        $options = self::getUInt2d($recordData, 0);
4193
4194
        // offset: 2; size: 2; index to first visible row
4195
        //$firstVisibleRow = self::getUInt2d($recordData, 2);
4196
4197
        // offset: 4; size: 2; index to first visible colum
4198
        //$firstVisibleColumn = self::getUInt2d($recordData, 4);
4199 95
        $zoomscaleInPageBreakPreview = 0;
4200 95
        $zoomscaleInNormalView = 0;
4201 95
        if ($this->version === self::XLS_BIFF8) {
4202
            // offset:  8; size: 2; not used
4203
            // offset: 10; size: 2; cached magnification factor in page break preview (in percent); 0 = Default (60%)
4204
            // offset: 12; size: 2; cached magnification factor in normal view (in percent); 0 = Default (100%)
4205
            // offset: 14; size: 4; not used
4206 93
            if (!isset($recordData[10])) {
4207
                $zoomscaleInPageBreakPreview = 0;
4208
            } else {
4209 93
                $zoomscaleInPageBreakPreview = self::getUInt2d($recordData, 10);
4210
            }
4211
4212 93
            if ($zoomscaleInPageBreakPreview === 0) {
4213 90
                $zoomscaleInPageBreakPreview = 60;
4214
            }
4215
4216 93
            if (!isset($recordData[12])) {
4217
                $zoomscaleInNormalView = 0;
4218
            } else {
4219 93
                $zoomscaleInNormalView = self::getUInt2d($recordData, 12);
4220
            }
4221
4222 93
            if ($zoomscaleInNormalView === 0) {
4223 41
                $zoomscaleInNormalView = 100;
4224
            }
4225
        }
4226
4227
        // bit: 1; mask: 0x0002; 0 = do not show gridlines, 1 = show gridlines
4228 95
        $showGridlines = (bool) ((0x0002 & $options) >> 1);
4229 95
        $this->phpSheet->setShowGridlines($showGridlines);
4230
4231
        // bit: 2; mask: 0x0004; 0 = do not show headers, 1 = show headers
4232 95
        $showRowColHeaders = (bool) ((0x0004 & $options) >> 2);
4233 95
        $this->phpSheet->setShowRowColHeaders($showRowColHeaders);
4234
4235
        // bit: 3; mask: 0x0008; 0 = panes are not frozen, 1 = panes are frozen
4236 95
        $this->frozen = (bool) ((0x0008 & $options) >> 3);
4237
4238
        // bit: 6; mask: 0x0040; 0 = columns from left to right, 1 = columns from right to left
4239 95
        $this->phpSheet->setRightToLeft((bool) ((0x0040 & $options) >> 6));
4240
4241
        // bit: 10; mask: 0x0400; 0 = sheet not active, 1 = sheet active
4242 95
        $isActive = (bool) ((0x0400 & $options) >> 10);
4243 95
        if ($isActive) {
4244 91
            $this->spreadsheet->setActiveSheetIndex($this->spreadsheet->getIndex($this->phpSheet));
4245 91
            $this->activeSheetSet = true;
4246
        }
4247
4248
        // bit: 11; mask: 0x0800; 0 = normal view, 1 = page break view
4249 95
        $isPageBreakPreview = (bool) ((0x0800 & $options) >> 11);
4250
4251
        //FIXME: set $firstVisibleRow and $firstVisibleColumn
4252
4253 95
        if ($this->phpSheet->getSheetView()->getView() !== SheetView::SHEETVIEW_PAGE_LAYOUT) {
4254
            //NOTE: this setting is inferior to page layout view(Excel2007-)
4255 95
            $view = $isPageBreakPreview ? SheetView::SHEETVIEW_PAGE_BREAK_PREVIEW : SheetView::SHEETVIEW_NORMAL;
4256 95
            $this->phpSheet->getSheetView()->setView($view);
4257 95
            if ($this->version === self::XLS_BIFF8) {
4258 93
                $zoomScale = $isPageBreakPreview ? $zoomscaleInPageBreakPreview : $zoomscaleInNormalView;
4259 93
                $this->phpSheet->getSheetView()->setZoomScale($zoomScale);
4260 93
                $this->phpSheet->getSheetView()->setZoomScaleNormal($zoomscaleInNormalView);
4261
            }
4262
        }
4263
    }
4264
4265
    /**
4266
     * Read PLV Record(Created by Excel2007 or upper).
4267
     */
4268 82
    private function readPageLayoutView(): void
4269
    {
4270 82
        $length = self::getUInt2d($this->data, $this->pos + 2);
4271 82
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4272
4273
        // move stream pointer to next record
4274 82
        $this->pos += 4 + $length;
4275
4276
        // offset: 0; size: 2; rt
4277
        //->ignore
4278
        //$rt = self::getUInt2d($recordData, 0);
4279
        // offset: 2; size: 2; grbitfr
4280
        //->ignore
4281
        //$grbitFrt = self::getUInt2d($recordData, 2);
4282
        // offset: 4; size: 8; reserved
4283
        //->ignore
4284
4285
        // offset: 12; size 2; zoom scale
4286 82
        $wScalePLV = self::getUInt2d($recordData, 12);
4287
        // offset: 14; size 2; grbit
4288 82
        $grbit = self::getUInt2d($recordData, 14);
4289
4290
        // decomprise grbit
4291 82
        $fPageLayoutView = $grbit & 0x01;
4292
        //$fRulerVisible = ($grbit >> 1) & 0x01; //no support
4293
        //$fWhitespaceHidden = ($grbit >> 3) & 0x01; //no support
4294
4295 82
        if ($fPageLayoutView === 1) {
4296
            $this->phpSheet->getSheetView()->setView(SheetView::SHEETVIEW_PAGE_LAYOUT);
4297
            $this->phpSheet->getSheetView()->setZoomScale($wScalePLV); //set by Excel2007 only if SHEETVIEW_PAGE_LAYOUT
4298
        }
4299
        //otherwise, we cannot know whether SHEETVIEW_PAGE_LAYOUT or SHEETVIEW_PAGE_BREAK_PREVIEW.
4300
    }
4301
4302
    /**
4303
     * Read SCL record.
4304
     */
4305 5
    private function readScl(): void
4306
    {
4307 5
        $length = self::getUInt2d($this->data, $this->pos + 2);
4308 5
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4309
4310
        // move stream pointer to next record
4311 5
        $this->pos += 4 + $length;
4312
4313
        // offset: 0; size: 2; numerator of the view magnification
4314 5
        $numerator = self::getUInt2d($recordData, 0);
4315
4316
        // offset: 2; size: 2; numerator of the view magnification
4317 5
        $denumerator = self::getUInt2d($recordData, 2);
4318
4319
        // set the zoom scale (in percent)
4320 5
        $this->phpSheet->getSheetView()->setZoomScale($numerator * 100 / $denumerator);
4321
    }
4322
4323
    /**
4324
     * Read PANE record.
4325
     */
4326 8
    private function readPane(): void
4327
    {
4328 8
        $length = self::getUInt2d($this->data, $this->pos + 2);
4329 8
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4330
4331
        // move stream pointer to next record
4332 8
        $this->pos += 4 + $length;
4333
4334 8
        if (!$this->readDataOnly) {
4335
            // offset: 0; size: 2; position of vertical split
4336 8
            $px = self::getUInt2d($recordData, 0);
4337
4338
            // offset: 2; size: 2; position of horizontal split
4339 8
            $py = self::getUInt2d($recordData, 2);
4340
4341
            // offset: 4; size: 2; top most visible row in the bottom pane
4342 8
            $rwTop = self::getUInt2d($recordData, 4);
4343
4344
            // offset: 6; size: 2; first visible left column in the right pane
4345 8
            $colLeft = self::getUInt2d($recordData, 6);
4346
4347 8
            if ($this->frozen) {
4348
                // frozen panes
4349 8
                $cell = Coordinate::stringFromColumnIndex($px + 1) . ($py + 1);
4350 8
                $topLeftCell = Coordinate::stringFromColumnIndex($colLeft + 1) . ($rwTop + 1);
4351 8
                $this->phpSheet->freezePane($cell, $topLeftCell);
4352
            }
4353
            // unfrozen panes; split windows; not supported by PhpSpreadsheet core
4354
        }
4355
    }
4356
4357
    /**
4358
     * Read SELECTION record. There is one such record for each pane in the sheet.
4359
     */
4360 92
    private function readSelection(): void
4361
    {
4362 92
        $length = self::getUInt2d($this->data, $this->pos + 2);
4363 92
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4364
4365
        // move stream pointer to next record
4366 92
        $this->pos += 4 + $length;
4367
4368 92
        if (!$this->readDataOnly) {
4369
            // offset: 0; size: 1; pane identifier
4370
            //$paneId = ord($recordData[0]);
4371
4372
            // offset: 1; size: 2; index to row of the active cell
4373
            //$r = self::getUInt2d($recordData, 1);
4374
4375
            // offset: 3; size: 2; index to column of the active cell
4376
            //$c = self::getUInt2d($recordData, 3);
4377
4378
            // offset: 5; size: 2; index into the following cell range list to the
4379
            //  entry that contains the active cell
4380
            //$index = self::getUInt2d($recordData, 5);
4381
4382
            // offset: 7; size: var; cell range address list containing all selected cell ranges
4383 91
            $data = substr($recordData, 7);
4384 91
            $cellRangeAddressList = $this->readBIFF5CellRangeAddressList($data); // note: also BIFF8 uses BIFF5 syntax
4385
4386 91
            $selectedCells = $cellRangeAddressList['cellRangeAddresses'][0];
4387
4388
            // first row '1' + last row '16384' indicates that full column is selected (apparently also in BIFF8!)
4389 91
            if (preg_match('/^([A-Z]+1\:[A-Z]+)16384$/', $selectedCells)) {
4390
                $selectedCells = (string) preg_replace('/^([A-Z]+1\:[A-Z]+)16384$/', '${1}1048576', $selectedCells);
4391
            }
4392
4393
            // first row '1' + last row '65536' indicates that full column is selected
4394 91
            if (preg_match('/^([A-Z]+1\:[A-Z]+)65536$/', $selectedCells)) {
4395
                $selectedCells = (string) preg_replace('/^([A-Z]+1\:[A-Z]+)65536$/', '${1}1048576', $selectedCells);
4396
            }
4397
4398
            // first column 'A' + last column 'IV' indicates that full row is selected
4399 91
            if (preg_match('/^(A\d+\:)IV(\d+)$/', $selectedCells)) {
4400 2
                $selectedCells = (string) preg_replace('/^(A\d+\:)IV(\d+)$/', '${1}XFD${2}', $selectedCells);
4401
            }
4402
4403 91
            $this->phpSheet->setSelectedCells($selectedCells);
4404
        }
4405
    }
4406
4407 17
    private function includeCellRangeFiltered(string $cellRangeAddress): bool
4408
    {
4409 17
        $includeCellRange = true;
4410 17
        if ($this->getReadFilter() !== null) {
4411 17
            $includeCellRange = false;
4412 17
            $rangeBoundaries = Coordinate::getRangeBoundaries($cellRangeAddress);
4413 17
            ++$rangeBoundaries[1][0];
4414 17
            for ($row = $rangeBoundaries[0][1]; $row <= $rangeBoundaries[1][1]; ++$row) {
4415 17
                for ($column = $rangeBoundaries[0][0]; $column != $rangeBoundaries[1][0]; ++$column) {
4416 17
                    if ($this->getReadFilter()->readCell($column, $row, $this->phpSheet->getTitle())) {
4417 17
                        $includeCellRange = true;
4418
4419 17
                        break 2;
4420
                    }
4421
                }
4422
            }
4423
        }
4424
4425 17
        return $includeCellRange;
4426
    }
4427
4428
    /**
4429
     * MERGEDCELLS.
4430
     *
4431
     * This record contains the addresses of merged cell ranges
4432
     * in the current sheet.
4433
     *
4434
     * --    "OpenOffice.org's Documentation of the Microsoft
4435
     *         Excel File Format"
4436
     */
4437 18
    private function readMergedCells(): void
4438
    {
4439 18
        $length = self::getUInt2d($this->data, $this->pos + 2);
4440 18
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4441
4442
        // move stream pointer to next record
4443 18
        $this->pos += 4 + $length;
4444
4445 18
        if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) {
4446 17
            $cellRangeAddressList = $this->readBIFF8CellRangeAddressList($recordData);
4447 17
            foreach ($cellRangeAddressList['cellRangeAddresses'] as $cellRangeAddress) {
4448
                if (
4449 17
                    (str_contains($cellRangeAddress, ':'))
4450 17
                    && ($this->includeCellRangeFiltered($cellRangeAddress))
4451
                ) {
4452 17
                    $this->phpSheet->mergeCells($cellRangeAddress, Worksheet::MERGE_CELL_CONTENT_HIDE);
4453
                }
4454
            }
4455
        }
4456
    }
4457
4458
    /**
4459
     * Read HYPERLINK record.
4460
     */
4461 6
    private function readHyperLink(): void
4462
    {
4463 6
        $length = self::getUInt2d($this->data, $this->pos + 2);
4464 6
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4465
4466
        // move stream pointer forward to next record
4467 6
        $this->pos += 4 + $length;
4468
4469 6
        if (!$this->readDataOnly) {
4470
            // offset: 0; size: 8; cell range address of all cells containing this hyperlink
4471
            try {
4472 6
                $cellRange = $this->readBIFF8CellRangeAddressFixed($recordData);
4473
            } catch (PhpSpreadsheetException) {
4474
                return;
4475
            }
4476
4477
            // offset: 8, size: 16; GUID of StdLink
4478
4479
            // offset: 24, size: 4; unknown value
4480
4481
            // offset: 28, size: 4; option flags
4482
            // bit: 0; mask: 0x00000001; 0 = no link or extant, 1 = file link or URL
4483 6
            $isFileLinkOrUrl = (0x00000001 & self::getUInt2d($recordData, 28)) >> 0;
4484
4485
            // bit: 1; mask: 0x00000002; 0 = relative path, 1 = absolute path or URL
4486
            //$isAbsPathOrUrl = (0x00000001 & self::getUInt2d($recordData, 28)) >> 1;
4487
4488
            // bit: 2 (and 4); mask: 0x00000014; 0 = no description
4489 6
            $hasDesc = (0x00000014 & self::getUInt2d($recordData, 28)) >> 2;
4490
4491
            // bit: 3; mask: 0x00000008; 0 = no text, 1 = has text
4492 6
            $hasText = (0x00000008 & self::getUInt2d($recordData, 28)) >> 3;
4493
4494
            // bit: 7; mask: 0x00000080; 0 = no target frame, 1 = has target frame
4495 6
            $hasFrame = (0x00000080 & self::getUInt2d($recordData, 28)) >> 7;
4496
4497
            // bit: 8; mask: 0x00000100; 0 = file link or URL, 1 = UNC path (inc. server name)
4498 6
            $isUNC = (0x00000100 & self::getUInt2d($recordData, 28)) >> 8;
4499
4500
            // offset within record data
4501 6
            $offset = 32;
4502
4503 6
            if ($hasDesc) {
4504
                // offset: 32; size: var; character count of description text
4505 3
                $dl = self::getInt4d($recordData, 32);
4506
                // offset: 36; size: var; character array of description text, no Unicode string header, always 16-bit characters, zero terminated
4507
                //$desc = self::encodeUTF16(substr($recordData, 36, 2 * ($dl - 1)), false);
4508 3
                $offset += 4 + 2 * $dl;
4509
            }
4510 6
            if ($hasFrame) {
4511
                $fl = self::getInt4d($recordData, $offset);
4512
                $offset += 4 + 2 * $fl;
4513
            }
4514
4515
            // detect type of hyperlink (there are 4 types)
4516 6
            $hyperlinkType = null;
4517
4518 6
            if ($isUNC) {
4519
                $hyperlinkType = 'UNC';
4520 6
            } elseif (!$isFileLinkOrUrl) {
4521 3
                $hyperlinkType = 'workbook';
4522 6
            } elseif (ord($recordData[$offset]) == 0x03) {
4523
                $hyperlinkType = 'local';
4524 6
            } elseif (ord($recordData[$offset]) == 0xE0) {
4525 6
                $hyperlinkType = 'URL';
4526
            }
4527
4528
            switch ($hyperlinkType) {
4529 6
                case 'URL':
4530
                    // section 5.58.2: Hyperlink containing a URL
4531
                    // e.g. http://example.org/index.php
4532
4533
                    // offset: var; size: 16; GUID of URL Moniker
4534 6
                    $offset += 16;
4535
                    // offset: var; size: 4; size (in bytes) of character array of the URL including trailing zero word
4536 6
                    $us = self::getInt4d($recordData, $offset);
4537 6
                    $offset += 4;
4538
                    // offset: var; size: $us; character array of the URL, no Unicode string header, always 16-bit characters, zero-terminated
4539 6
                    $url = self::encodeUTF16(substr($recordData, $offset, $us - 2), false);
4540 6
                    $nullOffset = strpos($url, chr(0x00));
4541 6
                    if ($nullOffset) {
4542 3
                        $url = substr($url, 0, $nullOffset);
4543
                    }
4544 6
                    $url .= $hasText ? '#' : '';
4545 6
                    $offset += $us;
4546
4547 6
                    break;
4548 3
                case 'local':
4549
                    // section 5.58.3: Hyperlink to local file
4550
                    // examples:
4551
                    //   mydoc.txt
4552
                    //   ../../somedoc.xls#Sheet!A1
4553
4554
                    // offset: var; size: 16; GUI of File Moniker
4555
                    $offset += 16;
4556
4557
                    // offset: var; size: 2; directory up-level count.
4558
                    $upLevelCount = self::getUInt2d($recordData, $offset);
4559
                    $offset += 2;
4560
4561
                    // offset: var; size: 4; character count of the shortened file path and name, including trailing zero word
4562
                    $sl = self::getInt4d($recordData, $offset);
4563
                    $offset += 4;
4564
4565
                    // offset: var; size: sl; character array of the shortened file path and name in 8.3-DOS-format (compressed Unicode string)
4566
                    $shortenedFilePath = substr($recordData, $offset, $sl);
4567
                    $shortenedFilePath = self::encodeUTF16($shortenedFilePath, true);
4568
                    $shortenedFilePath = substr($shortenedFilePath, 0, -1); // remove trailing zero
4569
4570
                    $offset += $sl;
4571
4572
                    // offset: var; size: 24; unknown sequence
4573
                    $offset += 24;
4574
4575
                    // extended file path
4576
                    // offset: var; size: 4; size of the following file link field including string lenth mark
4577
                    $sz = self::getInt4d($recordData, $offset);
4578
                    $offset += 4;
4579
4580
                    $extendedFilePath = '';
4581
                    // only present if $sz > 0
4582
                    if ($sz > 0) {
4583
                        // offset: var; size: 4; size of the character array of the extended file path and name
4584
                        $xl = self::getInt4d($recordData, $offset);
4585
                        $offset += 4;
4586
4587
                        // offset: var; size 2; unknown
4588
                        $offset += 2;
4589
4590
                        // offset: var; size $xl; character array of the extended file path and name.
4591
                        $extendedFilePath = substr($recordData, $offset, $xl);
4592
                        $extendedFilePath = self::encodeUTF16($extendedFilePath, false);
4593
                        $offset += $xl;
4594
                    }
4595
4596
                    // construct the path
4597
                    $url = str_repeat('..\\', $upLevelCount);
4598
                    $url .= ($sz > 0) ? $extendedFilePath : $shortenedFilePath; // use extended path if available
4599
                    $url .= $hasText ? '#' : '';
4600
4601
                    break;
4602 3
                case 'UNC':
4603
                    // section 5.58.4: Hyperlink to a File with UNC (Universal Naming Convention) Path
4604
                    // todo: implement
4605
                    return;
4606 3
                case 'workbook':
4607
                    // section 5.58.5: Hyperlink to the Current Workbook
4608
                    // e.g. Sheet2!B1:C2, stored in text mark field
4609 3
                    $url = 'sheet://';
4610
4611 3
                    break;
4612
                default:
4613
                    return;
4614
            }
4615
4616 6
            if ($hasText) {
4617
                // offset: var; size: 4; character count of text mark including trailing zero word
4618 3
                $tl = self::getInt4d($recordData, $offset);
4619 3
                $offset += 4;
4620
                // offset: var; size: var; character array of the text mark without the # sign, no Unicode header, always 16-bit characters, zero-terminated
4621 3
                $text = self::encodeUTF16(substr($recordData, $offset, 2 * ($tl - 1)), false);
4622 3
                $url .= $text;
4623
            }
4624
4625
            // apply the hyperlink to all the relevant cells
4626 6
            foreach (Coordinate::extractAllCellReferencesInRange($cellRange) as $coordinate) {
4627 6
                $this->phpSheet->getCell($coordinate)->getHyperLink()->setUrl($url);
4628
            }
4629
        }
4630
    }
4631
4632
    /**
4633
     * Read DATAVALIDATIONS record.
4634
     */
4635 3
    private function readDataValidations(): void
4636
    {
4637 3
        $length = self::getUInt2d($this->data, $this->pos + 2);
4638
        //$recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4639
4640
        // move stream pointer forward to next record
4641 3
        $this->pos += 4 + $length;
4642
    }
4643
4644
    /**
4645
     * Read DATAVALIDATION record.
4646
     */
4647 3
    private function readDataValidation(): void
4648
    {
4649 3
        $length = self::getUInt2d($this->data, $this->pos + 2);
4650 3
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4651
4652
        // move stream pointer forward to next record
4653 3
        $this->pos += 4 + $length;
4654
4655 3
        if ($this->readDataOnly) {
4656
            return;
4657
        }
4658
4659
        // offset: 0; size: 4; Options
4660 3
        $options = self::getInt4d($recordData, 0);
4661
4662
        // bit: 0-3; mask: 0x0000000F; type
4663 3
        $type = (0x0000000F & $options) >> 0;
4664 3
        $type = Xls\DataValidationHelper::type($type);
4665
4666
        // bit: 4-6; mask: 0x00000070; error type
4667 3
        $errorStyle = (0x00000070 & $options) >> 4;
4668 3
        $errorStyle = Xls\DataValidationHelper::errorStyle($errorStyle);
4669
4670
        // bit: 7; mask: 0x00000080; 1= formula is explicit (only applies to list)
4671
        // I have only seen cases where this is 1
4672
        //$explicitFormula = (0x00000080 & $options) >> 7;
4673
4674
        // bit: 8; mask: 0x00000100; 1= empty cells allowed
4675 3
        $allowBlank = (0x00000100 & $options) >> 8;
4676
4677
        // bit: 9; mask: 0x00000200; 1= suppress drop down arrow in list type validity
4678 3
        $suppressDropDown = (0x00000200 & $options) >> 9;
4679
4680
        // bit: 18; mask: 0x00040000; 1= show prompt box if cell selected
4681 3
        $showInputMessage = (0x00040000 & $options) >> 18;
4682
4683
        // bit: 19; mask: 0x00080000; 1= show error box if invalid values entered
4684 3
        $showErrorMessage = (0x00080000 & $options) >> 19;
4685
4686
        // bit: 20-23; mask: 0x00F00000; condition operator
4687 3
        $operator = (0x00F00000 & $options) >> 20;
4688 3
        $operator = Xls\DataValidationHelper::operator($operator);
4689
4690 3
        if ($type === null || $errorStyle === null || $operator === null) {
4691
            return;
4692
        }
4693
4694
        // offset: 4; size: var; title of the prompt box
4695 3
        $offset = 4;
4696 3
        $string = self::readUnicodeStringLong(substr($recordData, $offset));
4697 3
        $promptTitle = $string['value'] !== chr(0) ? $string['value'] : '';
4698 3
        $offset += $string['size'];
4699
4700
        // offset: var; size: var; title of the error box
4701 3
        $string = self::readUnicodeStringLong(substr($recordData, $offset));
4702 3
        $errorTitle = $string['value'] !== chr(0) ? $string['value'] : '';
4703 3
        $offset += $string['size'];
4704
4705
        // offset: var; size: var; text of the prompt box
4706 3
        $string = self::readUnicodeStringLong(substr($recordData, $offset));
4707 3
        $prompt = $string['value'] !== chr(0) ? $string['value'] : '';
4708 3
        $offset += $string['size'];
4709
4710
        // offset: var; size: var; text of the error box
4711 3
        $string = self::readUnicodeStringLong(substr($recordData, $offset));
4712 3
        $error = $string['value'] !== chr(0) ? $string['value'] : '';
4713 3
        $offset += $string['size'];
4714
4715
        // offset: var; size: 2; size of the formula data for the first condition
4716 3
        $sz1 = self::getUInt2d($recordData, $offset);
4717 3
        $offset += 2;
4718
4719
        // offset: var; size: 2; not used
4720 3
        $offset += 2;
4721
4722
        // offset: var; size: $sz1; formula data for first condition (without size field)
4723 3
        $formula1 = substr($recordData, $offset, $sz1);
4724 3
        $formula1 = pack('v', $sz1) . $formula1; // prepend the length
4725
4726
        try {
4727 3
            $formula1 = $this->getFormulaFromStructure($formula1);
4728
4729
            // in list type validity, null characters are used as item separators
4730 3
            if ($type == DataValidation::TYPE_LIST) {
4731 3
                $formula1 = str_replace(chr(0), ',', $formula1);
4732
            }
4733
        } catch (PhpSpreadsheetException) {
4734
            return;
4735
        }
4736 3
        $offset += $sz1;
4737
4738
        // offset: var; size: 2; size of the formula data for the first condition
4739 3
        $sz2 = self::getUInt2d($recordData, $offset);
4740 3
        $offset += 2;
4741
4742
        // offset: var; size: 2; not used
4743 3
        $offset += 2;
4744
4745
        // offset: var; size: $sz2; formula data for second condition (without size field)
4746 3
        $formula2 = substr($recordData, $offset, $sz2);
4747 3
        $formula2 = pack('v', $sz2) . $formula2; // prepend the length
4748
4749
        try {
4750 3
            $formula2 = $this->getFormulaFromStructure($formula2);
4751
        } catch (PhpSpreadsheetException) {
4752
            return;
4753
        }
4754 3
        $offset += $sz2;
4755
4756
        // offset: var; size: var; cell range address list with
4757 3
        $cellRangeAddressList = $this->readBIFF8CellRangeAddressList(substr($recordData, $offset));
4758 3
        $cellRangeAddresses = $cellRangeAddressList['cellRangeAddresses'];
4759
4760 3
        foreach ($cellRangeAddresses as $cellRange) {
4761 3
            $stRange = $this->phpSheet->shrinkRangeToFit($cellRange);
4762 3
            foreach (Coordinate::extractAllCellReferencesInRange($stRange) as $coordinate) {
4763 3
                $objValidation = $this->phpSheet->getCell($coordinate)->getDataValidation();
4764 3
                $objValidation->setType($type);
4765 3
                $objValidation->setErrorStyle($errorStyle);
4766 3
                $objValidation->setAllowBlank((bool) $allowBlank);
4767 3
                $objValidation->setShowInputMessage((bool) $showInputMessage);
4768 3
                $objValidation->setShowErrorMessage((bool) $showErrorMessage);
4769 3
                $objValidation->setShowDropDown(!$suppressDropDown);
4770 3
                $objValidation->setOperator($operator);
4771 3
                $objValidation->setErrorTitle($errorTitle);
4772 3
                $objValidation->setError($error);
4773 3
                $objValidation->setPromptTitle($promptTitle);
4774 3
                $objValidation->setPrompt($prompt);
4775 3
                $objValidation->setFormula1($formula1);
4776 3
                $objValidation->setFormula2($formula2);
4777
            }
4778
        }
4779
    }
4780
4781
    /**
4782
     * Read SHEETLAYOUT record. Stores sheet tab color information.
4783
     */
4784 5
    private function readSheetLayout(): void
4785
    {
4786 5
        $length = self::getUInt2d($this->data, $this->pos + 2);
4787 5
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4788
4789
        // move stream pointer to next record
4790 5
        $this->pos += 4 + $length;
4791
4792 5
        if (!$this->readDataOnly) {
4793
            // offset: 0; size: 2; repeated record identifier 0x0862
4794
4795
            // offset: 2; size: 10; not used
4796
4797
            // offset: 12; size: 4; size of record data
4798
            // Excel 2003 uses size of 0x14 (documented), Excel 2007 uses size of 0x28 (not documented?)
4799 5
            $sz = self::getInt4d($recordData, 12);
4800
4801
            switch ($sz) {
4802 5
                case 0x14:
4803
                    // offset: 16; size: 2; color index for sheet tab
4804 1
                    $colorIndex = self::getUInt2d($recordData, 16);
4805 1
                    $color = Xls\Color::map($colorIndex, $this->palette, $this->version);
4806 1
                    $this->phpSheet->getTabColor()->setRGB($color['rgb']);
4807
4808 1
                    break;
4809 4
                case 0x28:
4810
                    // TODO: Investigate structure for .xls SHEETLAYOUT record as saved by MS Office Excel 2007
4811 4
                    return;
4812
            }
4813
        }
4814
    }
4815
4816
    /**
4817
     * Read SHEETPROTECTION record (FEATHEADR).
4818
     */
4819 86
    private function readSheetProtection(): void
4820
    {
4821 86
        $length = self::getUInt2d($this->data, $this->pos + 2);
4822 86
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4823
4824
        // move stream pointer to next record
4825 86
        $this->pos += 4 + $length;
4826
4827 86
        if ($this->readDataOnly) {
4828 1
            return;
4829
        }
4830
4831
        // offset: 0; size: 2; repeated record header
4832
4833
        // offset: 2; size: 2; FRT cell reference flag (=0 currently)
4834
4835
        // offset: 4; size: 8; Currently not used and set to 0
4836
4837
        // offset: 12; size: 2; Shared feature type index (2=Enhanced Protetion, 4=SmartTag)
4838 85
        $isf = self::getUInt2d($recordData, 12);
4839 85
        if ($isf != 2) {
4840
            return;
4841
        }
4842
4843
        // offset: 14; size: 1; =1 since this is a feat header
4844
4845
        // offset: 15; size: 4; size of rgbHdrSData
4846
4847
        // rgbHdrSData, assume "Enhanced Protection"
4848
        // offset: 19; size: 2; option flags
4849 85
        $options = self::getUInt2d($recordData, 19);
4850
4851
        // bit: 0; mask 0x0001; 1 = user may edit objects, 0 = users must not edit objects
4852
        // Note - do not negate $bool
4853 85
        $bool = (0x0001 & $options) >> 0;
4854 85
        $this->phpSheet->getProtection()->setObjects((bool) $bool);
4855
4856
        // bit: 1; mask 0x0002; edit scenarios
4857
        // Note - do not negate $bool
4858 85
        $bool = (0x0002 & $options) >> 1;
4859 85
        $this->phpSheet->getProtection()->setScenarios((bool) $bool);
4860
4861
        // bit: 2; mask 0x0004; format cells
4862 85
        $bool = (0x0004 & $options) >> 2;
4863 85
        $this->phpSheet->getProtection()->setFormatCells(!$bool);
4864
4865
        // bit: 3; mask 0x0008; format columns
4866 85
        $bool = (0x0008 & $options) >> 3;
4867 85
        $this->phpSheet->getProtection()->setFormatColumns(!$bool);
4868
4869
        // bit: 4; mask 0x0010; format rows
4870 85
        $bool = (0x0010 & $options) >> 4;
4871 85
        $this->phpSheet->getProtection()->setFormatRows(!$bool);
4872
4873
        // bit: 5; mask 0x0020; insert columns
4874 85
        $bool = (0x0020 & $options) >> 5;
4875 85
        $this->phpSheet->getProtection()->setInsertColumns(!$bool);
4876
4877
        // bit: 6; mask 0x0040; insert rows
4878 85
        $bool = (0x0040 & $options) >> 6;
4879 85
        $this->phpSheet->getProtection()->setInsertRows(!$bool);
4880
4881
        // bit: 7; mask 0x0080; insert hyperlinks
4882 85
        $bool = (0x0080 & $options) >> 7;
4883 85
        $this->phpSheet->getProtection()->setInsertHyperlinks(!$bool);
4884
4885
        // bit: 8; mask 0x0100; delete columns
4886 85
        $bool = (0x0100 & $options) >> 8;
4887 85
        $this->phpSheet->getProtection()->setDeleteColumns(!$bool);
4888
4889
        // bit: 9; mask 0x0200; delete rows
4890 85
        $bool = (0x0200 & $options) >> 9;
4891 85
        $this->phpSheet->getProtection()->setDeleteRows(!$bool);
4892
4893
        // bit: 10; mask 0x0400; select locked cells
4894
        // Note that this is opposite of most of above.
4895 85
        $bool = (0x0400 & $options) >> 10;
4896 85
        $this->phpSheet->getProtection()->setSelectLockedCells((bool) $bool);
4897
4898
        // bit: 11; mask 0x0800; sort cell range
4899 85
        $bool = (0x0800 & $options) >> 11;
4900 85
        $this->phpSheet->getProtection()->setSort(!$bool);
4901
4902
        // bit: 12; mask 0x1000; auto filter
4903 85
        $bool = (0x1000 & $options) >> 12;
4904 85
        $this->phpSheet->getProtection()->setAutoFilter(!$bool);
4905
4906
        // bit: 13; mask 0x2000; pivot tables
4907 85
        $bool = (0x2000 & $options) >> 13;
4908 85
        $this->phpSheet->getProtection()->setPivotTables(!$bool);
4909
4910
        // bit: 14; mask 0x4000; select unlocked cells
4911
        // Note that this is opposite of most of above.
4912 85
        $bool = (0x4000 & $options) >> 14;
4913 85
        $this->phpSheet->getProtection()->setSelectUnlockedCells((bool) $bool);
4914
4915
        // offset: 21; size: 2; not used
4916
    }
4917
4918
    /**
4919
     * Read RANGEPROTECTION record
4920
     * Reading of this record is based on Microsoft Office Excel 97-2000 Binary File Format Specification,
4921
     * where it is referred to as FEAT record.
4922
     */
4923 1
    private function readRangeProtection(): void
4924
    {
4925 1
        $length = self::getUInt2d($this->data, $this->pos + 2);
4926 1
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4927
4928
        // move stream pointer to next record
4929 1
        $this->pos += 4 + $length;
4930
4931
        // local pointer in record data
4932 1
        $offset = 0;
4933
4934 1
        if (!$this->readDataOnly) {
4935 1
            $offset += 12;
4936
4937
            // offset: 12; size: 2; shared feature type, 2 = enhanced protection, 4 = smart tag
4938 1
            $isf = self::getUInt2d($recordData, 12);
4939 1
            if ($isf != 2) {
4940
                // we only read FEAT records of type 2
4941
                return;
4942
            }
4943 1
            $offset += 2;
4944
4945 1
            $offset += 5;
4946
4947
            // offset: 19; size: 2; count of ref ranges this feature is on
4948 1
            $cref = self::getUInt2d($recordData, 19);
4949 1
            $offset += 2;
4950
4951 1
            $offset += 6;
4952
4953
            // offset: 27; size: 8 * $cref; list of cell ranges (like in hyperlink record)
4954 1
            $cellRanges = [];
4955 1
            for ($i = 0; $i < $cref; ++$i) {
4956
                try {
4957 1
                    $cellRange = $this->readBIFF8CellRangeAddressFixed(substr($recordData, 27 + 8 * $i, 8));
4958
                } catch (PhpSpreadsheetException) {
4959
                    return;
4960
                }
4961 1
                $cellRanges[] = $cellRange;
4962 1
                $offset += 8;
4963
            }
4964
4965
            // offset: var; size: var; variable length of feature specific data
4966
            //$rgbFeat = substr($recordData, $offset);
4967 1
            $offset += 4;
4968
4969
            // offset: var; size: 4; the encrypted password (only 16-bit although field is 32-bit)
4970 1
            $wPassword = self::getInt4d($recordData, $offset);
4971 1
            $offset += 4;
4972
4973
            // Apply range protection to sheet
4974 1
            if ($cellRanges) {
4975 1
                $this->phpSheet->protectCells(implode(' ', $cellRanges), strtoupper(dechex($wPassword)), true);
4976
            }
4977
        }
4978
    }
4979
4980
    /**
4981
     * Read a free CONTINUE record. Free CONTINUE record may be a camouflaged MSODRAWING record
4982
     * When MSODRAWING data on a sheet exceeds 8224 bytes, CONTINUE records are used instead. Undocumented.
4983
     * In this case, we must treat the CONTINUE record as a MSODRAWING record.
4984
     */
4985 1
    private function readContinue(): void
4986
    {
4987 1
        $length = self::getUInt2d($this->data, $this->pos + 2);
4988 1
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4989
4990
        // check if we are reading drawing data
4991
        // this is in case a free CONTINUE record occurs in other circumstances we are unaware of
4992 1
        if ($this->drawingData == '') {
4993
            // move stream pointer to next record
4994 1
            $this->pos += 4 + $length;
4995
4996 1
            return;
4997
        }
4998
4999
        // check if record data is at least 4 bytes long, otherwise there is no chance this is MSODRAWING data
5000
        if ($length < 4) {
5001
            // move stream pointer to next record
5002
            $this->pos += 4 + $length;
5003
5004
            return;
5005
        }
5006
5007
        // dirty check to see if CONTINUE record could be a camouflaged MSODRAWING record
5008
        // look inside CONTINUE record to see if it looks like a part of an Escher stream
5009
        // we know that Escher stream may be split at least at
5010
        //        0xF003 MsofbtSpgrContainer
5011
        //        0xF004 MsofbtSpContainer
5012
        //        0xF00D MsofbtClientTextbox
5013
        $validSplitPoints = [0xF003, 0xF004, 0xF00D]; // add identifiers if we find more
5014
5015
        $splitPoint = self::getUInt2d($recordData, 2);
5016
        if (in_array($splitPoint, $validSplitPoints)) {
5017
            // get spliced record data (and move pointer to next record)
5018
            $splicedRecordData = $this->getSplicedRecordData();
5019
            $this->drawingData .= $splicedRecordData['recordData'];
5020
5021
            return;
5022
        }
5023
5024
        // move stream pointer to next record
5025
        $this->pos += 4 + $length;
5026
    }
5027
5028
    /**
5029
     * Reads a record from current position in data stream and continues reading data as long as CONTINUE
5030
     * records are found. Splices the record data pieces and returns the combined string as if record data
5031
     * is in one piece.
5032
     * Moves to next current position in data stream to start of next record different from a CONtINUE record.
5033
     */
5034 92
    private function getSplicedRecordData(): array
5035
    {
5036 92
        $data = '';
5037 92
        $spliceOffsets = [];
5038
5039 92
        $i = 0;
5040 92
        $spliceOffsets[0] = 0;
5041
5042
        do {
5043 92
            ++$i;
5044
5045
            // offset: 0; size: 2; identifier
5046
            //$identifier = self::getUInt2d($this->data, $this->pos);
5047
            // offset: 2; size: 2; length
5048 92
            $length = self::getUInt2d($this->data, $this->pos + 2);
5049 92
            $data .= $this->readRecordData($this->data, $this->pos + 4, $length);
5050
5051 92
            $spliceOffsets[$i] = $spliceOffsets[$i - 1] + $length;
5052
5053 92
            $this->pos += 4 + $length;
5054 92
            $nextIdentifier = self::getUInt2d($this->data, $this->pos);
5055 92
        } while ($nextIdentifier == self::XLS_TYPE_CONTINUE);
5056
5057 92
        return [
5058 92
            'recordData' => $data,
5059 92
            'spliceOffsets' => $spliceOffsets,
5060 92
        ];
5061
    }
5062
5063
    /**
5064
     * Convert formula structure into human readable Excel formula like 'A3+A5*5'.
5065
     *
5066
     * @param string $formulaStructure The complete binary data for the formula
5067
     * @param string $baseCell Base cell, only needed when formula contains tRefN tokens, e.g. with shared formulas
5068
     *
5069
     * @return string Human readable formula
5070
     */
5071 47
    private function getFormulaFromStructure(string $formulaStructure, string $baseCell = 'A1'): string
5072
    {
5073
        // offset: 0; size: 2; size of the following formula data
5074 47
        $sz = self::getUInt2d($formulaStructure, 0);
5075
5076
        // offset: 2; size: sz
5077 47
        $formulaData = substr($formulaStructure, 2, $sz);
5078
5079
        // offset: 2 + sz; size: variable (optional)
5080 47
        if (strlen($formulaStructure) > 2 + $sz) {
5081
            $additionalData = substr($formulaStructure, 2 + $sz);
5082
        } else {
5083 47
            $additionalData = '';
5084
        }
5085
5086 47
        return $this->getFormulaFromData($formulaData, $additionalData, $baseCell);
5087
    }
5088
5089
    /**
5090
     * Take formula data and additional data for formula and return human readable formula.
5091
     *
5092
     * @param string $formulaData The binary data for the formula itself
5093
     * @param string $additionalData Additional binary data going with the formula
5094
     * @param string $baseCell Base cell, only needed when formula contains tRefN tokens, e.g. with shared formulas
5095
     *
5096
     * @return string Human readable formula
5097
     */
5098 47
    private function getFormulaFromData(string $formulaData, string $additionalData = '', string $baseCell = 'A1'): string
5099
    {
5100
        // start parsing the formula data
5101 47
        $tokens = [];
5102
5103 47
        while ($formulaData !== '' && $token = $this->getNextToken($formulaData, $baseCell)) {
5104 47
            $tokens[] = $token;
5105 47
            $formulaData = substr($formulaData, $token['size']);
5106
        }
5107
5108 47
        $formulaString = $this->createFormulaFromTokens($tokens, $additionalData);
5109
5110 47
        return $formulaString;
5111
    }
5112
5113
    /**
5114
     * Take array of tokens together with additional data for formula and return human readable formula.
5115
     *
5116
     * @param string $additionalData Additional binary data going with the formula
5117
     *
5118
     * @return string Human readable formula
5119
     */
5120 47
    private function createFormulaFromTokens(array $tokens, string $additionalData): string
5121
    {
5122
        // empty formula?
5123 47
        if (empty($tokens)) {
5124 3
            return '';
5125
        }
5126
5127 47
        $formulaStrings = [];
5128 47
        foreach ($tokens as $token) {
5129
            // initialize spaces
5130 47
            $space0 ??= ''; // spaces before next token, not tParen
1 ignored issue
show
Comprehensibility Best Practice introduced by
The variable $space0 seems to be defined later in this foreach loop on line 5130. Are you sure it is defined here?
Loading history...
5131 47
            $space1 ??= ''; // carriage returns before next token, not tParen
1 ignored issue
show
Comprehensibility Best Practice introduced by
The variable $space1 seems to be defined later in this foreach loop on line 5131. Are you sure it is defined here?
Loading history...
5132 47
            $space2 ??= ''; // spaces before opening parenthesis
1 ignored issue
show
Comprehensibility Best Practice introduced by
The variable $space2 seems to be defined later in this foreach loop on line 5132. Are you sure it is defined here?
Loading history...
5133 47
            $space3 ??= ''; // carriage returns before opening parenthesis
1 ignored issue
show
Comprehensibility Best Practice introduced by
The variable $space3 seems to be defined later in this foreach loop on line 5133. Are you sure it is defined here?
Loading history...
5134 47
            $space4 ??= ''; // spaces before closing parenthesis
1 ignored issue
show
Comprehensibility Best Practice introduced by
The variable $space4 seems to be defined later in this foreach loop on line 5134. Are you sure it is defined here?
Loading history...
5135 47
            $space5 ??= ''; // carriage returns before closing parenthesis
1 ignored issue
show
Comprehensibility Best Practice introduced by
The variable $space5 seems to be defined later in this foreach loop on line 5135. Are you sure it is defined here?
Loading history...
5136
5137 47
            switch ($token['name']) {
5138 47
                case 'tAdd': // addition
5139 47
                case 'tConcat': // addition
5140 47
                case 'tDiv': // division
5141 47
                case 'tEQ': // equality
5142 47
                case 'tGE': // greater than or equal
5143 47
                case 'tGT': // greater than
5144 47
                case 'tIsect': // intersection
5145 47
                case 'tLE': // less than or equal
5146 47
                case 'tList': // less than or equal
5147 47
                case 'tLT': // less than
5148 47
                case 'tMul': // multiplication
5149 47
                case 'tNE': // multiplication
5150 47
                case 'tPower': // power
5151 47
                case 'tRange': // range
5152 47
                case 'tSub': // subtraction
5153 28
                    $op2 = array_pop($formulaStrings);
5154 28
                    $op1 = array_pop($formulaStrings);
5155 28
                    $formulaStrings[] = "$op1$space1$space0{$token['data']}$op2";
5156 28
                    unset($space0, $space1);
5157
5158 28
                    break;
5159 47
                case 'tUplus': // unary plus
5160 47
                case 'tUminus': // unary minus
5161 3
                    $op = array_pop($formulaStrings);
5162 3
                    $formulaStrings[] = "$space1$space0{$token['data']}$op";
5163 3
                    unset($space0, $space1);
5164
5165 3
                    break;
5166 47
                case 'tPercent': // percent sign
5167 1
                    $op = array_pop($formulaStrings);
5168 1
                    $formulaStrings[] = "$op$space1$space0{$token['data']}";
5169 1
                    unset($space0, $space1);
5170
5171 1
                    break;
5172 47
                case 'tAttrVolatile': // indicates volatile function
5173 47
                case 'tAttrIf':
5174 47
                case 'tAttrSkip':
5175 47
                case 'tAttrChoose':
5176
                    // token is only important for Excel formula evaluator
5177
                    // do nothing
5178 3
                    break;
5179 47
                case 'tAttrSpace': // space / carriage return
5180
                    // space will be used when next token arrives, do not alter formulaString stack
5181
                    switch ($token['data']['spacetype']) {
5182
                        case 'type0':
5183
                            $space0 = str_repeat(' ', $token['data']['spacecount']);
5184
5185
                            break;
5186
                        case 'type1':
5187
                            $space1 = str_repeat("\n", $token['data']['spacecount']);
5188
5189
                            break;
5190
                        case 'type2':
5191
                            $space2 = str_repeat(' ', $token['data']['spacecount']);
5192
5193
                            break;
5194
                        case 'type3':
5195
                            $space3 = str_repeat("\n", $token['data']['spacecount']);
5196
5197
                            break;
5198
                        case 'type4':
5199
                            $space4 = str_repeat(' ', $token['data']['spacecount']);
5200
5201
                            break;
5202
                        case 'type5':
5203
                            $space5 = str_repeat("\n", $token['data']['spacecount']);
5204
5205
                            break;
5206
                    }
5207
5208
                    break;
5209 47
                case 'tAttrSum': // SUM function with one parameter
5210 12
                    $op = array_pop($formulaStrings);
5211 12
                    $formulaStrings[] = "{$space1}{$space0}SUM($op)";
5212 12
                    unset($space0, $space1);
5213
5214 12
                    break;
5215 47
                case 'tFunc': // function with fixed number of arguments
5216 47
                case 'tFuncV': // function with variable number of arguments
5217 31
                    if ($token['data']['function'] != '') {
5218
                        // normal function
5219 31
                        $ops = []; // array of operators
5220 31
                        for ($i = 0; $i < $token['data']['args']; ++$i) {
5221 23
                            $ops[] = array_pop($formulaStrings);
5222
                        }
5223 31
                        $ops = array_reverse($ops);
5224 31
                        $formulaStrings[] = "$space1$space0{$token['data']['function']}(" . implode(',', $ops) . ')';
5225 31
                        unset($space0, $space1);
5226
                    } else {
5227
                        // add-in function
5228
                        $ops = []; // array of operators
5229
                        for ($i = 0; $i < $token['data']['args'] - 1; ++$i) {
5230
                            $ops[] = array_pop($formulaStrings);
5231
                        }
5232
                        $ops = array_reverse($ops);
5233
                        $function = array_pop($formulaStrings);
5234
                        $formulaStrings[] = "$space1$space0$function(" . implode(',', $ops) . ')';
5235
                        unset($space0, $space1);
5236
                    }
5237
5238 31
                    break;
5239 47
                case 'tParen': // parenthesis
5240 1
                    $expression = array_pop($formulaStrings);
5241 1
                    $formulaStrings[] = "$space3$space2($expression$space5$space4)";
5242 1
                    unset($space2, $space3, $space4, $space5);
5243
5244 1
                    break;
5245 47
                case 'tArray': // array constant
5246
                    $constantArray = self::readBIFF8ConstantArray($additionalData);
5247
                    $formulaStrings[] = $space1 . $space0 . $constantArray['value'];
5248
                    $additionalData = substr($additionalData, $constantArray['size']); // bite of chunk of additional data
5249
                    unset($space0, $space1);
5250
5251
                    break;
5252 47
                case 'tMemArea':
5253
                    // bite off chunk of additional data
5254
                    $cellRangeAddressList = $this->readBIFF8CellRangeAddressList($additionalData);
5255
                    $additionalData = substr($additionalData, $cellRangeAddressList['size']);
5256
                    $formulaStrings[] = "$space1$space0{$token['data']}";
5257
                    unset($space0, $space1);
5258
5259
                    break;
5260 47
                case 'tArea': // cell range address
5261 45
                case 'tBool': // boolean
5262 44
                case 'tErr': // error code
5263 43
                case 'tInt': // integer
5264 34
                case 'tMemErr':
5265 34
                case 'tMemFunc':
5266 34
                case 'tMissArg':
5267 34
                case 'tName':
5268 34
                case 'tNameX':
5269 34
                case 'tNum': // number
5270 34
                case 'tRef': // single cell reference
5271 29
                case 'tRef3d': // 3d cell reference
5272 27
                case 'tArea3d': // 3d cell range reference
5273 19
                case 'tRefN':
5274 19
                case 'tAreaN':
5275 19
                case 'tStr': // string
5276 47
                    $formulaStrings[] = "$space1$space0{$token['data']}";
5277 47
                    unset($space0, $space1);
5278
5279 47
                    break;
5280
            }
5281
        }
5282 47
        $formulaString = $formulaStrings[0];
5283
5284 47
        return $formulaString;
5285
    }
5286
5287
    /**
5288
     * Fetch next token from binary formula data.
5289
     *
5290
     * @param string $formulaData Formula data
5291
     * @param string $baseCell Base cell, only needed when formula contains tRefN tokens, e.g. with shared formulas
5292
     */
5293 47
    private function getNextToken(string $formulaData, string $baseCell = 'A1'): array
5294
    {
5295
        // offset: 0; size: 1; token id
5296 47
        $id = ord($formulaData[0]); // token id
5297 47
        $name = false; // initialize token name
5298
5299
        switch ($id) {
5300 47
            case 0x03:
5301 10
                $name = 'tAdd';
5302 10
                $size = 1;
5303 10
                $data = '+';
5304
5305 10
                break;
5306 47
            case 0x04:
5307 9
                $name = 'tSub';
5308 9
                $size = 1;
5309 9
                $data = '-';
5310
5311 9
                break;
5312 47
            case 0x05:
5313 4
                $name = 'tMul';
5314 4
                $size = 1;
5315 4
                $data = '*';
5316
5317 4
                break;
5318 47
            case 0x06:
5319 12
                $name = 'tDiv';
5320 12
                $size = 1;
5321 12
                $data = '/';
5322
5323 12
                break;
5324 47
            case 0x07:
5325 1
                $name = 'tPower';
5326 1
                $size = 1;
5327 1
                $data = '^';
5328
5329 1
                break;
5330 47
            case 0x08:
5331 4
                $name = 'tConcat';
5332 4
                $size = 1;
5333 4
                $data = '&';
5334
5335 4
                break;
5336 47
            case 0x09:
5337 1
                $name = 'tLT';
5338 1
                $size = 1;
5339 1
                $data = '<';
5340
5341 1
                break;
5342 47
            case 0x0A:
5343 1
                $name = 'tLE';
5344 1
                $size = 1;
5345 1
                $data = '<=';
5346
5347 1
                break;
5348 47
            case 0x0B:
5349 3
                $name = 'tEQ';
5350 3
                $size = 1;
5351 3
                $data = '=';
5352
5353 3
                break;
5354 47
            case 0x0C:
5355 1
                $name = 'tGE';
5356 1
                $size = 1;
5357 1
                $data = '>=';
5358
5359 1
                break;
5360 47
            case 0x0D:
5361 1
                $name = 'tGT';
5362 1
                $size = 1;
5363 1
                $data = '>';
5364
5365 1
                break;
5366 47
            case 0x0E:
5367 2
                $name = 'tNE';
5368 2
                $size = 1;
5369 2
                $data = '<>';
5370
5371 2
                break;
5372 47
            case 0x0F:
5373
                $name = 'tIsect';
5374
                $size = 1;
5375
                $data = ' ';
5376
5377
                break;
5378 47
            case 0x10:
5379 1
                $name = 'tList';
5380 1
                $size = 1;
5381 1
                $data = ',';
5382
5383 1
                break;
5384 47
            case 0x11:
5385
                $name = 'tRange';
5386
                $size = 1;
5387
                $data = ':';
5388
5389
                break;
5390 47
            case 0x12:
5391 1
                $name = 'tUplus';
5392 1
                $size = 1;
5393 1
                $data = '+';
5394
5395 1
                break;
5396 47
            case 0x13:
5397 3
                $name = 'tUminus';
5398 3
                $size = 1;
5399 3
                $data = '-';
5400
5401 3
                break;
5402 47
            case 0x14:
5403 1
                $name = 'tPercent';
5404 1
                $size = 1;
5405 1
                $data = '%';
5406
5407 1
                break;
5408 47
            case 0x15:    //    parenthesis
5409 1
                $name = 'tParen';
5410 1
                $size = 1;
5411 1
                $data = null;
5412
5413 1
                break;
5414 47
            case 0x16:    //    missing argument
5415
                $name = 'tMissArg';
5416
                $size = 1;
5417
                $data = '';
5418
5419
                break;
5420 47
            case 0x17:    //    string
5421 19
                $name = 'tStr';
5422
                // offset: 1; size: var; Unicode string, 8-bit string length
5423 19
                $string = self::readUnicodeStringShort(substr($formulaData, 1));
5424 19
                $size = 1 + $string['size'];
5425 19
                $data = self::UTF8toExcelDoubleQuoted($string['value']);
5426
5427 19
                break;
5428 47
            case 0x19:    //    Special attribute
5429
                // offset: 1; size: 1; attribute type flags:
5430 14
                switch (ord($formulaData[1])) {
5431 14
                    case 0x01:
5432 3
                        $name = 'tAttrVolatile';
5433 3
                        $size = 4;
5434 3
                        $data = null;
5435
5436 3
                        break;
5437 12
                    case 0x02:
5438 1
                        $name = 'tAttrIf';
5439 1
                        $size = 4;
5440 1
                        $data = null;
5441
5442 1
                        break;
5443 12
                    case 0x04:
5444 1
                        $name = 'tAttrChoose';
5445
                        // offset: 2; size: 2; number of choices in the CHOOSE function ($nc, number of parameters decreased by 1)
5446 1
                        $nc = self::getUInt2d($formulaData, 2);
5447
                        // offset: 4; size: 2 * $nc
5448
                        // offset: 4 + 2 * $nc; size: 2
5449 1
                        $size = 2 * $nc + 6;
5450 1
                        $data = null;
5451
5452 1
                        break;
5453 12
                    case 0x08:
5454 1
                        $name = 'tAttrSkip';
5455 1
                        $size = 4;
5456 1
                        $data = null;
5457
5458 1
                        break;
5459 12
                    case 0x10:
5460 12
                        $name = 'tAttrSum';
5461 12
                        $size = 4;
5462 12
                        $data = null;
5463
5464 12
                        break;
5465
                    case 0x40:
5466
                    case 0x41:
5467
                        $name = 'tAttrSpace';
5468
                        $size = 4;
5469
                        // offset: 2; size: 2; space type and position
5470
                        $spacetype = match (ord($formulaData[2])) {
5471
                            0x00 => 'type0',
5472
                            0x01 => 'type1',
5473
                            0x02 => 'type2',
5474
                            0x03 => 'type3',
5475
                            0x04 => 'type4',
5476
                            0x05 => 'type5',
5477
                            default => throw new Exception('Unrecognized space type in tAttrSpace token'),
5478
                        };
5479
                        // offset: 3; size: 1; number of inserted spaces/carriage returns
5480
                        $spacecount = ord($formulaData[3]);
5481
5482
                        $data = ['spacetype' => $spacetype, 'spacecount' => $spacecount];
5483
5484
                        break;
5485
                    default:
5486
                        throw new Exception('Unrecognized attribute flag in tAttr token');
5487
                }
5488
5489 14
                break;
5490 47
            case 0x1C:    //    error code
5491
                // offset: 1; size: 1; error code
5492 4
                $name = 'tErr';
5493 4
                $size = 2;
5494 4
                $data = Xls\ErrorCode::lookup(ord($formulaData[1]));
5495
5496 4
                break;
5497 46
            case 0x1D:    //    boolean
5498
                // offset: 1; size: 1; 0 = false, 1 = true;
5499 1
                $name = 'tBool';
5500 1
                $size = 2;
5501 1
                $data = ord($formulaData[1]) ? 'TRUE' : 'FALSE';
5502
5503 1
                break;
5504 46
            case 0x1E:    //    integer
5505
                // offset: 1; size: 2; unsigned 16-bit integer
5506 26
                $name = 'tInt';
5507 26
                $size = 3;
5508 26
                $data = self::getUInt2d($formulaData, 1);
5509
5510 26
                break;
5511 44
            case 0x1F:    //    number
5512
                // offset: 1; size: 8;
5513 7
                $name = 'tNum';
5514 7
                $size = 9;
5515 7
                $data = self::extractNumber(substr($formulaData, 1));
5516 7
                $data = str_replace(',', '.', (string) $data); // in case non-English locale
5517
5518 7
                break;
5519 44
            case 0x20:    //    array constant
5520 44
            case 0x40:
5521 44
            case 0x60:
5522
                // offset: 1; size: 7; not used
5523
                $name = 'tArray';
5524
                $size = 8;
5525
                $data = null;
5526
5527
                break;
5528 44
            case 0x21:    //    function with fixed number of arguments
5529 44
            case 0x41:
5530 43
            case 0x61:
5531 17
                $name = 'tFunc';
5532 17
                $size = 3;
5533
                // offset: 1; size: 2; index to built-in sheet function
5534 17
                switch (self::getUInt2d($formulaData, 1)) {
5535 17
                    case 2:
5536 1
                        $function = 'ISNA';
5537 1
                        $args = 1;
5538
5539 1
                        break;
5540 17
                    case 3:
5541 1
                        $function = 'ISERROR';
5542 1
                        $args = 1;
5543
5544 1
                        break;
5545 17
                    case 10:
5546 9
                        $function = 'NA';
5547 9
                        $args = 0;
5548
5549 9
                        break;
5550 9
                    case 15:
5551 2
                        $function = 'SIN';
5552 2
                        $args = 1;
5553
5554 2
                        break;
5555 8
                    case 16:
5556 1
                        $function = 'COS';
5557 1
                        $args = 1;
5558
5559 1
                        break;
5560 8
                    case 17:
5561 1
                        $function = 'TAN';
5562 1
                        $args = 1;
5563
5564 1
                        break;
5565 8
                    case 18:
5566 1
                        $function = 'ATAN';
5567 1
                        $args = 1;
5568
5569 1
                        break;
5570 8
                    case 19:
5571 1
                        $function = 'PI';
5572 1
                        $args = 0;
5573
5574 1
                        break;
5575 8
                    case 20:
5576 1
                        $function = 'SQRT';
5577 1
                        $args = 1;
5578
5579 1
                        break;
5580 8
                    case 21:
5581 1
                        $function = 'EXP';
5582 1
                        $args = 1;
5583
5584 1
                        break;
5585 8
                    case 22:
5586 1
                        $function = 'LN';
5587 1
                        $args = 1;
5588
5589 1
                        break;
5590 8
                    case 23:
5591 1
                        $function = 'LOG10';
5592 1
                        $args = 1;
5593
5594 1
                        break;
5595 8
                    case 24:
5596 1
                        $function = 'ABS';
5597 1
                        $args = 1;
5598
5599 1
                        break;
5600 8
                    case 25:
5601 1
                        $function = 'INT';
5602 1
                        $args = 1;
5603
5604 1
                        break;
5605 8
                    case 26:
5606 1
                        $function = 'SIGN';
5607 1
                        $args = 1;
5608
5609 1
                        break;
5610 8
                    case 27:
5611 1
                        $function = 'ROUND';
5612 1
                        $args = 2;
5613
5614 1
                        break;
5615 8
                    case 30:
5616 2
                        $function = 'REPT';
5617 2
                        $args = 2;
5618
5619 2
                        break;
5620 8
                    case 31:
5621 1
                        $function = 'MID';
5622 1
                        $args = 3;
5623
5624 1
                        break;
5625 8
                    case 32:
5626 1
                        $function = 'LEN';
5627 1
                        $args = 1;
5628
5629 1
                        break;
5630 8
                    case 33:
5631 1
                        $function = 'VALUE';
5632 1
                        $args = 1;
5633
5634 1
                        break;
5635 8
                    case 34:
5636 3
                        $function = 'TRUE';
5637 3
                        $args = 0;
5638
5639 3
                        break;
5640 8
                    case 35:
5641 3
                        $function = 'FALSE';
5642 3
                        $args = 0;
5643
5644 3
                        break;
5645 7
                    case 38:
5646 1
                        $function = 'NOT';
5647 1
                        $args = 1;
5648
5649 1
                        break;
5650 7
                    case 39:
5651 1
                        $function = 'MOD';
5652 1
                        $args = 2;
5653
5654 1
                        break;
5655 7
                    case 40:
5656 1
                        $function = 'DCOUNT';
5657 1
                        $args = 3;
5658
5659 1
                        break;
5660 7
                    case 41:
5661 1
                        $function = 'DSUM';
5662 1
                        $args = 3;
5663
5664 1
                        break;
5665 7
                    case 42:
5666 1
                        $function = 'DAVERAGE';
5667 1
                        $args = 3;
5668
5669 1
                        break;
5670 7
                    case 43:
5671 1
                        $function = 'DMIN';
5672 1
                        $args = 3;
5673
5674 1
                        break;
5675 7
                    case 44:
5676 1
                        $function = 'DMAX';
5677 1
                        $args = 3;
5678
5679 1
                        break;
5680 7
                    case 45:
5681 1
                        $function = 'DSTDEV';
5682 1
                        $args = 3;
5683
5684 1
                        break;
5685 7
                    case 48:
5686 1
                        $function = 'TEXT';
5687 1
                        $args = 2;
5688
5689 1
                        break;
5690 7
                    case 61:
5691 1
                        $function = 'MIRR';
5692 1
                        $args = 3;
5693
5694 1
                        break;
5695 7
                    case 63:
5696 1
                        $function = 'RAND';
5697 1
                        $args = 0;
5698
5699 1
                        break;
5700 7
                    case 65:
5701 1
                        $function = 'DATE';
5702 1
                        $args = 3;
5703
5704 1
                        break;
5705 7
                    case 66:
5706 1
                        $function = 'TIME';
5707 1
                        $args = 3;
5708
5709 1
                        break;
5710 7
                    case 67:
5711 1
                        $function = 'DAY';
5712 1
                        $args = 1;
5713
5714 1
                        break;
5715 7
                    case 68:
5716 1
                        $function = 'MONTH';
5717 1
                        $args = 1;
5718
5719 1
                        break;
5720 7
                    case 69:
5721 1
                        $function = 'YEAR';
5722 1
                        $args = 1;
5723
5724 1
                        break;
5725 7
                    case 71:
5726 1
                        $function = 'HOUR';
5727 1
                        $args = 1;
5728
5729 1
                        break;
5730 7
                    case 72:
5731 1
                        $function = 'MINUTE';
5732 1
                        $args = 1;
5733
5734 1
                        break;
5735 7
                    case 73:
5736 1
                        $function = 'SECOND';
5737 1
                        $args = 1;
5738
5739 1
                        break;
5740 7
                    case 74:
5741 1
                        $function = 'NOW';
5742 1
                        $args = 0;
5743
5744 1
                        break;
5745 7
                    case 75:
5746 1
                        $function = 'AREAS';
5747 1
                        $args = 1;
5748
5749 1
                        break;
5750 7
                    case 76:
5751 1
                        $function = 'ROWS';
5752 1
                        $args = 1;
5753
5754 1
                        break;
5755 7
                    case 77:
5756 1
                        $function = 'COLUMNS';
5757 1
                        $args = 1;
5758
5759 1
                        break;
5760 7
                    case 83:
5761 1
                        $function = 'TRANSPOSE';
5762 1
                        $args = 1;
5763
5764 1
                        break;
5765 7
                    case 86:
5766 1
                        $function = 'TYPE';
5767 1
                        $args = 1;
5768
5769 1
                        break;
5770 7
                    case 97:
5771 1
                        $function = 'ATAN2';
5772 1
                        $args = 2;
5773
5774 1
                        break;
5775 7
                    case 98:
5776 1
                        $function = 'ASIN';
5777 1
                        $args = 1;
5778
5779 1
                        break;
5780 7
                    case 99:
5781 1
                        $function = 'ACOS';
5782 1
                        $args = 1;
5783
5784 1
                        break;
5785 7
                    case 105:
5786 1
                        $function = 'ISREF';
5787 1
                        $args = 1;
5788
5789 1
                        break;
5790 7
                    case 111:
5791 2
                        $function = 'CHAR';
5792 2
                        $args = 1;
5793
5794 2
                        break;
5795 6
                    case 112:
5796 1
                        $function = 'LOWER';
5797 1
                        $args = 1;
5798
5799 1
                        break;
5800 6
                    case 113:
5801 1
                        $function = 'UPPER';
5802 1
                        $args = 1;
5803
5804 1
                        break;
5805 6
                    case 114:
5806 1
                        $function = 'PROPER';
5807 1
                        $args = 1;
5808
5809 1
                        break;
5810 6
                    case 117:
5811 1
                        $function = 'EXACT';
5812 1
                        $args = 2;
5813
5814 1
                        break;
5815 6
                    case 118:
5816 1
                        $function = 'TRIM';
5817 1
                        $args = 1;
5818
5819 1
                        break;
5820 6
                    case 119:
5821 1
                        $function = 'REPLACE';
5822 1
                        $args = 4;
5823
5824 1
                        break;
5825 6
                    case 121:
5826 1
                        $function = 'CODE';
5827 1
                        $args = 1;
5828
5829 1
                        break;
5830 6
                    case 126:
5831 1
                        $function = 'ISERR';
5832 1
                        $args = 1;
5833
5834 1
                        break;
5835 6
                    case 127:
5836 1
                        $function = 'ISTEXT';
5837 1
                        $args = 1;
5838
5839 1
                        break;
5840 6
                    case 128:
5841 1
                        $function = 'ISNUMBER';
5842 1
                        $args = 1;
5843
5844 1
                        break;
5845 6
                    case 129:
5846 1
                        $function = 'ISBLANK';
5847 1
                        $args = 1;
5848
5849 1
                        break;
5850 6
                    case 130:
5851 1
                        $function = 'T';
5852 1
                        $args = 1;
5853
5854 1
                        break;
5855 6
                    case 131:
5856 1
                        $function = 'N';
5857 1
                        $args = 1;
5858
5859 1
                        break;
5860 6
                    case 140:
5861 1
                        $function = 'DATEVALUE';
5862 1
                        $args = 1;
5863
5864 1
                        break;
5865 6
                    case 141:
5866 1
                        $function = 'TIMEVALUE';
5867 1
                        $args = 1;
5868
5869 1
                        break;
5870 6
                    case 142:
5871 1
                        $function = 'SLN';
5872 1
                        $args = 3;
5873
5874 1
                        break;
5875 6
                    case 143:
5876 1
                        $function = 'SYD';
5877 1
                        $args = 4;
5878
5879 1
                        break;
5880 6
                    case 162:
5881 1
                        $function = 'CLEAN';
5882 1
                        $args = 1;
5883
5884 1
                        break;
5885 6
                    case 163:
5886 1
                        $function = 'MDETERM';
5887 1
                        $args = 1;
5888
5889 1
                        break;
5890 6
                    case 164:
5891 1
                        $function = 'MINVERSE';
5892 1
                        $args = 1;
5893
5894 1
                        break;
5895 6
                    case 165:
5896 1
                        $function = 'MMULT';
5897 1
                        $args = 2;
5898
5899 1
                        break;
5900 6
                    case 184:
5901 1
                        $function = 'FACT';
5902 1
                        $args = 1;
5903
5904 1
                        break;
5905 6
                    case 189:
5906 1
                        $function = 'DPRODUCT';
5907 1
                        $args = 3;
5908
5909 1
                        break;
5910 6
                    case 190:
5911 1
                        $function = 'ISNONTEXT';
5912 1
                        $args = 1;
5913
5914 1
                        break;
5915 6
                    case 195:
5916 1
                        $function = 'DSTDEVP';
5917 1
                        $args = 3;
5918
5919 1
                        break;
5920 6
                    case 196:
5921 1
                        $function = 'DVARP';
5922 1
                        $args = 3;
5923
5924 1
                        break;
5925 6
                    case 198:
5926 1
                        $function = 'ISLOGICAL';
5927 1
                        $args = 1;
5928
5929 1
                        break;
5930 6
                    case 199:
5931 1
                        $function = 'DCOUNTA';
5932 1
                        $args = 3;
5933
5934 1
                        break;
5935 6
                    case 207:
5936 1
                        $function = 'REPLACEB';
5937 1
                        $args = 4;
5938
5939 1
                        break;
5940 6
                    case 210:
5941 1
                        $function = 'MIDB';
5942 1
                        $args = 3;
5943
5944 1
                        break;
5945 6
                    case 211:
5946 1
                        $function = 'LENB';
5947 1
                        $args = 1;
5948
5949 1
                        break;
5950 6
                    case 212:
5951 1
                        $function = 'ROUNDUP';
5952 1
                        $args = 2;
5953
5954 1
                        break;
5955 6
                    case 213:
5956 1
                        $function = 'ROUNDDOWN';
5957 1
                        $args = 2;
5958
5959 1
                        break;
5960 6
                    case 214:
5961 1
                        $function = 'ASC';
5962 1
                        $args = 1;
5963
5964 1
                        break;
5965 6
                    case 215:
5966 1
                        $function = 'DBCS';
5967 1
                        $args = 1;
5968
5969 1
                        break;
5970 6
                    case 221:
5971 1
                        $function = 'TODAY';
5972 1
                        $args = 0;
5973
5974 1
                        break;
5975 6
                    case 229:
5976 1
                        $function = 'SINH';
5977 1
                        $args = 1;
5978
5979 1
                        break;
5980 6
                    case 230:
5981 1
                        $function = 'COSH';
5982 1
                        $args = 1;
5983
5984 1
                        break;
5985 6
                    case 231:
5986 1
                        $function = 'TANH';
5987 1
                        $args = 1;
5988
5989 1
                        break;
5990 6
                    case 232:
5991 1
                        $function = 'ASINH';
5992 1
                        $args = 1;
5993
5994 1
                        break;
5995 6
                    case 233:
5996 1
                        $function = 'ACOSH';
5997 1
                        $args = 1;
5998
5999 1
                        break;
6000 6
                    case 234:
6001 1
                        $function = 'ATANH';
6002 1
                        $args = 1;
6003
6004 1
                        break;
6005 6
                    case 235:
6006 1
                        $function = 'DGET';
6007 1
                        $args = 3;
6008
6009 1
                        break;
6010 6
                    case 244:
6011 2
                        $function = 'INFO';
6012 2
                        $args = 1;
6013
6014 2
                        break;
6015 5
                    case 252:
6016 1
                        $function = 'FREQUENCY';
6017 1
                        $args = 2;
6018
6019 1
                        break;
6020 4
                    case 261:
6021 1
                        $function = 'ERROR.TYPE';
6022 1
                        $args = 1;
6023
6024 1
                        break;
6025 4
                    case 271:
6026 1
                        $function = 'GAMMALN';
6027 1
                        $args = 1;
6028
6029 1
                        break;
6030 4
                    case 273:
6031 1
                        $function = 'BINOMDIST';
6032 1
                        $args = 4;
6033
6034 1
                        break;
6035 4
                    case 274:
6036 1
                        $function = 'CHIDIST';
6037 1
                        $args = 2;
6038
6039 1
                        break;
6040 4
                    case 275:
6041 1
                        $function = 'CHIINV';
6042 1
                        $args = 2;
6043
6044 1
                        break;
6045 4
                    case 276:
6046 1
                        $function = 'COMBIN';
6047 1
                        $args = 2;
6048
6049 1
                        break;
6050 4
                    case 277:
6051 1
                        $function = 'CONFIDENCE';
6052 1
                        $args = 3;
6053
6054 1
                        break;
6055 4
                    case 278:
6056 1
                        $function = 'CRITBINOM';
6057 1
                        $args = 3;
6058
6059 1
                        break;
6060 4
                    case 279:
6061 1
                        $function = 'EVEN';
6062 1
                        $args = 1;
6063
6064 1
                        break;
6065 4
                    case 280:
6066 1
                        $function = 'EXPONDIST';
6067 1
                        $args = 3;
6068
6069 1
                        break;
6070 4
                    case 281:
6071 1
                        $function = 'FDIST';
6072 1
                        $args = 3;
6073
6074 1
                        break;
6075 4
                    case 282:
6076 1
                        $function = 'FINV';
6077 1
                        $args = 3;
6078
6079 1
                        break;
6080 4
                    case 283:
6081 1
                        $function = 'FISHER';
6082 1
                        $args = 1;
6083
6084 1
                        break;
6085 4
                    case 284:
6086 1
                        $function = 'FISHERINV';
6087 1
                        $args = 1;
6088
6089 1
                        break;
6090 4
                    case 285:
6091 1
                        $function = 'FLOOR';
6092 1
                        $args = 2;
6093
6094 1
                        break;
6095 4
                    case 286:
6096 1
                        $function = 'GAMMADIST';
6097 1
                        $args = 4;
6098
6099 1
                        break;
6100 4
                    case 287:
6101 1
                        $function = 'GAMMAINV';
6102 1
                        $args = 3;
6103
6104 1
                        break;
6105 4
                    case 288:
6106 1
                        $function = 'CEILING';
6107 1
                        $args = 2;
6108
6109 1
                        break;
6110 4
                    case 289:
6111 1
                        $function = 'HYPGEOMDIST';
6112 1
                        $args = 4;
6113
6114 1
                        break;
6115 4
                    case 290:
6116 1
                        $function = 'LOGNORMDIST';
6117 1
                        $args = 3;
6118
6119 1
                        break;
6120 4
                    case 291:
6121 1
                        $function = 'LOGINV';
6122 1
                        $args = 3;
6123
6124 1
                        break;
6125 4
                    case 292:
6126 1
                        $function = 'NEGBINOMDIST';
6127 1
                        $args = 3;
6128
6129 1
                        break;
6130 4
                    case 293:
6131 1
                        $function = 'NORMDIST';
6132 1
                        $args = 4;
6133
6134 1
                        break;
6135 4
                    case 294:
6136 1
                        $function = 'NORMSDIST';
6137 1
                        $args = 1;
6138
6139 1
                        break;
6140 4
                    case 295:
6141 1
                        $function = 'NORMINV';
6142 1
                        $args = 3;
6143
6144 1
                        break;
6145 4
                    case 296:
6146 1
                        $function = 'NORMSINV';
6147 1
                        $args = 1;
6148
6149 1
                        break;
6150 4
                    case 297:
6151 1
                        $function = 'STANDARDIZE';
6152 1
                        $args = 3;
6153
6154 1
                        break;
6155 4
                    case 298:
6156 1
                        $function = 'ODD';
6157 1
                        $args = 1;
6158
6159 1
                        break;
6160 4
                    case 299:
6161 1
                        $function = 'PERMUT';
6162 1
                        $args = 2;
6163
6164 1
                        break;
6165 4
                    case 300:
6166 1
                        $function = 'POISSON';
6167 1
                        $args = 3;
6168
6169 1
                        break;
6170 4
                    case 301:
6171 1
                        $function = 'TDIST';
6172 1
                        $args = 3;
6173
6174 1
                        break;
6175 4
                    case 302:
6176 1
                        $function = 'WEIBULL';
6177 1
                        $args = 4;
6178
6179 1
                        break;
6180 3
                    case 303:
6181 1
                        $function = 'SUMXMY2';
6182 1
                        $args = 2;
6183
6184 1
                        break;
6185 3
                    case 304:
6186 1
                        $function = 'SUMX2MY2';
6187 1
                        $args = 2;
6188
6189 1
                        break;
6190 3
                    case 305:
6191 1
                        $function = 'SUMX2PY2';
6192 1
                        $args = 2;
6193
6194 1
                        break;
6195 3
                    case 306:
6196 1
                        $function = 'CHITEST';
6197 1
                        $args = 2;
6198
6199 1
                        break;
6200 3
                    case 307:
6201 1
                        $function = 'CORREL';
6202 1
                        $args = 2;
6203
6204 1
                        break;
6205 3
                    case 308:
6206 1
                        $function = 'COVAR';
6207 1
                        $args = 2;
6208
6209 1
                        break;
6210 3
                    case 309:
6211 1
                        $function = 'FORECAST';
6212 1
                        $args = 3;
6213
6214 1
                        break;
6215 3
                    case 310:
6216 1
                        $function = 'FTEST';
6217 1
                        $args = 2;
6218
6219 1
                        break;
6220 3
                    case 311:
6221 1
                        $function = 'INTERCEPT';
6222 1
                        $args = 2;
6223
6224 1
                        break;
6225 3
                    case 312:
6226 1
                        $function = 'PEARSON';
6227 1
                        $args = 2;
6228
6229 1
                        break;
6230 3
                    case 313:
6231 1
                        $function = 'RSQ';
6232 1
                        $args = 2;
6233
6234 1
                        break;
6235 3
                    case 314:
6236 1
                        $function = 'STEYX';
6237 1
                        $args = 2;
6238
6239 1
                        break;
6240 3
                    case 315:
6241 1
                        $function = 'SLOPE';
6242 1
                        $args = 2;
6243
6244 1
                        break;
6245 3
                    case 316:
6246 1
                        $function = 'TTEST';
6247 1
                        $args = 4;
6248
6249 1
                        break;
6250 3
                    case 325:
6251 1
                        $function = 'LARGE';
6252 1
                        $args = 2;
6253
6254 1
                        break;
6255 3
                    case 326:
6256 1
                        $function = 'SMALL';
6257 1
                        $args = 2;
6258
6259 1
                        break;
6260 3
                    case 327:
6261 1
                        $function = 'QUARTILE';
6262 1
                        $args = 2;
6263
6264 1
                        break;
6265 3
                    case 328:
6266 1
                        $function = 'PERCENTILE';
6267 1
                        $args = 2;
6268
6269 1
                        break;
6270 3
                    case 331:
6271 1
                        $function = 'TRIMMEAN';
6272 1
                        $args = 2;
6273
6274 1
                        break;
6275 3
                    case 332:
6276 1
                        $function = 'TINV';
6277 1
                        $args = 2;
6278
6279 1
                        break;
6280 3
                    case 337:
6281 1
                        $function = 'POWER';
6282 1
                        $args = 2;
6283
6284 1
                        break;
6285 3
                    case 342:
6286 1
                        $function = 'RADIANS';
6287 1
                        $args = 1;
6288
6289 1
                        break;
6290 3
                    case 343:
6291 1
                        $function = 'DEGREES';
6292 1
                        $args = 1;
6293
6294 1
                        break;
6295 3
                    case 346:
6296 1
                        $function = 'COUNTIF';
6297 1
                        $args = 2;
6298
6299 1
                        break;
6300 3
                    case 347:
6301 1
                        $function = 'COUNTBLANK';
6302 1
                        $args = 1;
6303
6304 1
                        break;
6305 3
                    case 350:
6306 1
                        $function = 'ISPMT';
6307 1
                        $args = 4;
6308
6309 1
                        break;
6310 3
                    case 351:
6311 1
                        $function = 'DATEDIF';
6312 1
                        $args = 3;
6313
6314 1
                        break;
6315 3
                    case 352:
6316 1
                        $function = 'DATESTRING';
6317 1
                        $args = 1;
6318
6319 1
                        break;
6320 3
                    case 353:
6321 1
                        $function = 'NUMBERSTRING';
6322 1
                        $args = 2;
6323
6324 1
                        break;
6325 3
                    case 360:
6326 1
                        $function = 'PHONETIC';
6327 1
                        $args = 1;
6328
6329 1
                        break;
6330 2
                    case 368:
6331 1
                        $function = 'BAHTTEXT';
6332 1
                        $args = 1;
6333
6334 1
                        break;
6335
                    default:
6336 1
                        throw new Exception('Unrecognized function in formula');
6337
                }
6338 17
                $data = ['function' => $function, 'args' => $args];
6339
6340 17
                break;
6341 43
            case 0x22:    //    function with variable number of arguments
6342 43
            case 0x42:
6343 41
            case 0x62:
6344 19
                $name = 'tFuncV';
6345 19
                $size = 4;
6346
                // offset: 1; size: 1; number of arguments
6347 19
                $args = ord($formulaData[1]);
6348
                // offset: 2: size: 2; index to built-in sheet function
6349 19
                $index = self::getUInt2d($formulaData, 2);
6350 19
                $function = match ($index) {
6351 19
                    0 => 'COUNT',
6352 19
                    1 => 'IF',
6353 19
                    4 => 'SUM',
6354 19
                    5 => 'AVERAGE',
6355 19
                    6 => 'MIN',
6356 19
                    7 => 'MAX',
6357 19
                    8 => 'ROW',
6358 19
                    9 => 'COLUMN',
6359 19
                    11 => 'NPV',
6360 19
                    12 => 'STDEV',
6361 19
                    13 => 'DOLLAR',
6362 19
                    14 => 'FIXED',
6363 19
                    28 => 'LOOKUP',
6364 19
                    29 => 'INDEX',
6365 19
                    36 => 'AND',
6366 19
                    37 => 'OR',
6367 19
                    46 => 'VAR',
6368 19
                    49 => 'LINEST',
6369 19
                    50 => 'TREND',
6370 19
                    51 => 'LOGEST',
6371 19
                    52 => 'GROWTH',
6372 19
                    56 => 'PV',
6373 19
                    57 => 'FV',
6374 19
                    58 => 'NPER',
6375 19
                    59 => 'PMT',
6376 19
                    60 => 'RATE',
6377 19
                    62 => 'IRR',
6378 19
                    64 => 'MATCH',
6379 19
                    70 => 'WEEKDAY',
6380 19
                    78 => 'OFFSET',
6381 19
                    82 => 'SEARCH',
6382 19
                    100 => 'CHOOSE',
6383 19
                    101 => 'HLOOKUP',
6384 19
                    102 => 'VLOOKUP',
6385 19
                    109 => 'LOG',
6386 19
                    115 => 'LEFT',
6387 19
                    116 => 'RIGHT',
6388 19
                    120 => 'SUBSTITUTE',
6389 19
                    124 => 'FIND',
6390 19
                    125 => 'CELL',
6391 19
                    144 => 'DDB',
6392 19
                    148 => 'INDIRECT',
6393 19
                    167 => 'IPMT',
6394 19
                    168 => 'PPMT',
6395 19
                    169 => 'COUNTA',
6396 19
                    183 => 'PRODUCT',
6397 19
                    193 => 'STDEVP',
6398 19
                    194 => 'VARP',
6399 19
                    197 => 'TRUNC',
6400 19
                    204 => 'USDOLLAR',
6401 19
                    205 => 'FINDB',
6402 19
                    206 => 'SEARCHB',
6403 19
                    208 => 'LEFTB',
6404 19
                    209 => 'RIGHTB',
6405 19
                    216 => 'RANK',
6406 19
                    219 => 'ADDRESS',
6407 19
                    220 => 'DAYS360',
6408 19
                    222 => 'VDB',
6409 19
                    227 => 'MEDIAN',
6410 19
                    228 => 'SUMPRODUCT',
6411 19
                    247 => 'DB',
6412 19
                    255 => '',
6413 19
                    269 => 'AVEDEV',
6414 19
                    270 => 'BETADIST',
6415 19
                    272 => 'BETAINV',
6416 19
                    317 => 'PROB',
6417 19
                    318 => 'DEVSQ',
6418 19
                    319 => 'GEOMEAN',
6419 19
                    320 => 'HARMEAN',
6420 19
                    321 => 'SUMSQ',
6421 19
                    322 => 'KURT',
6422 19
                    323 => 'SKEW',
6423 19
                    324 => 'ZTEST',
6424 19
                    329 => 'PERCENTRANK',
6425 19
                    330 => 'MODE',
6426 19
                    336 => 'CONCATENATE',
6427 19
                    344 => 'SUBTOTAL',
6428 19
                    345 => 'SUMIF',
6429 19
                    354 => 'ROMAN',
6430 19
                    358 => 'GETPIVOTDATA',
6431 19
                    359 => 'HYPERLINK',
6432 19
                    361 => 'AVERAGEA',
6433 19
                    362 => 'MAXA',
6434 19
                    363 => 'MINA',
6435 19
                    364 => 'STDEVPA',
6436 19
                    365 => 'VARPA',
6437 19
                    366 => 'STDEVA',
6438 19
                    367 => 'VARA',
6439 19
                    default => throw new Exception('Unrecognized function in formula'),
6440 19
                };
6441 19
                $data = ['function' => $function, 'args' => $args];
6442
6443 19
                break;
6444 41
            case 0x23:    //    index to defined name
6445 41
            case 0x43:
6446 41
            case 0x63:
6447 1
                $name = 'tName';
6448 1
                $size = 5;
6449
                // offset: 1; size: 2; one-based index to definedname record
6450 1
                $definedNameIndex = self::getUInt2d($formulaData, 1) - 1;
6451
                // offset: 2; size: 2; not used
6452 1
                $data = $this->definedname[$definedNameIndex]['name'] ?? '';
6453
6454 1
                break;
6455 40
            case 0x24:    //    single cell reference e.g. A5
6456 40
            case 0x44:
6457 36
            case 0x64:
6458 18
                $name = 'tRef';
6459 18
                $size = 5;
6460 18
                $data = $this->readBIFF8CellAddress(substr($formulaData, 1, 4));
6461
6462 18
                break;
6463 34
            case 0x25:    //    cell range reference to cells in the same sheet (2d)
6464 14
            case 0x45:
6465 14
            case 0x65:
6466 26
                $name = 'tArea';
6467 26
                $size = 9;
6468 26
                $data = $this->readBIFF8CellRangeAddress(substr($formulaData, 1, 8));
6469
6470 26
                break;
6471 13
            case 0x26:    //    Constant reference sub-expression
6472 13
            case 0x46:
6473 13
            case 0x66:
6474
                $name = 'tMemArea';
6475
                // offset: 1; size: 4; not used
6476
                // offset: 5; size: 2; size of the following subexpression
6477
                $subSize = self::getUInt2d($formulaData, 5);
6478
                $size = 7 + $subSize;
6479
                $data = $this->getFormulaFromData(substr($formulaData, 7, $subSize));
6480
6481
                break;
6482 13
            case 0x27:    //    Deleted constant reference sub-expression
6483 13
            case 0x47:
6484 13
            case 0x67:
6485
                $name = 'tMemErr';
6486
                // offset: 1; size: 4; not used
6487
                // offset: 5; size: 2; size of the following subexpression
6488
                $subSize = self::getUInt2d($formulaData, 5);
6489
                $size = 7 + $subSize;
6490
                $data = $this->getFormulaFromData(substr($formulaData, 7, $subSize));
6491
6492
                break;
6493 13
            case 0x29:    //    Variable reference sub-expression
6494 13
            case 0x49:
6495 13
            case 0x69:
6496
                $name = 'tMemFunc';
6497
                // offset: 1; size: 2; size of the following sub-expression
6498
                $subSize = self::getUInt2d($formulaData, 1);
6499
                $size = 3 + $subSize;
6500
                $data = $this->getFormulaFromData(substr($formulaData, 3, $subSize));
6501
6502
                break;
6503 13
            case 0x2C: // Relative 2d cell reference reference, used in shared formulas and some other places
6504 13
            case 0x4C:
6505 13
            case 0x6C:
6506 2
                $name = 'tRefN';
6507 2
                $size = 5;
6508 2
                $data = $this->readBIFF8CellAddressB(substr($formulaData, 1, 4), $baseCell);
6509
6510 2
                break;
6511 11
            case 0x2D:    //    Relative 2d range reference
6512 11
            case 0x4D:
6513 11
            case 0x6D:
6514
                $name = 'tAreaN';
6515
                $size = 9;
6516
                $data = $this->readBIFF8CellRangeAddressB(substr($formulaData, 1, 8), $baseCell);
6517
6518
                break;
6519 11
            case 0x39:    //    External name
6520 11
            case 0x59:
6521 11
            case 0x79:
6522
                $name = 'tNameX';
6523
                $size = 7;
6524
                // offset: 1; size: 2; index to REF entry in EXTERNSHEET record
6525
                // offset: 3; size: 2; one-based index to DEFINEDNAME or EXTERNNAME record
6526
                $index = self::getUInt2d($formulaData, 3);
6527
                // assume index is to EXTERNNAME record
6528
                $data = $this->externalNames[$index - 1]['name'] ?? '';
6529
6530
                // offset: 5; size: 2; not used
6531
                break;
6532 11
            case 0x3A:    //    3d reference to cell
6533 9
            case 0x5A:
6534 9
            case 0x7A:
6535 2
                $name = 'tRef3d';
6536 2
                $size = 7;
6537
6538
                try {
6539
                    // offset: 1; size: 2; index to REF entry
6540 2
                    $sheetRange = $this->readSheetRangeByRefIndex(self::getUInt2d($formulaData, 1));
6541
                    // offset: 3; size: 4; cell address
6542 2
                    $cellAddress = $this->readBIFF8CellAddress(substr($formulaData, 3, 4));
6543
6544 2
                    $data = "$sheetRange!$cellAddress";
6545
                } catch (PhpSpreadsheetException) {
6546
                    // deleted sheet reference
6547
                    $data = '#REF!';
6548
                }
6549
6550 2
                break;
6551 9
            case 0x3B:    //    3d reference to cell range
6552 1
            case 0x5B:
6553 1
            case 0x7B:
6554 8
                $name = 'tArea3d';
6555 8
                $size = 11;
6556
6557
                try {
6558
                    // offset: 1; size: 2; index to REF entry
6559 8
                    $sheetRange = $this->readSheetRangeByRefIndex(self::getUInt2d($formulaData, 1));
6560
                    // offset: 3; size: 8; cell address
6561 8
                    $cellRangeAddress = $this->readBIFF8CellRangeAddress(substr($formulaData, 3, 8));
6562
6563 8
                    $data = "$sheetRange!$cellRangeAddress";
6564
                } catch (PhpSpreadsheetException) {
6565
                    // deleted sheet reference
6566
                    $data = '#REF!';
6567
                }
6568
6569 8
                break;
6570
                // Unknown cases    // don't know how to deal with
6571
            default:
6572 1
                throw new Exception('Unrecognized token ' . sprintf('%02X', $id) . ' in formula');
6573
        }
6574
6575 47
        return [
6576 47
            'id' => $id,
6577 47
            'name' => $name,
6578 47
            'size' => $size,
6579 47
            'data' => $data,
6580 47
        ];
6581
    }
6582
6583
    /**
6584
     * Reads a cell address in BIFF8 e.g. 'A2' or '$A$2'
6585
     * section 3.3.4.
6586
     */
6587 21
    private function readBIFF8CellAddress(string $cellAddressStructure): string
6588
    {
6589
        // offset: 0; size: 2; index to row (0... 65535) (or offset (-32768... 32767))
6590 21
        $row = self::getUInt2d($cellAddressStructure, 0) + 1;
6591
6592
        // offset: 2; size: 2; index to column or column offset + relative flags
6593
        // bit: 7-0; mask 0x00FF; column index
6594 21
        $column = Coordinate::stringFromColumnIndex((0x00FF & self::getUInt2d($cellAddressStructure, 2)) + 1);
6595
6596
        // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index)
6597 21
        if (!(0x4000 & self::getUInt2d($cellAddressStructure, 2))) {
6598 11
            $column = '$' . $column;
6599
        }
6600
        // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index)
6601 21
        if (!(0x8000 & self::getUInt2d($cellAddressStructure, 2))) {
6602 11
            $row = '$' . $row;
6603
        }
6604
6605 21
        return $column . $row;
6606
    }
6607
6608
    /**
6609
     * Reads a cell address in BIFF8 for shared formulas. Uses positive and negative values for row and column
6610
     * to indicate offsets from a base cell
6611
     * section 3.3.4.
6612
     *
6613
     * @param string $baseCell Base cell, only needed when formula contains tRefN tokens, e.g. with shared formulas
6614
     */
6615 2
    private function readBIFF8CellAddressB(string $cellAddressStructure, string $baseCell = 'A1'): string
6616
    {
6617 2
        [$baseCol, $baseRow] = Coordinate::coordinateFromString($baseCell);
6618 2
        $baseCol = Coordinate::columnIndexFromString($baseCol) - 1;
6619 2
        $baseRow = (int) $baseRow;
6620
6621
        // offset: 0; size: 2; index to row (0... 65535) (or offset (-32768... 32767))
6622 2
        $rowIndex = self::getUInt2d($cellAddressStructure, 0);
6623 2
        $row = self::getUInt2d($cellAddressStructure, 0) + 1;
6624
6625
        // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index)
6626 2
        if (!(0x4000 & self::getUInt2d($cellAddressStructure, 2))) {
6627
            // offset: 2; size: 2; index to column or column offset + relative flags
6628
            // bit: 7-0; mask 0x00FF; column index
6629 2
            $colIndex = 0x00FF & self::getUInt2d($cellAddressStructure, 2);
6630
6631 2
            $column = Coordinate::stringFromColumnIndex($colIndex + 1);
6632 2
            $column = '$' . $column;
6633
        } else {
6634
            // offset: 2; size: 2; index to column or column offset + relative flags
6635
            // bit: 7-0; mask 0x00FF; column index
6636
            $relativeColIndex = 0x00FF & self::getInt2d($cellAddressStructure, 2);
6637
            $colIndex = $baseCol + $relativeColIndex;
6638
            $colIndex = ($colIndex < 256) ? $colIndex : $colIndex - 256;
6639
            $colIndex = ($colIndex >= 0) ? $colIndex : $colIndex + 256;
6640
            $column = Coordinate::stringFromColumnIndex($colIndex + 1);
6641
        }
6642
6643
        // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index)
6644 2
        if (!(0x8000 & self::getUInt2d($cellAddressStructure, 2))) {
6645
            $row = '$' . $row;
6646
        } else {
6647 2
            $rowIndex = ($rowIndex <= 32767) ? $rowIndex : $rowIndex - 65536;
6648 2
            $row = $baseRow + $rowIndex;
6649
        }
6650
6651 2
        return $column . $row;
6652
    }
6653
6654
    /**
6655
     * Reads a cell range address in BIFF5 e.g. 'A2:B6' or 'A1'
6656
     * always fixed range
6657
     * section 2.5.14.
6658
     */
6659 91
    private function readBIFF5CellRangeAddressFixed(string $subData): string
6660
    {
6661
        // offset: 0; size: 2; index to first row
6662 91
        $fr = self::getUInt2d($subData, 0) + 1;
6663
6664
        // offset: 2; size: 2; index to last row
6665 91
        $lr = self::getUInt2d($subData, 2) + 1;
6666
6667
        // offset: 4; size: 1; index to first column
6668 91
        $fc = ord($subData[4]);
6669
6670
        // offset: 5; size: 1; index to last column
6671 91
        $lc = ord($subData[5]);
6672
6673
        // check values
6674 91
        if ($fr > $lr || $fc > $lc) {
6675
            throw new Exception('Not a cell range address');
6676
        }
6677
6678
        // column index to letter
6679 91
        $fc = Coordinate::stringFromColumnIndex($fc + 1);
6680 91
        $lc = Coordinate::stringFromColumnIndex($lc + 1);
6681
6682 91
        if ($fr == $lr && $fc == $lc) {
6683 79
            return "$fc$fr";
6684
        }
6685
6686 26
        return "$fc$fr:$lc$lr";
6687
    }
6688
6689
    /**
6690
     * Reads a cell range address in BIFF8 e.g. 'A2:B6' or 'A1'
6691
     * always fixed range
6692
     * section 2.5.14.
6693
     */
6694 30
    private function readBIFF8CellRangeAddressFixed(string $subData): string
6695
    {
6696
        // offset: 0; size: 2; index to first row
6697 30
        $fr = self::getUInt2d($subData, 0) + 1;
6698
6699
        // offset: 2; size: 2; index to last row
6700 30
        $lr = self::getUInt2d($subData, 2) + 1;
6701
6702
        // offset: 4; size: 2; index to first column
6703 30
        $fc = self::getUInt2d($subData, 4);
6704
6705
        // offset: 6; size: 2; index to last column
6706 30
        $lc = self::getUInt2d($subData, 6);
6707
6708
        // check values
6709 30
        if ($fr > $lr || $fc > $lc) {
6710
            throw new Exception('Not a cell range address');
6711
        }
6712
6713
        // column index to letter
6714 30
        $fc = Coordinate::stringFromColumnIndex($fc + 1);
6715 30
        $lc = Coordinate::stringFromColumnIndex($lc + 1);
6716
6717 30
        if ($fr == $lr && $fc == $lc) {
6718 9
            return "$fc$fr";
6719
        }
6720
6721 25
        return "$fc$fr:$lc$lr";
6722
    }
6723
6724
    /**
6725
     * Reads a cell range address in BIFF8 e.g. 'A2:B6' or '$A$2:$B$6'
6726
     * there are flags indicating whether column/row index is relative
6727
     * section 3.3.4.
6728
     */
6729 30
    private function readBIFF8CellRangeAddress(string $subData): string
6730
    {
6731
        // todo: if cell range is just a single cell, should this funciton
6732
        // not just return e.g. 'A1' and not 'A1:A1' ?
6733
6734
        // offset: 0; size: 2; index to first row (0... 65535) (or offset (-32768... 32767))
6735 30
        $fr = self::getUInt2d($subData, 0) + 1;
6736
6737
        // offset: 2; size: 2; index to last row (0... 65535) (or offset (-32768... 32767))
6738 30
        $lr = self::getUInt2d($subData, 2) + 1;
6739
6740
        // offset: 4; size: 2; index to first column or column offset + relative flags
6741
6742
        // bit: 7-0; mask 0x00FF; column index
6743 30
        $fc = Coordinate::stringFromColumnIndex((0x00FF & self::getUInt2d($subData, 4)) + 1);
6744
6745
        // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index)
6746 30
        if (!(0x4000 & self::getUInt2d($subData, 4))) {
6747 14
            $fc = '$' . $fc;
6748
        }
6749
6750
        // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index)
6751 30
        if (!(0x8000 & self::getUInt2d($subData, 4))) {
6752 14
            $fr = '$' . $fr;
6753
        }
6754
6755
        // offset: 6; size: 2; index to last column or column offset + relative flags
6756
6757
        // bit: 7-0; mask 0x00FF; column index
6758 30
        $lc = Coordinate::stringFromColumnIndex((0x00FF & self::getUInt2d($subData, 6)) + 1);
6759
6760
        // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index)
6761 30
        if (!(0x4000 & self::getUInt2d($subData, 6))) {
6762 14
            $lc = '$' . $lc;
6763
        }
6764
6765
        // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index)
6766 30
        if (!(0x8000 & self::getUInt2d($subData, 6))) {
6767 14
            $lr = '$' . $lr;
6768
        }
6769
6770 30
        return "$fc$fr:$lc$lr";
6771
    }
6772
6773
    /**
6774
     * Reads a cell range address in BIFF8 for shared formulas. Uses positive and negative values for row and column
6775
     * to indicate offsets from a base cell
6776
     * section 3.3.4.
6777
     *
6778
     * @param string $baseCell Base cell
6779
     *
6780
     * @return string Cell range address
6781
     */
6782
    private function readBIFF8CellRangeAddressB(string $subData, string $baseCell = 'A1'): string
6783
    {
6784
        [$baseCol, $baseRow] = Coordinate::indexesFromString($baseCell);
6785
        $baseCol = $baseCol - 1;
6786
6787
        // TODO: if cell range is just a single cell, should this funciton
6788
        // not just return e.g. 'A1' and not 'A1:A1' ?
6789
6790
        // offset: 0; size: 2; first row
6791
        $frIndex = self::getUInt2d($subData, 0); // adjust below
6792
6793
        // offset: 2; size: 2; relative index to first row (0... 65535) should be treated as offset (-32768... 32767)
6794
        $lrIndex = self::getUInt2d($subData, 2); // adjust below
6795
6796
        // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index)
6797
        if (!(0x4000 & self::getUInt2d($subData, 4))) {
6798
            // absolute column index
6799
            // offset: 4; size: 2; first column with relative/absolute flags
6800
            // bit: 7-0; mask 0x00FF; column index
6801
            $fcIndex = 0x00FF & self::getUInt2d($subData, 4);
6802
            $fc = Coordinate::stringFromColumnIndex($fcIndex + 1);
6803
            $fc = '$' . $fc;
6804
        } else {
6805
            // column offset
6806
            // offset: 4; size: 2; first column with relative/absolute flags
6807
            // bit: 7-0; mask 0x00FF; column index
6808
            $relativeFcIndex = 0x00FF & self::getInt2d($subData, 4);
6809
            $fcIndex = $baseCol + $relativeFcIndex;
6810
            $fcIndex = ($fcIndex < 256) ? $fcIndex : $fcIndex - 256;
6811
            $fcIndex = ($fcIndex >= 0) ? $fcIndex : $fcIndex + 256;
6812
            $fc = Coordinate::stringFromColumnIndex($fcIndex + 1);
6813
        }
6814
6815
        // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index)
6816
        if (!(0x8000 & self::getUInt2d($subData, 4))) {
6817
            // absolute row index
6818
            $fr = $frIndex + 1;
6819
            $fr = '$' . $fr;
6820
        } else {
6821
            // row offset
6822
            $frIndex = ($frIndex <= 32767) ? $frIndex : $frIndex - 65536;
6823
            $fr = $baseRow + $frIndex;
6824
        }
6825
6826
        // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index)
6827
        if (!(0x4000 & self::getUInt2d($subData, 6))) {
6828
            // absolute column index
6829
            // offset: 6; size: 2; last column with relative/absolute flags
6830
            // bit: 7-0; mask 0x00FF; column index
6831
            $lcIndex = 0x00FF & self::getUInt2d($subData, 6);
6832
            $lc = Coordinate::stringFromColumnIndex($lcIndex + 1);
6833
            $lc = '$' . $lc;
6834
        } else {
6835
            // column offset
6836
            // offset: 4; size: 2; first column with relative/absolute flags
6837
            // bit: 7-0; mask 0x00FF; column index
6838
            $relativeLcIndex = 0x00FF & self::getInt2d($subData, 4);
6839
            $lcIndex = $baseCol + $relativeLcIndex;
6840
            $lcIndex = ($lcIndex < 256) ? $lcIndex : $lcIndex - 256;
6841
            $lcIndex = ($lcIndex >= 0) ? $lcIndex : $lcIndex + 256;
6842
            $lc = Coordinate::stringFromColumnIndex($lcIndex + 1);
6843
        }
6844
6845
        // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index)
6846
        if (!(0x8000 & self::getUInt2d($subData, 6))) {
6847
            // absolute row index
6848
            $lr = $lrIndex + 1;
6849
            $lr = '$' . $lr;
6850
        } else {
6851
            // row offset
6852
            $lrIndex = ($lrIndex <= 32767) ? $lrIndex : $lrIndex - 65536;
6853
            $lr = $baseRow + $lrIndex;
6854
        }
6855
6856
        return "$fc$fr:$lc$lr";
6857
    }
6858
6859
    /**
6860
     * Read BIFF8 cell range address list
6861
     * section 2.5.15.
6862
     */
6863 28
    private function readBIFF8CellRangeAddressList(string $subData): array
6864
    {
6865 28
        $cellRangeAddresses = [];
6866
6867
        // offset: 0; size: 2; number of the following cell range addresses
6868 28
        $nm = self::getUInt2d($subData, 0);
6869
6870 28
        $offset = 2;
6871
        // offset: 2; size: 8 * $nm; list of $nm (fixed) cell range addresses
6872 28
        for ($i = 0; $i < $nm; ++$i) {
6873 28
            $cellRangeAddresses[] = $this->readBIFF8CellRangeAddressFixed(substr($subData, $offset, 8));
6874 28
            $offset += 8;
6875
        }
6876
6877 28
        return [
6878 28
            'size' => 2 + 8 * $nm,
6879 28
            'cellRangeAddresses' => $cellRangeAddresses,
6880 28
        ];
6881
    }
6882
6883
    /**
6884
     * Read BIFF5 cell range address list
6885
     * section 2.5.15.
6886
     */
6887 91
    private function readBIFF5CellRangeAddressList(string $subData): array
6888
    {
6889 91
        $cellRangeAddresses = [];
6890
6891
        // offset: 0; size: 2; number of the following cell range addresses
6892 91
        $nm = self::getUInt2d($subData, 0);
6893
6894 91
        $offset = 2;
6895
        // offset: 2; size: 6 * $nm; list of $nm (fixed) cell range addresses
6896 91
        for ($i = 0; $i < $nm; ++$i) {
6897 91
            $cellRangeAddresses[] = $this->readBIFF5CellRangeAddressFixed(substr($subData, $offset, 6));
6898 91
            $offset += 6;
6899
        }
6900
6901 91
        return [
6902 91
            'size' => 2 + 6 * $nm,
6903 91
            'cellRangeAddresses' => $cellRangeAddresses,
6904 91
        ];
6905
    }
6906
6907
    /**
6908
     * Get a sheet range like Sheet1:Sheet3 from REF index
6909
     * Note: If there is only one sheet in the range, one gets e.g Sheet1
6910
     * It can also happen that the REF structure uses the -1 (FFFF) code to indicate deleted sheets,
6911
     * in which case an Exception is thrown.
6912
     */
6913 10
    private function readSheetRangeByRefIndex(int $index): string|false
6914
    {
6915 10
        if (isset($this->ref[$index])) {
6916 10
            $type = $this->externalBooks[$this->ref[$index]['externalBookIndex']]['type'];
6917
6918
            switch ($type) {
6919 10
                case 'internal':
6920
                    // check if we have a deleted 3d reference
6921 10
                    if ($this->ref[$index]['firstSheetIndex'] == 0xFFFF || $this->ref[$index]['lastSheetIndex'] == 0xFFFF) {
6922
                        throw new Exception('Deleted sheet reference');
6923
                    }
6924
6925
                    // we have normal sheet range (collapsed or uncollapsed)
6926 10
                    $firstSheetName = $this->sheets[$this->ref[$index]['firstSheetIndex']]['name'];
6927 10
                    $lastSheetName = $this->sheets[$this->ref[$index]['lastSheetIndex']]['name'];
6928
6929 10
                    if ($firstSheetName == $lastSheetName) {
6930
                        // collapsed sheet range
6931 10
                        $sheetRange = $firstSheetName;
6932
                    } else {
6933
                        $sheetRange = "$firstSheetName:$lastSheetName";
6934
                    }
6935
6936
                    // escape the single-quotes
6937 10
                    $sheetRange = str_replace("'", "''", $sheetRange);
6938
6939
                    // if there are special characters, we need to enclose the range in single-quotes
6940
                    // todo: check if we have identified the whole set of special characters
6941
                    // it seems that the following characters are not accepted for sheet names
6942
                    // and we may assume that they are not present: []*/:\?
6943 10
                    if (preg_match("/[ !\"@#£$%&{()}<>=+'|^,;-]/u", $sheetRange)) {
6944 3
                        $sheetRange = "'$sheetRange'";
6945
                    }
6946
6947 10
                    return $sheetRange;
6948
                default:
6949
                    // TODO: external sheet support
6950
                    throw new Exception('Xls reader only supports internal sheets in formulas');
6951
            }
6952
        }
6953
6954
        return false;
6955
    }
6956
6957
    /**
6958
     * read BIFF8 constant value array from array data
6959
     * returns e.g. ['value' => '{1,2;3,4}', 'size' => 40]
6960
     * section 2.5.8.
6961
     */
6962
    private static function readBIFF8ConstantArray(string $arrayData): array
6963
    {
6964
        // offset: 0; size: 1; number of columns decreased by 1
6965
        $nc = ord($arrayData[0]);
6966
6967
        // offset: 1; size: 2; number of rows decreased by 1
6968
        $nr = self::getUInt2d($arrayData, 1);
6969
        $size = 3; // initialize
6970
        $arrayData = substr($arrayData, 3);
6971
6972
        // offset: 3; size: var; list of ($nc + 1) * ($nr + 1) constant values
6973
        $matrixChunks = [];
6974
        for ($r = 1; $r <= $nr + 1; ++$r) {
6975
            $items = [];
6976
            for ($c = 1; $c <= $nc + 1; ++$c) {
6977
                $constant = self::readBIFF8Constant($arrayData);
6978
                $items[] = $constant['value'];
6979
                $arrayData = substr($arrayData, $constant['size']);
6980
                $size += $constant['size'];
6981
            }
6982
            $matrixChunks[] = implode(',', $items); // looks like e.g. '1,"hello"'
6983
        }
6984
        $matrix = '{' . implode(';', $matrixChunks) . '}';
6985
6986
        return [
6987
            'value' => $matrix,
6988
            'size' => $size,
6989
        ];
6990
    }
6991
6992
    /**
6993
     * read BIFF8 constant value which may be 'Empty Value', 'Number', 'String Value', 'Boolean Value', 'Error Value'
6994
     * section 2.5.7
6995
     * returns e.g. ['value' => '5', 'size' => 9].
6996
     */
6997
    private static function readBIFF8Constant(string $valueData): array
6998
    {
6999
        // offset: 0; size: 1; identifier for type of constant
7000
        $identifier = ord($valueData[0]);
7001
7002
        switch ($identifier) {
7003
            case 0x00: // empty constant (what is this?)
7004
                $value = '';
7005
                $size = 9;
7006
7007
                break;
7008
            case 0x01: // number
7009
                // offset: 1; size: 8; IEEE 754 floating-point value
7010
                $value = self::extractNumber(substr($valueData, 1, 8));
7011
                $size = 9;
7012
7013
                break;
7014
            case 0x02: // string value
7015
                // offset: 1; size: var; Unicode string, 16-bit string length
7016
                $string = self::readUnicodeStringLong(substr($valueData, 1));
7017
                $value = '"' . $string['value'] . '"';
7018
                $size = 1 + $string['size'];
7019
7020
                break;
7021
            case 0x04: // boolean
7022
                // offset: 1; size: 1; 0 = FALSE, 1 = TRUE
7023
                if (ord($valueData[1])) {
7024
                    $value = 'TRUE';
7025
                } else {
7026
                    $value = 'FALSE';
7027
                }
7028
                $size = 9;
7029
7030
                break;
7031
            case 0x10: // error code
7032
                // offset: 1; size: 1; error code
7033
                $value = Xls\ErrorCode::lookup(ord($valueData[1]));
7034
                $size = 9;
7035
7036
                break;
7037
            default:
7038
                throw new PhpSpreadsheetException('Unsupported BIFF8 constant');
7039
        }
7040
7041
        return [
7042
            'value' => $value,
7043
            'size' => $size,
7044
        ];
7045
    }
7046
7047
    /**
7048
     * Extract RGB color
7049
     * OpenOffice.org's Documentation of the Microsoft Excel File Format, section 2.5.4.
7050
     *
7051
     * @param string $rgb Encoded RGB value (4 bytes)
7052
     */
7053 64
    private static function readRGB(string $rgb): array
7054
    {
7055
        // offset: 0; size 1; Red component
7056 64
        $r = ord($rgb[0]);
7057
7058
        // offset: 1; size: 1; Green component
7059 64
        $g = ord($rgb[1]);
7060
7061
        // offset: 2; size: 1; Blue component
7062 64
        $b = ord($rgb[2]);
7063
7064
        // HEX notation, e.g. 'FF00FC'
7065 64
        $rgb = sprintf('%02X%02X%02X', $r, $g, $b);
7066
7067 64
        return ['rgb' => $rgb];
7068
    }
7069
7070
    /**
7071
     * Read byte string (8-bit string length)
7072
     * OpenOffice documentation: 2.5.2.
7073
     */
7074 6
    private function readByteStringShort(string $subData): array
7075
    {
7076
        // offset: 0; size: 1; length of the string (character count)
7077 6
        $ln = ord($subData[0]);
7078
7079
        // offset: 1: size: var; character array (8-bit characters)
7080 6
        $value = $this->decodeCodepage(substr($subData, 1, $ln));
7081
7082 6
        return [
7083 6
            'value' => $value,
7084 6
            'size' => 1 + $ln, // size in bytes of data structure
7085 6
        ];
7086
    }
7087
7088
    /**
7089
     * Read byte string (16-bit string length)
7090
     * OpenOffice documentation: 2.5.2.
7091
     */
7092 2
    private function readByteStringLong(string $subData): array
7093
    {
7094
        // offset: 0; size: 2; length of the string (character count)
7095 2
        $ln = self::getUInt2d($subData, 0);
7096
7097
        // offset: 2: size: var; character array (8-bit characters)
7098 2
        $value = $this->decodeCodepage(substr($subData, 2));
7099
7100
        //return $string;
7101 2
        return [
7102 2
            'value' => $value,
7103 2
            'size' => 2 + $ln, // size in bytes of data structure
7104 2
        ];
7105
    }
7106
7107
    /**
7108
     * Extracts an Excel Unicode short string (8-bit string length)
7109
     * OpenOffice documentation: 2.5.3
7110
     * function will automatically find out where the Unicode string ends.
7111
     */
7112 99
    private static function readUnicodeStringShort(string $subData): array
7113
    {
7114
        // offset: 0: size: 1; length of the string (character count)
7115 99
        $characterCount = ord($subData[0]);
7116
7117 99
        $string = self::readUnicodeString(substr($subData, 1), $characterCount);
7118
7119
        // add 1 for the string length
7120 99
        ++$string['size'];
7121
7122 99
        return $string;
7123
    }
7124
7125
    /**
7126
     * Extracts an Excel Unicode long string (16-bit string length)
7127
     * OpenOffice documentation: 2.5.3
7128
     * this function is under construction, needs to support rich text, and Asian phonetic settings.
7129
     */
7130 93
    private static function readUnicodeStringLong(string $subData): array
7131
    {
7132
        // offset: 0: size: 2; length of the string (character count)
7133 93
        $characterCount = self::getUInt2d($subData, 0);
7134
7135 93
        $string = self::readUnicodeString(substr($subData, 2), $characterCount);
7136
7137
        // add 2 for the string length
7138 93
        $string['size'] += 2;
7139
7140 93
        return $string;
7141
    }
7142
7143
    /**
7144
     * Read Unicode string with no string length field, but with known character count
7145
     * this function is under construction, needs to support rich text, and Asian phonetic settings
7146
     * OpenOffice.org's Documentation of the Microsoft Excel File Format, section 2.5.3.
7147
     */
7148 99
    private static function readUnicodeString(string $subData, int $characterCount): array
7149
    {
7150
        // offset: 0: size: 1; option flags
7151
        // bit: 0; mask: 0x01; character compression (0 = compressed 8-bit, 1 = uncompressed 16-bit)
7152 99
        $isCompressed = !((0x01 & ord($subData[0])) >> 0);
7153
7154
        // bit: 2; mask: 0x04; Asian phonetic settings
7155
        //$hasAsian = (0x04) & ord($subData[0]) >> 2;
7156
7157
        // bit: 3; mask: 0x08; Rich-Text settings
7158
        //$hasRichText = (0x08) & ord($subData[0]) >> 3;
7159
7160
        // offset: 1: size: var; character array
7161
        // this offset assumes richtext and Asian phonetic settings are off which is generally wrong
7162
        // needs to be fixed
7163 99
        $value = self::encodeUTF16(substr($subData, 1, $isCompressed ? $characterCount : 2 * $characterCount), $isCompressed);
7164
7165 99
        return [
7166 99
            'value' => $value,
7167 99
            'size' => $isCompressed ? 1 + $characterCount : 1 + 2 * $characterCount, // the size in bytes including the option flags
7168 99
        ];
7169
    }
7170
7171
    /**
7172
     * Convert UTF-8 string to string surounded by double quotes. Used for explicit string tokens in formulas.
7173
     * Example:  hello"world  -->  "hello""world".
7174
     *
7175
     * @param string $value UTF-8 encoded string
7176
     */
7177 19
    private static function UTF8toExcelDoubleQuoted(string $value): string
7178
    {
7179 19
        return '"' . str_replace('"', '""', $value) . '"';
7180
    }
7181
7182
    /**
7183
     * Reads first 8 bytes of a string and return IEEE 754 float.
7184
     *
7185
     * @param string $data Binary string that is at least 8 bytes long
7186
     */
7187 95
    private static function extractNumber(string $data): int|float
7188
    {
7189 95
        $rknumhigh = self::getInt4d($data, 4);
7190 95
        $rknumlow = self::getInt4d($data, 0);
7191 95
        $sign = ($rknumhigh & self::HIGH_ORDER_BIT) >> 31;
7192 95
        $exp = (($rknumhigh & 0x7FF00000) >> 20) - 1023;
7193 95
        $mantissa = (0x100000 | ($rknumhigh & 0x000FFFFF));
7194 95
        $mantissalow1 = ($rknumlow & self::HIGH_ORDER_BIT) >> 31;
7195 95
        $mantissalow2 = ($rknumlow & 0x7FFFFFFF);
7196 95
        $value = $mantissa / 2 ** (20 - $exp);
7197
7198 95
        if ($mantissalow1 != 0) {
7199 26
            $value += 1 / 2 ** (21 - $exp);
7200
        }
7201
7202 95
        if ($mantissalow2 != 0) {
7203 90
            $value += $mantissalow2 / 2 ** (52 - $exp);
7204
        }
7205 95
        if ($sign) {
7206 19
            $value *= -1;
7207
        }
7208
7209 95
        return $value;
7210
    }
7211
7212 33
    private static function getIEEE754(int $rknum): float|int
7213
    {
7214 33
        if (($rknum & 0x02) != 0) {
7215 7
            $value = $rknum >> 2;
7216
        } else {
7217
            // changes by mmp, info on IEEE754 encoding from
7218
            // research.microsoft.com/~hollasch/cgindex/coding/ieeefloat.html
7219
            // The RK format calls for using only the most significant 30 bits
7220
            // of the 64 bit floating point value. The other 34 bits are assumed
7221
            // to be 0 so we use the upper 30 bits of $rknum as follows...
7222 28
            $sign = ($rknum & self::HIGH_ORDER_BIT) >> 31;
7223 28
            $exp = ($rknum & 0x7FF00000) >> 20;
7224 28
            $mantissa = (0x100000 | ($rknum & 0x000FFFFC));
7225 28
            $value = $mantissa / 2 ** (20 - ($exp - 1023));
7226 28
            if ($sign) {
7227 11
                $value = -1 * $value;
7228
            }
7229
            //end of changes by mmp
7230
        }
7231 33
        if (($rknum & 0x01) != 0) {
7232 14
            $value /= 100;
7233
        }
7234
7235 33
        return $value;
7236
    }
7237
7238
    /**
7239
     * Get UTF-8 string from (compressed or uncompressed) UTF-16 string.
7240
     */
7241 99
    private static function encodeUTF16(string $string, bool $compressed = false): string
7242
    {
7243 99
        if ($compressed) {
7244 56
            $string = self::uncompressByteString($string);
7245
        }
7246
7247 99
        return StringHelper::convertEncoding($string, 'UTF-8', 'UTF-16LE');
7248
    }
7249
7250
    /**
7251
     * Convert UTF-16 string in compressed notation to uncompressed form. Only used for BIFF8.
7252
     */
7253 56
    private static function uncompressByteString(string $string): string
7254
    {
7255 56
        $uncompressedString = '';
7256 56
        $strLen = strlen($string);
7257 56
        for ($i = 0; $i < $strLen; ++$i) {
7258 55
            $uncompressedString .= $string[$i] . "\0";
7259
        }
7260
7261 56
        return $uncompressedString;
7262
    }
7263
7264
    /**
7265
     * Convert string to UTF-8. Only used for BIFF5.
7266
     */
7267 6
    private function decodeCodepage(string $string): string
7268
    {
7269 6
        return StringHelper::convertEncoding($string, 'UTF-8', $this->codepage);
7270
    }
7271
7272
    /**
7273
     * Read 16-bit unsigned integer.
7274
     */
7275 105
    public static function getUInt2d(string $data, int $pos): int
7276
    {
7277 105
        return ord($data[$pos]) | (ord($data[$pos + 1]) << 8);
7278
    }
7279
7280
    /**
7281
     * Read 16-bit signed integer.
7282
     */
7283
    public static function getInt2d(string $data, int $pos): int
7284
    {
7285
        return unpack('s', $data[$pos] . $data[$pos + 1])[1]; // @phpstan-ignore-line
7286
    }
7287
7288
    /**
7289
     * Read 32-bit signed integer.
7290
     */
7291 105
    public static function getInt4d(string $data, int $pos): int
7292
    {
7293
        // FIX: represent numbers correctly on 64-bit system
7294
        // http://sourceforge.net/tracker/index.php?func=detail&aid=1487372&group_id=99160&atid=623334
7295
        // Changed by Andreas Rehm 2006 to ensure correct result of the <<24 block on 32 and 64bit systems
7296 105
        $_or_24 = ord($data[$pos + 3]);
7297 105
        if ($_or_24 >= 128) {
7298
            // negative number
7299 35
            $_ord_24 = -abs((256 - $_or_24) << 24);
7300
        } else {
7301 105
            $_ord_24 = ($_or_24 & 127) << 24;
7302
        }
7303
7304 105
        return ord($data[$pos]) | (ord($data[$pos + 1]) << 8) | (ord($data[$pos + 2]) << 16) | $_ord_24;
7305
    }
7306
7307 3
    private function parseRichText(string $is): RichText
7308
    {
7309 3
        $value = new RichText();
7310 3
        $value->createText($is);
7311
7312 3
        return $value;
7313
    }
7314
7315
    /**
7316
     * Phpstan 1.4.4 complains that this property is never read.
7317
     * So, we might be able to get rid of it altogether.
7318
     * For now, however, this function makes it readable,
7319
     * which satisfies Phpstan.
7320
     *
7321
     * @codeCoverageIgnore
7322
     */
7323
    public function getMapCellStyleXfIndex(): array
7324
    {
7325
        return $this->mapCellStyleXfIndex;
7326
    }
7327
7328 16
    private function readCFHeader(): array
7329
    {
7330 16
        $length = self::getUInt2d($this->data, $this->pos + 2);
7331 16
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
7332
7333
        // move stream pointer forward to next record
7334 16
        $this->pos += 4 + $length;
7335
7336 16
        if ($this->readDataOnly) {
7337 1
            return [];
7338
        }
7339
7340
        // offset: 0; size: 2; Rule Count
7341
//        $ruleCount = self::getUInt2d($recordData, 0);
7342
7343
        // offset: var; size: var; cell range address list with
7344 15
        $cellRangeAddressList = ($this->version == self::XLS_BIFF8)
7345 15
            ? $this->readBIFF8CellRangeAddressList(substr($recordData, 12))
7346
            : $this->readBIFF5CellRangeAddressList(substr($recordData, 12));
7347 15
        $cellRangeAddresses = $cellRangeAddressList['cellRangeAddresses'];
7348
7349 15
        return $cellRangeAddresses;
7350
    }
7351
7352 16
    private function readCFRule(array $cellRangeAddresses): void
7353
    {
7354 16
        $length = self::getUInt2d($this->data, $this->pos + 2);
7355 16
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
7356
7357
        // move stream pointer forward to next record
7358 16
        $this->pos += 4 + $length;
7359
7360 16
        if ($this->readDataOnly) {
7361 1
            return;
7362
        }
7363
7364
        // offset: 0; size: 2; Options
7365 15
        $cfRule = self::getUInt2d($recordData, 0);
7366
7367
        // bit: 8-15; mask: 0x00FF; type
7368 15
        $type = (0x00FF & $cfRule) >> 0;
7369 15
        $type = ConditionalFormatting::type($type);
7370
7371
        // bit: 0-7; mask: 0xFF00; type
7372 15
        $operator = (0xFF00 & $cfRule) >> 8;
7373 15
        $operator = ConditionalFormatting::operator($operator);
7374
7375 15
        if ($type === null || $operator === null) {
7376
            return;
7377
        }
7378
7379
        // offset: 2; size: 2; Size1
7380 15
        $size1 = self::getUInt2d($recordData, 2);
7381
7382
        // offset: 4; size: 2; Size2
7383 15
        $size2 = self::getUInt2d($recordData, 4);
7384
7385
        // offset: 6; size: 4; Options
7386 15
        $options = self::getInt4d($recordData, 6);
7387
7388 15
        $style = new Style(false, true); // non-supervisor, conditional
7389
        //$this->getCFStyleOptions($options, $style);
7390
7391 15
        $hasFontRecord = (bool) ((0x04000000 & $options) >> 26);
7392 15
        $hasAlignmentRecord = (bool) ((0x08000000 & $options) >> 27);
7393 15
        $hasBorderRecord = (bool) ((0x10000000 & $options) >> 28);
7394 15
        $hasFillRecord = (bool) ((0x20000000 & $options) >> 29);
7395 15
        $hasProtectionRecord = (bool) ((0x40000000 & $options) >> 30);
7396
7397 15
        $offset = 12;
7398
7399 15
        if ($hasFontRecord === true) {
7400 15
            $fontStyle = substr($recordData, $offset, 118);
7401 15
            $this->getCFFontStyle($fontStyle, $style);
7402 15
            $offset += 118;
7403
        }
7404
7405 15
        if ($hasAlignmentRecord === true) {
7406
            //$alignmentStyle = substr($recordData, $offset, 8);
7407
            //$this->getCFAlignmentStyle($alignmentStyle, $style);
7408
            $offset += 8;
7409
        }
7410
7411 15
        if ($hasBorderRecord === true) {
7412
            //$borderStyle = substr($recordData, $offset, 8);
7413
            //$this->getCFBorderStyle($borderStyle, $style);
7414
            $offset += 8;
7415
        }
7416
7417 15
        if ($hasFillRecord === true) {
7418 2
            $fillStyle = substr($recordData, $offset, 4);
7419 2
            $this->getCFFillStyle($fillStyle, $style);
7420 2
            $offset += 4;
7421
        }
7422
7423 15
        if ($hasProtectionRecord === true) {
7424
            //$protectionStyle = substr($recordData, $offset, 4);
7425
            //$this->getCFProtectionStyle($protectionStyle, $style);
7426
            $offset += 2;
7427
        }
7428
7429 15
        $formula1 = $formula2 = null;
7430 15
        if ($size1 > 0) {
7431 15
            $formula1 = $this->readCFFormula($recordData, $offset, $size1);
7432 15
            if ($formula1 === null) {
7433
                return;
7434
            }
7435
7436 15
            $offset += $size1;
7437
        }
7438
7439 15
        if ($size2 > 0) {
7440 6
            $formula2 = $this->readCFFormula($recordData, $offset, $size2);
7441 6
            if ($formula2 === null) {
7442
                return;
7443
            }
7444
7445 6
            $offset += $size2;
7446
        }
7447
7448 15
        $this->setCFRules($cellRangeAddresses, $type, $operator, $formula1, $formula2, $style);
7449
    }
7450
7451
    /*private function getCFStyleOptions(int $options, Style $style): void
7452
    {
7453
    }*/
7454
7455 15
    private function getCFFontStyle(string $options, Style $style): void
7456
    {
7457 15
        $fontSize = self::getInt4d($options, 64);
7458 15
        if ($fontSize !== -1) {
7459 8
            $style->getFont()->setSize($fontSize / 20); // Convert twips to points
7460
        }
7461
7462 15
        $bold = self::getUInt2d($options, 72) === 700; // 400 = normal, 700 = bold
7463 15
        $style->getFont()->setBold($bold);
7464
7465 15
        $color = self::getInt4d($options, 80);
7466
7467 15
        if ($color !== -1) {
7468 15
            $style->getFont()->getColor()->setRGB(Xls\Color::map($color, $this->palette, $this->version)['rgb']);
7469
        }
7470
    }
7471
7472
    /*private function getCFAlignmentStyle(string $options, Style $style): void
7473
    {
7474
    }*/
7475
7476
    /*private function getCFBorderStyle(string $options, Style $style): void
7477
    {
7478
    }*/
7479
7480 2
    private function getCFFillStyle(string $options, Style $style): void
7481
    {
7482 2
        $fillPattern = self::getUInt2d($options, 0);
7483
        // bit: 10-15; mask: 0xFC00; type
7484 2
        $fillPattern = (0xFC00 & $fillPattern) >> 10;
7485 2
        $fillPattern = FillPattern::lookup($fillPattern);
7486 2
        $fillPattern = $fillPattern === Fill::FILL_NONE ? Fill::FILL_SOLID : $fillPattern;
7487
7488 2
        if ($fillPattern !== Fill::FILL_NONE) {
7489 2
            $style->getFill()->setFillType($fillPattern);
7490
7491 2
            $fillColors = self::getUInt2d($options, 2);
7492
7493
            // bit: 0-6; mask: 0x007F; type
7494 2
            $color1 = (0x007F & $fillColors) >> 0;
7495 2
            $style->getFill()->getStartColor()->setRGB(Xls\Color::map($color1, $this->palette, $this->version)['rgb']);
7496
7497
            // bit: 7-13; mask: 0x3F80; type
7498 2
            $color2 = (0x3F80 & $fillColors) >> 7;
7499 2
            $style->getFill()->getEndColor()->setRGB(Xls\Color::map($color2, $this->palette, $this->version)['rgb']);
7500
        }
7501
    }
7502
7503
    /*private function getCFProtectionStyle(string $options, Style $style): void
7504
    {
7505
    }*/
7506
7507 15
    private function readCFFormula(string $recordData, int $offset, int $size): float|int|string|null
7508
    {
7509
        try {
7510 15
            $formula = substr($recordData, $offset, $size);
7511 15
            $formula = pack('v', $size) . $formula; // prepend the length
7512
7513 15
            $formula = $this->getFormulaFromStructure($formula);
7514 15
            if (is_numeric($formula)) {
7515 13
                return (str_contains($formula, '.')) ? (float) $formula : (int) $formula;
7516
            }
7517
7518 8
            return $formula;
7519
        } catch (PhpSpreadsheetException) {
7520
            return null;
7521
        }
7522
    }
7523
7524 15
    private function setCFRules(array $cellRanges, string $type, string $operator, null|float|int|string $formula1, null|float|int|string $formula2, Style $style): void
7525
    {
7526 15
        foreach ($cellRanges as $cellRange) {
7527 15
            $conditional = new Conditional();
7528 15
            $conditional->setConditionType($type);
7529 15
            $conditional->setOperatorType($operator);
7530 15
            if ($formula1 !== null) {
7531 15
                $conditional->addCondition($formula1);
7532
            }
7533 15
            if ($formula2 !== null) {
7534 6
                $conditional->addCondition($formula2);
7535
            }
7536 15
            $conditional->setStyle($style);
7537
7538 15
            $conditionalStyles = $this->phpSheet->getStyle($cellRange)->getConditionalStyles();
7539 15
            $conditionalStyles[] = $conditional;
7540
7541 15
            $this->phpSheet->getStyle($cellRange)->setConditionalStyles($conditionalStyles);
7542
        }
7543
    }
7544
7545 5
    public function getVersion(): int
7546
    {
7547 5
        return $this->version;
7548
    }
7549
}
7550