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