Passed
Push — master ( 440039...6cc2bb )
by Mark
23:06 queued 12:16
created

Xls::readDataValidation()   C

Complexity

Conditions 14
Paths 162

Size

Total Lines 130
Code Lines 73

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 66
CRAP Score 14.1132

Importance

Changes 0
Metric Value
cc 14
eloc 73
c 0
b 0
f 0
nc 162
nop 0
dl 0
loc 130
ccs 66
cts 72
cp 0.9167
crap 14.1132
rs 5.189

How to fix   Long Method    Complexity   

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