Completed
Push — master ( ac1172...310282 )
by Mark
32s queued 27s
created

Xls::verifyPassword()   B

Complexity

Conditions 8
Paths 52

Size

Total Lines 73
Code Lines 48

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

Changes 0
Metric Value
cc 8
eloc 48
nc 52
nop 5
dl 0
loc 73
ccs 0
cts 49
cp 0
crap 72
rs 7.8901
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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