Passed
Pull Request — master (#3358)
by Mark
14:12 queued 02:39
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 97
    public function __construct()
424
    {
425 97
        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 82
    protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
633
    {
634
        // Read the OLE file
635 82
        $this->loadOLE($filename);
636
637
        // Initialisations
638 82
        $this->spreadsheet = new Spreadsheet();
639 82
        $this->spreadsheet->removeSheetByIndex(0); // remove 1st sheet
640 82
        if (!$this->readDataOnly) {
641 81
            $this->spreadsheet->removeCellStyleXfByIndex(0); // remove the default style
642 81
            $this->spreadsheet->removeCellXfByIndex(0); // remove the default style
643
        }
644
645
        // Read the summary information stream (containing meta data)
646 82
        $this->readSummaryInformation();
647
648
        // Read the Additional document summary information stream (containing application-specific meta data)
649 82
        $this->readDocumentSummaryInformation();
650
651
        // total byte size of Excel data (workbook global substream + sheet substreams)
652 82
        $this->dataSize = strlen($this->data);
653
654
        // initialize
655 82
        $this->pos = 0;
656 82
        $this->codepage = $this->codepage ?: CodePage::DEFAULT_CODE_PAGE;
657 82
        $this->formats = [];
658 82
        $this->objFonts = [];
659 82
        $this->palette = [];
660 82
        $this->sheets = [];
661 82
        $this->externalBooks = [];
662 82
        $this->ref = [];
663 82
        $this->definedname = [];
664 82
        $this->sst = [];
665 82
        $this->drawingGroupData = '';
666 82
        $this->xfIndex = 0;
667 82
        $this->mapCellXfIndex = [];
668 82
        $this->mapCellStyleXfIndex = [];
669
670
        // Parse Workbook Global Substream
671 82
        while ($this->pos < $this->dataSize) {
672 82
            $code = self::getUInt2d($this->data, $this->pos);
673
674
            switch ($code) {
675 82
                case self::XLS_TYPE_BOF:
676 82
                    $this->readBof();
677
678 82
                    break;
679 82
                case self::XLS_TYPE_FILEPASS:
680
                    $this->readFilepass();
681
682
                    break;
683 82
                case self::XLS_TYPE_CODEPAGE:
684 79
                    $this->readCodepage();
685
686 79
                    break;
687 82
                case self::XLS_TYPE_DATEMODE:
688 81
                    $this->readDateMode();
689
690 81
                    break;
691 82
                case self::XLS_TYPE_FONT:
692 82
                    $this->readFont();
693
694 82
                    break;
695 82
                case self::XLS_TYPE_FORMAT:
696 47
                    $this->readFormat();
697
698 47
                    break;
699 82
                case self::XLS_TYPE_XF:
700 82
                    $this->readXf();
701
702 82
                    break;
703 82
                case self::XLS_TYPE_XFEXT:
704 20
                    $this->readXfExt();
705
706 20
                    break;
707 82
                case self::XLS_TYPE_STYLE:
708 82
                    $this->readStyle();
709
710 82
                    break;
711 82
                case self::XLS_TYPE_PALETTE:
712 55
                    $this->readPalette();
713
714 55
                    break;
715 82
                case self::XLS_TYPE_SHEET:
716 82
                    $this->readSheet();
717
718 82
                    break;
719 82
                case self::XLS_TYPE_EXTERNALBOOK:
720 56
                    $this->readExternalBook();
721
722 56
                    break;
723 82
                case self::XLS_TYPE_EXTERNNAME:
724
                    $this->readExternName();
725
726
                    break;
727 82
                case self::XLS_TYPE_EXTERNSHEET:
728 57
                    $this->readExternSheet();
729
730 57
                    break;
731 82
                case self::XLS_TYPE_DEFINEDNAME:
732 14
                    $this->readDefinedName();
733
734 14
                    break;
735 82
                case self::XLS_TYPE_MSODRAWINGGROUP:
736 17
                    $this->readMsoDrawingGroup();
737
738 17
                    break;
739 82
                case self::XLS_TYPE_SST:
740 78
                    $this->readSst();
741
742 78
                    break;
743 82
                case self::XLS_TYPE_EOF:
744 82
                    $this->readDefault();
745
746 82
                    break 2;
747
                default:
748 82
                    $this->readDefault();
749
750 82
                    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 82
        if (!$this->readDataOnly) {
757 81
            foreach ($this->objFonts as $objFont) {
758 81
                if (isset($objFont->colorIndex)) {
759 81
                    $color = Xls\Color::map($objFont->colorIndex, $this->palette, $this->version);
760 81
                    $objFont->getColor()->setRGB($color['rgb']);
761
                }
762
            }
763
764 81
            foreach ($this->spreadsheet->getCellXfCollection() as $objStyle) {
765
                // fill start and end color
766 81
                $fill = $objStyle->getFill();
767
768 81
                if (isset($fill->startcolorIndex)) {
769 81
                    $startColor = Xls\Color::map($fill->startcolorIndex, $this->palette, $this->version);
770 81
                    $fill->getStartColor()->setRGB($startColor['rgb']);
771
                }
772 81
                if (isset($fill->endcolorIndex)) {
773 81
                    $endColor = Xls\Color::map($fill->endcolorIndex, $this->palette, $this->version);
774 81
                    $fill->getEndColor()->setRGB($endColor['rgb']);
775
                }
776
777
                // border colors
778 81
                $top = $objStyle->getBorders()->getTop();
779 81
                $right = $objStyle->getBorders()->getRight();
780 81
                $bottom = $objStyle->getBorders()->getBottom();
781 81
                $left = $objStyle->getBorders()->getLeft();
782 81
                $diagonal = $objStyle->getBorders()->getDiagonal();
783
784 81
                if (isset($top->colorIndex)) {
785 81
                    $borderTopColor = Xls\Color::map($top->colorIndex, $this->palette, $this->version);
786 81
                    $top->getColor()->setRGB($borderTopColor['rgb']);
787
                }
788 81
                if (isset($right->colorIndex)) {
789 81
                    $borderRightColor = Xls\Color::map($right->colorIndex, $this->palette, $this->version);
790 81
                    $right->getColor()->setRGB($borderRightColor['rgb']);
791
                }
792 81
                if (isset($bottom->colorIndex)) {
793 81
                    $borderBottomColor = Xls\Color::map($bottom->colorIndex, $this->palette, $this->version);
794 81
                    $bottom->getColor()->setRGB($borderBottomColor['rgb']);
795
                }
796 81
                if (isset($left->colorIndex)) {
797 81
                    $borderLeftColor = Xls\Color::map($left->colorIndex, $this->palette, $this->version);
798 81
                    $left->getColor()->setRGB($borderLeftColor['rgb']);
799
                }
800 81
                if (isset($diagonal->colorIndex)) {
801 80
                    $borderDiagonalColor = Xls\Color::map($diagonal->colorIndex, $this->palette, $this->version);
802 80
                    $diagonal->getColor()->setRGB($borderDiagonalColor['rgb']);
803
                }
804
            }
805
        }
806
807
        // treat MSODRAWINGGROUP records, workbook-level Escher
808 82
        $escherWorkbook = null;
809 82
        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 82
        foreach ($this->sheets as $sheet) {
817 82
            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 82
            if (isset($this->loadSheetsOnly) && !in_array($sheet['name'], $this->loadSheetsOnly)) {
824 5
                continue;
825
            }
826
827
            // add sheet to PhpSpreadsheet object
828 82
            $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 82
            $this->phpSheet->setTitle($sheet['name'], false, false);
833 82
            $this->phpSheet->setSheetState($sheet['sheetState']);
834
835 82
            $this->pos = $sheet['offset'];
836
837
            // Initialize isFitToPages. May change after reading SHEETPR record.
838 82
            $this->isFitToPages = false;
839
840
            // Initialize drawingData
841 82
            $this->drawingData = '';
842
843
            // Initialize objs
844 82
            $this->objs = [];
845
846
            // Initialize shared formula parts
847 82
            $this->sharedFormulaParts = [];
848
849
            // Initialize shared formulas
850 82
            $this->sharedFormulas = [];
851
852
            // Initialize text objs
853 82
            $this->textObjects = [];
854
855
            // Initialize cell annotations
856 82
            $this->cellNotes = [];
857 82
            $this->textObjRef = -1;
858
859 82
            while ($this->pos <= $this->dataSize - 4) {
860 82
                $code = self::getUInt2d($this->data, $this->pos);
861
862
                switch ($code) {
863 82
                    case self::XLS_TYPE_BOF:
864 82
                        $this->readBof();
865
866 82
                        break;
867 82
                    case self::XLS_TYPE_PRINTGRIDLINES:
868 79
                        $this->readPrintGridlines();
869
870 79
                        break;
871 82
                    case self::XLS_TYPE_DEFAULTROWHEIGHT:
872 47
                        $this->readDefaultRowHeight();
873
874 47
                        break;
875 82
                    case self::XLS_TYPE_SHEETPR:
876 81
                        $this->readSheetPr();
877
878 81
                        break;
879 82
                    case self::XLS_TYPE_HORIZONTALPAGEBREAKS:
880 4
                        $this->readHorizontalPageBreaks();
881
882 4
                        break;
883 82
                    case self::XLS_TYPE_VERTICALPAGEBREAKS:
884 4
                        $this->readVerticalPageBreaks();
885
886 4
                        break;
887 82
                    case self::XLS_TYPE_HEADER:
888 79
                        $this->readHeader();
889
890 79
                        break;
891 82
                    case self::XLS_TYPE_FOOTER:
892 79
                        $this->readFooter();
893
894 79
                        break;
895 82
                    case self::XLS_TYPE_HCENTER:
896 79
                        $this->readHcenter();
897
898 79
                        break;
899 82
                    case self::XLS_TYPE_VCENTER:
900 79
                        $this->readVcenter();
901
902 79
                        break;
903 82
                    case self::XLS_TYPE_LEFTMARGIN:
904 64
                        $this->readLeftMargin();
905
906 64
                        break;
907 82
                    case self::XLS_TYPE_RIGHTMARGIN:
908 64
                        $this->readRightMargin();
909
910 64
                        break;
911 82
                    case self::XLS_TYPE_TOPMARGIN:
912 64
                        $this->readTopMargin();
913
914 64
                        break;
915 82
                    case self::XLS_TYPE_BOTTOMMARGIN:
916 64
                        $this->readBottomMargin();
917
918 64
                        break;
919 82
                    case self::XLS_TYPE_PAGESETUP:
920 81
                        $this->readPageSetup();
921
922 81
                        break;
923 82
                    case self::XLS_TYPE_PROTECT:
924 6
                        $this->readProtect();
925
926 6
                        break;
927 82
                    case self::XLS_TYPE_SCENPROTECT:
928
                        $this->readScenProtect();
929
930
                        break;
931 82
                    case self::XLS_TYPE_OBJECTPROTECT:
932 1
                        $this->readObjectProtect();
933
934 1
                        break;
935 82
                    case self::XLS_TYPE_PASSWORD:
936 2
                        $this->readPassword();
937
938 2
                        break;
939 82
                    case self::XLS_TYPE_DEFCOLWIDTH:
940 80
                        $this->readDefColWidth();
941
942 80
                        break;
943 82
                    case self::XLS_TYPE_COLINFO:
944 74
                        $this->readColInfo();
945
946 74
                        break;
947 82
                    case self::XLS_TYPE_DIMENSION:
948 82
                        $this->readDefault();
949
950 82
                        break;
951 82
                    case self::XLS_TYPE_ROW:
952 54
                        $this->readRow();
953
954 54
                        break;
955 82
                    case self::XLS_TYPE_DBCELL:
956 37
                        $this->readDefault();
957
958 37
                        break;
959 82
                    case self::XLS_TYPE_RK:
960 26
                        $this->readRk();
961
962 26
                        break;
963 82
                    case self::XLS_TYPE_LABELSST:
964 55
                        $this->readLabelSst();
965
966 55
                        break;
967 82
                    case self::XLS_TYPE_MULRK:
968 19
                        $this->readMulRk();
969
970 19
                        break;
971 82
                    case self::XLS_TYPE_NUMBER:
972 42
                        $this->readNumber();
973
974 42
                        break;
975 82
                    case self::XLS_TYPE_FORMULA:
976 24
                        $this->readFormula();
977
978 24
                        break;
979 82
                    case self::XLS_TYPE_SHAREDFMLA:
980
                        $this->readSharedFmla();
981
982
                        break;
983 82
                    case self::XLS_TYPE_BOOLERR:
984 10
                        $this->readBoolErr();
985
986 10
                        break;
987 82
                    case self::XLS_TYPE_MULBLANK:
988 24
                        $this->readMulBlank();
989
990 24
                        break;
991 82
                    case self::XLS_TYPE_LABEL:
992 2
                        $this->readLabel();
993
994 2
                        break;
995 82
                    case self::XLS_TYPE_BLANK:
996 21
                        $this->readBlank();
997
998 21
                        break;
999 82
                    case self::XLS_TYPE_MSODRAWING:
1000 16
                        $this->readMsoDrawing();
1001
1002 16
                        break;
1003 82
                    case self::XLS_TYPE_OBJ:
1004 12
                        $this->readObj();
1005
1006 12
                        break;
1007 82
                    case self::XLS_TYPE_WINDOW2:
1008 82
                        $this->readWindow2();
1009
1010 82
                        break;
1011 82
                    case self::XLS_TYPE_PAGELAYOUTVIEW:
1012 55
                        $this->readPageLayoutView();
1013
1014 55
                        break;
1015 82
                    case self::XLS_TYPE_SCL:
1016 5
                        $this->readScl();
1017
1018 5
                        break;
1019 82
                    case self::XLS_TYPE_PANE:
1020 8
                        $this->readPane();
1021
1022 8
                        break;
1023 82
                    case self::XLS_TYPE_SELECTION:
1024 79
                        $this->readSelection();
1025
1026 79
                        break;
1027 82
                    case self::XLS_TYPE_MERGEDCELLS:
1028 18
                        $this->readMergedCells();
1029
1030 18
                        break;
1031 82
                    case self::XLS_TYPE_HYPERLINK:
1032 5
                        $this->readHyperLink();
1033
1034 5
                        break;
1035 82
                    case self::XLS_TYPE_DATAVALIDATIONS:
1036 2
                        $this->readDataValidations();
1037
1038 2
                        break;
1039 82
                    case self::XLS_TYPE_DATAVALIDATION:
1040 2
                        $this->readDataValidation();
1041
1042 2
                        break;
1043 82
                    case self::XLS_TYPE_CFHEADER:
1044 15
                        $cellRangeAddresses = $this->readCFHeader();
1045
1046 15
                        break;
1047 82
                    case self::XLS_TYPE_CFRULE:
1048 15
                        $this->readCFRule($cellRangeAddresses ?? []);
1049
1050 15
                        break;
1051 82
                    case self::XLS_TYPE_SHEETLAYOUT:
1052 5
                        $this->readSheetLayout();
1053
1054 5
                        break;
1055 82
                    case self::XLS_TYPE_SHEETPROTECTION:
1056 65
                        $this->readSheetProtection();
1057
1058 65
                        break;
1059 82
                    case self::XLS_TYPE_RANGEPROTECTION:
1060 1
                        $this->readRangeProtection();
1061
1062 1
                        break;
1063 82
                    case self::XLS_TYPE_NOTE:
1064 3
                        $this->readNote();
1065
1066 3
                        break;
1067 82
                    case self::XLS_TYPE_TXO:
1068 2
                        $this->readTextObject();
1069
1070 2
                        break;
1071 82
                    case self::XLS_TYPE_CONTINUE:
1072 1
                        $this->readContinue();
1073
1074 1
                        break;
1075 82
                    case self::XLS_TYPE_EOF:
1076 82
                        $this->readDefault();
1077
1078 82
                        break 2;
1079
                    default:
1080 81
                        $this->readDefault();
1081
1082 81
                        break;
1083
                }
1084
            }
1085
1086
            // treat MSODRAWING records, sheet-level Escher
1087 82
            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 82
            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 82
            if ($this->version == self::XLS_BIFF8) {
1202 81
                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 82
            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 82
        foreach ($this->definedname as $definedName) {
1230 13
            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 8
                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 82
        $this->data = '';
1319
1320 82
        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 85
    private function readRecordData($data, $pos, $len)
1333
    {
1334 85
        $data = substr($data, $pos, $len);
1335
1336
        // File not encrypted, or record before encryption start point
1337 85
        if ($this->encryption == self::MS_BIFF_CRYPTO_NONE || $pos < $this->encryptionStartPos) {
1338 85
            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 85
    private function loadOLE($filename): void
1385
    {
1386
        // OLE reader
1387 85
        $ole = new OLERead();
1388
        // get excel data,
1389 85
        $ole->read($filename);
1390
        // Get workbook data: workbook stream + sheet streams
1391 85
        $this->data = $ole->getStream($ole->wrkbook); // @phpstan-ignore-line
1392
        // Get summary information data
1393 85
        $this->summaryInformation = $ole->getStream($ole->summaryInformation);
1394
        // Get additional document summary information data
1395 85
        $this->documentSummaryInformation = $ole->getStream($ole->documentSummaryInformation);
1396
    }
1397
1398
    /**
1399
     * Read summary information.
1400
     */
1401 82
    private function readSummaryInformation(): void
1402
    {
1403 82
        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 79
        $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 79
        $secOffset = self::getInt4d($this->summaryInformation, 44);
1418
1419
        // section header
1420
        // offset: $secOffset; size: 4; section length
1421 79
        $secLength = self::getInt4d($this->summaryInformation, $secOffset);
1422
1423
        // offset: $secOffset+4; size: 4; property count
1424 79
        $countProperties = self::getInt4d($this->summaryInformation, $secOffset + 4);
1425
1426
        // initialize code page (used to resolve string values)
1427 79
        $codePage = 'CP1252';
1428
1429
        // offset: ($secOffset+8); size: var
1430
        // loop through property decarations and properties
1431 79
        for ($i = 0; $i < $countProperties; ++$i) {
1432
            // offset: ($secOffset+8) + (8 * $i); size: 4; property ID
1433 79
            $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 79
            $offset = self::getInt4d($this->summaryInformation, ($secOffset + 12) + (8 * $i));
1438
1439 79
            $type = self::getInt4d($this->summaryInformation, $secOffset + $offset);
1440
1441
            // initialize property value
1442 79
            $value = null;
1443
1444
            // extract property value based on property type
1445
            switch ($type) {
1446 79
                case 0x02: // 2 byte signed integer
1447 79
                    $value = self::getUInt2d($this->summaryInformation, $secOffset + 4 + $offset);
1448
1449 79
                    break;
1450 79
                case 0x03: // 4 byte signed integer
1451 69
                    $value = self::getInt4d($this->summaryInformation, $secOffset + 4 + $offset);
1452
1453 69
                    break;
1454 79
                case 0x13: // 4 byte unsigned integer
1455
                    // not needed yet, fix later if necessary
1456 1
                    break;
1457 79
                case 0x1E: // null-terminated string prepended by dword string length
1458 77
                    $byteLength = self::getInt4d($this->summaryInformation, $secOffset + 4 + $offset);
1459 77
                    $value = substr($this->summaryInformation, $secOffset + 8 + $offset, $byteLength);
1460 77
                    $value = StringHelper::convertEncoding($value, 'UTF-8', $codePage);
1461 77
                    $value = rtrim($value);
1462
1463 77
                    break;
1464 79
                case 0x40: // Filetime (64-bit value representing the number of 100-nanosecond intervals since January 1, 1601)
1465
                    // PHP-time
1466 79
                    $value = OLE::OLE2LocalDate(substr($this->summaryInformation, $secOffset + 4 + $offset, 8));
1467
1468 79
                    break;
1469 2
                case 0x47: // Clipboard format
1470
                    // not needed yet, fix later if necessary
1471
                    break;
1472
            }
1473
1474
            switch ($id) {
1475 79
                case 0x01:    //    Code Page
1476 79
                    $codePage = CodePage::numberToName((int) $value);
1477
1478 79
                    break;
1479 79
                case 0x02:    //    Title
1480 45
                    $this->spreadsheet->getProperties()->setTitle("$value");
1481
1482 45
                    break;
1483 79
                case 0x03:    //    Subject
1484 13
                    $this->spreadsheet->getProperties()->setSubject("$value");
1485
1486 13
                    break;
1487 79
                case 0x04:    //    Author (Creator)
1488 72
                    $this->spreadsheet->getProperties()->setCreator("$value");
1489
1490 72
                    break;
1491 79
                case 0x05:    //    Keywords
1492 13
                    $this->spreadsheet->getProperties()->setKeywords("$value");
1493
1494 13
                    break;
1495 79
                case 0x06:    //    Comments (Description)
1496 13
                    $this->spreadsheet->getProperties()->setDescription("$value");
1497
1498 13
                    break;
1499 79
                case 0x07:    //    Template
1500
                    //    Not supported by PhpSpreadsheet
1501
                    break;
1502 79
                case 0x08:    //    Last Saved By (LastModifiedBy)
1503 71
                    $this->spreadsheet->getProperties()->setLastModifiedBy("$value");
1504
1505 71
                    break;
1506 79
                case 0x09:    //    Revision
1507
                    //    Not supported by PhpSpreadsheet
1508 9
                    break;
1509 79
                case 0x0A:    //    Total Editing Time
1510
                    //    Not supported by PhpSpreadsheet
1511 9
                    break;
1512 79
                case 0x0B:    //    Last Printed
1513
                    //    Not supported by PhpSpreadsheet
1514 13
                    break;
1515 79
                case 0x0C:    //    Created Date/Time
1516 79
                    $this->spreadsheet->getProperties()->setCreated($value);
1517
1518 79
                    break;
1519 79
                case 0x0D:    //    Modified Date/Time
1520 79
                    $this->spreadsheet->getProperties()->setModified($value);
1521
1522 79
                    break;
1523 70
                case 0x0E:    //    Number of Pages
1524
                    //    Not supported by PhpSpreadsheet
1525
                    break;
1526 70
                case 0x0F:    //    Number of Words
1527
                    //    Not supported by PhpSpreadsheet
1528
                    break;
1529 70
                case 0x10:    //    Number of Characters
1530
                    //    Not supported by PhpSpreadsheet
1531
                    break;
1532 70
                case 0x11:    //    Thumbnail
1533
                    //    Not supported by PhpSpreadsheet
1534
                    break;
1535 70
                case 0x12:    //    Name of creating application
1536
                    //    Not supported by PhpSpreadsheet
1537 28
                    break;
1538 70
                case 0x13:    //    Security
1539
                    //    Not supported by PhpSpreadsheet
1540 69
                    break;
1541
            }
1542
        }
1543
    }
1544
1545
    /**
1546
     * Read additional document summary information.
1547
     */
1548 82
    private function readDocumentSummaryInformation(): void
1549
    {
1550 82
        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 79
        $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 79
        $secOffset = self::getInt4d($this->documentSummaryInformation, 44);
1565
1566
        //    section header
1567
        //    offset: $secOffset;    size: 4;    section length
1568 79
        $secLength = self::getInt4d($this->documentSummaryInformation, $secOffset);
1569
1570
        //    offset: $secOffset+4;    size: 4;    property count
1571 79
        $countProperties = self::getInt4d($this->documentSummaryInformation, $secOffset + 4);
1572
1573
        // initialize code page (used to resolve string values)
1574 79
        $codePage = 'CP1252';
1575
1576
        //    offset: ($secOffset+8);    size: var
1577
        //    loop through property decarations and properties
1578 79
        for ($i = 0; $i < $countProperties; ++$i) {
1579
            //    offset: ($secOffset+8) + (8 * $i);    size: 4;    property ID
1580 79
            $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 79
            $offset = self::getInt4d($this->documentSummaryInformation, ($secOffset + 12) + (8 * $i));
1585
1586 79
            $type = self::getInt4d($this->documentSummaryInformation, $secOffset + $offset);
1587
1588
            // initialize property value
1589 79
            $value = null;
1590
1591
            // extract property value based on property type
1592
            switch ($type) {
1593 79
                case 0x02:    //    2 byte signed integer
1594 79
                    $value = self::getUInt2d($this->documentSummaryInformation, $secOffset + 4 + $offset);
1595
1596 79
                    break;
1597 70
                case 0x03:    //    4 byte signed integer
1598 68
                    $value = self::getInt4d($this->documentSummaryInformation, $secOffset + 4 + $offset);
1599
1600 68
                    break;
1601 70
                case 0x0B:  // Boolean
1602 70
                    $value = self::getUInt2d($this->documentSummaryInformation, $secOffset + 4 + $offset);
1603 70
                    $value = ($value == 0 ? false : true);
1604
1605 70
                    break;
1606 69
                case 0x13:    //    4 byte unsigned integer
1607
                    // not needed yet, fix later if necessary
1608 1
                    break;
1609 68
                case 0x1E:    //    null-terminated string prepended by dword string length
1610 32
                    $byteLength = self::getInt4d($this->documentSummaryInformation, $secOffset + 4 + $offset);
1611 32
                    $value = substr($this->documentSummaryInformation, $secOffset + 8 + $offset, $byteLength);
1612 32
                    $value = StringHelper::convertEncoding($value, 'UTF-8', $codePage);
1613 32
                    $value = rtrim($value);
1614
1615 32
                    break;
1616 68
                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 68
                case 0x47:    //    Clipboard format
1622
                    // not needed yet, fix later if necessary
1623
                    break;
1624
            }
1625
1626
            switch ($id) {
1627 79
                case 0x01:    //    Code Page
1628 79
                    $codePage = CodePage::numberToName((int) $value);
1629
1630 79
                    break;
1631 70
                case 0x02:    //    Category
1632 13
                    $this->spreadsheet->getProperties()->setCategory("$value");
1633
1634 13
                    break;
1635 70
                case 0x03:    //    Presentation Target
1636
                    //    Not supported by PhpSpreadsheet
1637
                    break;
1638 70
                case 0x04:    //    Bytes
1639
                    //    Not supported by PhpSpreadsheet
1640
                    break;
1641 70
                case 0x05:    //    Lines
1642
                    //    Not supported by PhpSpreadsheet
1643
                    break;
1644 70
                case 0x06:    //    Paragraphs
1645
                    //    Not supported by PhpSpreadsheet
1646
                    break;
1647 70
                case 0x07:    //    Slides
1648
                    //    Not supported by PhpSpreadsheet
1649
                    break;
1650 70
                case 0x08:    //    Notes
1651
                    //    Not supported by PhpSpreadsheet
1652
                    break;
1653 70
                case 0x09:    //    Hidden Slides
1654
                    //    Not supported by PhpSpreadsheet
1655
                    break;
1656 70
                case 0x0A:    //    MM Clips
1657
                    //    Not supported by PhpSpreadsheet
1658
                    break;
1659 70
                case 0x0B:    //    Scale Crop
1660
                    //    Not supported by PhpSpreadsheet
1661 70
                    break;
1662 70
                case 0x0C:    //    Heading Pairs
1663
                    //    Not supported by PhpSpreadsheet
1664 68
                    break;
1665 70
                case 0x0D:    //    Titles of Parts
1666
                    //    Not supported by PhpSpreadsheet
1667 68
                    break;
1668 70
                case 0x0E:    //    Manager
1669 2
                    $this->spreadsheet->getProperties()->setManager("$value");
1670
1671 2
                    break;
1672 70
                case 0x0F:    //    Company
1673 24
                    $this->spreadsheet->getProperties()->setCompany("$value");
1674
1675 24
                    break;
1676 70
                case 0x10:    //    Links up-to-date
1677
                    //    Not supported by PhpSpreadsheet
1678 70
                    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 85
    private function readDefault(): void
1687
    {
1688 85
        $length = self::getUInt2d($this->data, $this->pos + 2);
1689
1690
        // move stream pointer to next record
1691 85
        $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 85
    private function readBof(): void
1800
    {
1801 85
        $length = self::getUInt2d($this->data, $this->pos + 2);
1802 85
        $recordData = substr($this->data, $this->pos + 4, $length);
1803
1804
        // move stream pointer to next record
1805 85
        $this->pos += 4 + $length;
1806
1807
        // offset: 2; size: 2; type of the following data
1808 85
        $substreamType = self::getUInt2d($recordData, 2);
1809
1810
        switch ($substreamType) {
1811 85
            case self::XLS_WORKBOOKGLOBALS:
1812 85
                $version = self::getUInt2d($recordData, 0);
1813 85
                if (($version != self::XLS_BIFF8) && ($version != self::XLS_BIFF7)) {
1814
                    throw new Exception('Cannot read this Excel file. Version is too old.');
1815
                }
1816 85
                $this->version = $version;
1817
1818 85
                break;
1819 83
            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 83
                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 79
    private function readCodepage(): void
2001
    {
2002 79
        $length = self::getUInt2d($this->data, $this->pos + 2);
2003 79
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2004
2005
        // move stream pointer to next record
2006 79
        $this->pos += 4 + $length;
2007
2008
        // offset: 0; size: 2; code page identifier
2009 79
        $codepage = self::getUInt2d($recordData, 0);
2010
2011 79
        $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 81
    private function readDateMode(): void
2027
    {
2028 81
        $length = self::getUInt2d($this->data, $this->pos + 2);
2029 81
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2030
2031
        // move stream pointer to next record
2032 81
        $this->pos += 4 + $length;
2033
2034
        // offset: 0; size: 2; 0 = base 1900, 1 = base 1904
2035 81
        Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900);
2036 81
        if (ord($recordData[0]) == 1) {
2037
            Date::setExcelCalendar(Date::CALENDAR_MAC_1904);
2038
        }
2039
    }
2040
2041
    /**
2042
     * Read a FONT record.
2043
     */
2044 82
    private function readFont(): void
2045
    {
2046 82
        $length = self::getUInt2d($this->data, $this->pos + 2);
2047 82
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2048
2049
        // move stream pointer to next record
2050 82
        $this->pos += 4 + $length;
2051
2052 82
        if (!$this->readDataOnly) {
2053 81
            $objFont = new Font();
2054
2055
            // offset: 0; size: 2; height of the font (in twips = 1/20 of a point)
2056 81
            $size = self::getUInt2d($recordData, 0);
2057 81
            $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 81
            $isItalic = (0x0002 & self::getUInt2d($recordData, 2)) >> 1;
2063 81
            if ($isItalic) {
2064 28
                $objFont->setItalic(true);
2065
            }
2066
2067
            // bit: 2; mask 0x0004; underlined (redundant in BIFF5-BIFF8)
2068
            // bit: 3; mask 0x0008; strikethrough
2069 81
            $isStrike = (0x0008 & self::getUInt2d($recordData, 2)) >> 3;
2070 81
            if ($isStrike) {
2071
                $objFont->setStrikethrough(true);
2072
            }
2073
2074
            // offset: 4; size: 2; colour index
2075 81
            $colorIndex = self::getUInt2d($recordData, 4);
2076 81
            $objFont->colorIndex = $colorIndex;
2077
2078
            // offset: 6; size: 2; font weight
2079 81
            $weight = self::getUInt2d($recordData, 6);
2080
            switch ($weight) {
2081 81
                case 0x02BC:
2082 47
                    $objFont->setBold(true);
2083
2084 47
                    break;
2085
            }
2086
2087
            // offset: 8; size: 2; escapement type
2088 81
            $escapement = self::getUInt2d($recordData, 8);
2089 81
            CellFont::escapement($objFont, $escapement);
2090
2091
            // offset: 10; size: 1; underline type
2092 81
            $underlineType = ord($recordData[10]);
2093 81
            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 81
            if ($this->version == self::XLS_BIFF8) {
2100 80
                $string = self::readUnicodeStringShort(substr($recordData, 14));
2101
            } else {
2102 1
                $string = $this->readByteStringShort(substr($recordData, 14));
2103
            }
2104 81
            $objFont->setName($string['value']);
2105
2106 81
            $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 47
    private function readFormat(): void
2125
    {
2126 47
        $length = self::getUInt2d($this->data, $this->pos + 2);
2127 47
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2128
2129
        // move stream pointer to next record
2130 47
        $this->pos += 4 + $length;
2131
2132 47
        if (!$this->readDataOnly) {
2133 46
            $indexCode = self::getUInt2d($recordData, 0);
2134
2135 46
            if ($this->version == self::XLS_BIFF8) {
2136 45
                $string = self::readUnicodeStringLong(substr($recordData, 2));
2137
            } else {
2138
                // BIFF7
2139 1
                $string = $this->readByteStringShort(substr($recordData, 2));
2140
            }
2141
2142 46
            $formatString = $string['value'];
2143
            // Apache Open Office sets wrong case writing to xls - issue 2239
2144 46
            if ($formatString === 'GENERAL') {
2145 1
                $formatString = NumberFormat::FORMAT_GENERAL;
2146
            }
2147 46
            $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 82
    private function readXf(): void
2166
    {
2167 82
        $length = self::getUInt2d($this->data, $this->pos + 2);
2168 82
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2169
2170
        // move stream pointer to next record
2171 82
        $this->pos += 4 + $length;
2172
2173 82
        $objStyle = new Style();
2174
2175 82
        if (!$this->readDataOnly) {
2176
            // offset:  0; size: 2; Index to FONT record
2177 81
            if (self::getUInt2d($recordData, 0) < 4) {
2178 81
                $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 45
                $fontIndex = self::getUInt2d($recordData, 0) - 1;
2183
            }
2184 81
            $objStyle->setFont($this->objFonts[$fontIndex]);
2185
2186
            // offset:  2; size: 2; Index to FORMAT record
2187 81
            $numberFormatIndex = self::getUInt2d($recordData, 2);
2188 81
            if (isset($this->formats[$numberFormatIndex])) {
2189
                // then we have user-defined format code
2190 41
                $numberFormat = ['formatCode' => $this->formats[$numberFormatIndex]];
2191 81
            } elseif (($code = NumberFormat::builtInFormatCode($numberFormatIndex)) !== '') {
2192
                // then we have built-in format code
2193 81
                $numberFormat = ['formatCode' => $code];
2194
            } else {
2195
                // we set the general format code
2196 10
                $numberFormat = ['formatCode' => NumberFormat::FORMAT_GENERAL];
2197
            }
2198 81
            $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 81
            $xfTypeProt = self::getUInt2d($recordData, 4);
2203
            // bit 0; mask 0x01; 1 = cell is locked
2204 81
            $isLocked = (0x01 & $xfTypeProt) >> 0;
2205 81
            $objStyle->getProtection()->setLocked($isLocked ? Protection::PROTECTION_INHERIT : Protection::PROTECTION_UNPROTECTED);
2206
2207
            // bit 1; mask 0x02; 1 = Formula is hidden
2208 81
            $isHidden = (0x02 & $xfTypeProt) >> 1;
2209 81
            $objStyle->getProtection()->setHidden($isHidden ? Protection::PROTECTION_PROTECTED : Protection::PROTECTION_UNPROTECTED);
2210
2211
            // bit 2; mask 0x04; 0 = Cell XF, 1 = Cell Style XF
2212 81
            $isCellStyleXf = (0x04 & $xfTypeProt) >> 2;
2213
2214
            // offset:  6; size: 1; Alignment and text break
2215
            // bit 2-0, mask 0x07; horizontal alignment
2216 81
            $horAlign = (0x07 & ord($recordData[6])) >> 0;
2217 81
            Xls\Style\CellAlignment::horizontal($objStyle->getAlignment(), $horAlign);
2218
2219
            // bit 3, mask 0x08; wrap text
2220 81
            $wrapText = (0x08 & ord($recordData[6])) >> 3;
2221 81
            Xls\Style\CellAlignment::wrap($objStyle->getAlignment(), $wrapText);
2222
2223
            // bit 6-4, mask 0x70; vertical alignment
2224 81
            $vertAlign = (0x70 & ord($recordData[6])) >> 4;
2225 81
            Xls\Style\CellAlignment::vertical($objStyle->getAlignment(), $vertAlign);
2226
2227 81
            if ($this->version == self::XLS_BIFF8) {
2228
                // offset:  7; size: 1; XF_ROTATION: Text rotation angle
2229 80
                $angle = ord($recordData[7]);
2230 80
                $rotation = 0;
2231 80
                if ($angle <= 90) {
2232 80
                    $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 80
                $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 80
                $indent = (0x0F & ord($recordData[8])) >> 0;
2243 80
                $objStyle->getAlignment()->setIndent($indent);
2244
2245
                // bit: 4; mask: 0x10; 1 = shrink content to fit into cell
2246 80
                $shrinkToFit = (0x10 & ord($recordData[8])) >> 4;
2247
                switch ($shrinkToFit) {
2248 80
                    case 0:
2249 80
                        $objStyle->getAlignment()->setShrinkToFit(false);
2250
2251 80
                        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 80
                if ($bordersLeftStyle = Xls\Style\Border::lookup((0x0000000F & self::getInt4d($recordData, 10)) >> 0)) {
2263 80
                    $objStyle->getBorders()->getLeft()->setBorderStyle($bordersLeftStyle);
2264
                }
2265
                // bit: 7-4; mask: 0x000000F0; right style
2266 80
                if ($bordersRightStyle = Xls\Style\Border::lookup((0x000000F0 & self::getInt4d($recordData, 10)) >> 4)) {
2267 80
                    $objStyle->getBorders()->getRight()->setBorderStyle($bordersRightStyle);
2268
                }
2269
                // bit: 11-8; mask: 0x00000F00; top style
2270 80
                if ($bordersTopStyle = Xls\Style\Border::lookup((0x00000F00 & self::getInt4d($recordData, 10)) >> 8)) {
2271 80
                    $objStyle->getBorders()->getTop()->setBorderStyle($bordersTopStyle);
2272
                }
2273
                // bit: 15-12; mask: 0x0000F000; bottom style
2274 80
                if ($bordersBottomStyle = Xls\Style\Border::lookup((0x0000F000 & self::getInt4d($recordData, 10)) >> 12)) {
2275 80
                    $objStyle->getBorders()->getBottom()->setBorderStyle($bordersBottomStyle);
2276
                }
2277
                // bit: 22-16; mask: 0x007F0000; left color
2278 80
                $objStyle->getBorders()->getLeft()->colorIndex = (0x007F0000 & self::getInt4d($recordData, 10)) >> 16;
2279
2280
                // bit: 29-23; mask: 0x3F800000; right color
2281 80
                $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 80
                $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 80
                $diagonalUp = ((int) 0x80000000 & self::getInt4d($recordData, 10)) >> 31 ? true : false;
2288
2289 80
                if ($diagonalUp === false) {
2290 80
                    if ($diagonalDown == false) {
2291 80
                        $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_NONE);
2292
                    } else {
2293 80
                        $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 80
                $objStyle->getBorders()->getTop()->colorIndex = (0x0000007F & self::getInt4d($recordData, 14)) >> 0;
2304
2305
                // bit: 13-7; mask: 0x00003F80; bottom color
2306 80
                $objStyle->getBorders()->getBottom()->colorIndex = (0x00003F80 & self::getInt4d($recordData, 14)) >> 7;
2307
2308
                // bit: 20-14; mask: 0x001FC000; diagonal color
2309 80
                $objStyle->getBorders()->getDiagonal()->colorIndex = (0x001FC000 & self::getInt4d($recordData, 14)) >> 14;
2310
2311
                // bit: 24-21; mask: 0x01E00000; diagonal style
2312 80
                if ($bordersDiagonalStyle = Xls\Style\Border::lookup((0x01E00000 & self::getInt4d($recordData, 14)) >> 21)) {
2313 80
                    $objStyle->getBorders()->getDiagonal()->setBorderStyle($bordersDiagonalStyle);
2314
                }
2315
2316
                // bit: 31-26; mask: 0xFC000000 fill pattern
2317 80
                if ($fillType = Xls\Style\FillPattern::lookup(((int) 0xFC000000 & self::getInt4d($recordData, 14)) >> 26)) {
2318 80
                    $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 80
                $objStyle->getFill()->startcolorIndex = (0x007F & self::getUInt2d($recordData, 18)) >> 0;
2323
2324
                // bit: 13-7; mask: 0x3F80; color index for pattern background
2325 80
                $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 81
            if ($isCellStyleXf) {
2395
                // we only read one style XF record which is always the first
2396 81
                if ($this->xfIndex == 0) {
2397 81
                    $this->spreadsheet->addCellStyleXf($objStyle);
2398 81
                    $this->mapCellStyleXfIndex[$this->xfIndex] = 0;
2399
                }
2400
            } else {
2401
                // we read all cell XF records
2402 81
                $this->spreadsheet->addCellXf($objStyle);
2403 81
                $this->mapCellXfIndex[$this->xfIndex] = count($this->spreadsheet->getCellXfCollection()) - 1;
2404
            }
2405
2406
            // update XF index for when we read next record
2407 81
            ++$this->xfIndex;
2408
        }
2409
    }
2410
2411 20
    private function readXfExt(): void
2412
    {
2413 20
        $length = self::getUInt2d($this->data, $this->pos + 2);
2414 20
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2415
2416
        // move stream pointer to next record
2417 20
        $this->pos += 4 + $length;
2418
2419 20
        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 20
            $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 20
            $cexts = self::getUInt2d($recordData, 18);
2435
2436
            // start reading the actual extension data
2437 20
            $offset = 20;
2438 20
            while ($offset < $length) {
2439
                // extension type
2440 20
                $extType = self::getUInt2d($recordData, $offset);
2441
2442
                // extension length
2443 20
                $cb = self::getUInt2d($recordData, $offset + 2);
2444
2445
                // extension data
2446 20
                $extData = substr($recordData, $offset + 4, $cb);
2447
2448
                switch ($extType) {
2449 20
                    case 4:        // fill start color
2450 20
                        $xclfType = self::getUInt2d($extData, 0); // color type
2451 20
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2452
2453 20
                        if ($xclfType == 2) {
2454 18
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2455
2456
                            // modify the relevant style property
2457 18
                            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 20
                        break;
2465 18
                    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 18
                    case 7:        // border color top
2482 18
                        $xclfType = self::getUInt2d($extData, 0); // color type
2483 18
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2484
2485 18
                        if ($xclfType == 2) {
2486 18
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2487
2488
                            // modify the relevant style property
2489 18
                            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 18
                        break;
2497 18
                    case 8:        // border color bottom
2498 18
                        $xclfType = self::getUInt2d($extData, 0); // color type
2499 18
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2500
2501 18
                        if ($xclfType == 2) {
2502 18
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2503
2504
                            // modify the relevant style property
2505 18
                            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 18
                        break;
2513 18
                    case 9:        // border color left
2514 18
                        $xclfType = self::getUInt2d($extData, 0); // color type
2515 18
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2516
2517 18
                        if ($xclfType == 2) {
2518 18
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2519
2520
                            // modify the relevant style property
2521 18
                            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 18
                        break;
2529 18
                    case 10:        // border color right
2530 18
                        $xclfType = self::getUInt2d($extData, 0); // color type
2531 18
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2532
2533 18
                        if ($xclfType == 2) {
2534 18
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2535
2536
                            // modify the relevant style property
2537 18
                            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 18
                        break;
2545 18
                    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 18
                    case 13:    // font color
2562 18
                        $xclfType = self::getUInt2d($extData, 0); // color type
2563 18
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2564
2565 18
                        if ($xclfType == 2) {
2566 18
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2567
2568
                            // modify the relevant style property
2569 18
                            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 18
                        break;
2577
                }
2578
2579 20
                $offset += $cb;
2580
            }
2581
        }
2582
    }
2583
2584
    /**
2585
     * Read STYLE record.
2586
     */
2587 82
    private function readStyle(): void
2588
    {
2589 82
        $length = self::getUInt2d($this->data, $this->pos + 2);
2590 82
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2591
2592
        // move stream pointer to next record
2593 82
        $this->pos += 4 + $length;
2594
2595 82
        if (!$this->readDataOnly) {
2596
            // offset: 0; size: 2; index to XF record and flag for built-in style
2597 81
            $ixfe = self::getUInt2d($recordData, 0);
2598
2599
            // bit: 11-0; mask 0x0FFF; index to XF record
2600 81
            $xfIndex = (0x0FFF & $ixfe) >> 0;
2601
2602
            // bit: 15; mask 0x8000; 0 = user-defined style, 1 = built-in style
2603 81
            $isBuiltIn = (bool) ((0x8000 & $ixfe) >> 15);
2604
2605 81
            if ($isBuiltIn) {
2606
                // offset: 2; size: 1; identifier for built-in style
2607 81
                $builtInId = ord($recordData[2]);
2608
2609
                switch ($builtInId) {
2610 81
                    case 0x00:
2611
                        // currently, we are not using this for anything
2612 81
                        break;
2613
                    default:
2614 40
                        break;
2615
                }
2616
            }
2617
            // user-defined; not supported by PhpSpreadsheet
2618
        }
2619
    }
2620
2621
    /**
2622
     * Read PALETTE record.
2623
     */
2624 55
    private function readPalette(): void
2625
    {
2626 55
        $length = self::getUInt2d($this->data, $this->pos + 2);
2627 55
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2628
2629
        // move stream pointer to next record
2630 55
        $this->pos += 4 + $length;
2631
2632 55
        if (!$this->readDataOnly) {
2633
            // offset: 0; size: 2; number of following colors
2634 55
            $nm = self::getUInt2d($recordData, 0);
2635
2636
            // list of RGB colors
2637 55
            for ($i = 0; $i < $nm; ++$i) {
2638 55
                $rgb = substr($recordData, 2 + 4 * $i, 4);
2639 55
                $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 85
    private function readSheet(): void
2657
    {
2658 85
        $length = self::getUInt2d($this->data, $this->pos + 2);
2659 85
        $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 85
        $rec_offset = self::getInt4d($this->data, $this->pos + 4);
2664
2665
        // move stream pointer to next record
2666 85
        $this->pos += 4 + $length;
2667
2668
        // offset: 4; size: 1; sheet state
2669 85
        switch (ord($recordData[4])) {
2670 85
            case 0x00:
2671 85
                $sheetState = Worksheet::SHEETSTATE_VISIBLE;
2672
2673 85
                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 85
        $sheetType = ord($recordData[5]);
2690
2691
        // offset: 6; size: var; sheet name
2692 85
        $rec_name = null;
2693 85
        if ($this->version == self::XLS_BIFF8) {
2694 84
            $string = self::readUnicodeStringShort(substr($recordData, 6));
2695 84
            $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 85
        $this->sheets[] = [
2702 85
            'name' => $rec_name,
2703 85
            'offset' => $rec_offset,
2704 85
            'sheetState' => $sheetState,
2705 85
            'sheetType' => $sheetType,
2706 85
        ];
2707
    }
2708
2709
    /**
2710
     * Read EXTERNALBOOK record.
2711
     */
2712 56
    private function readExternalBook(): void
2713
    {
2714 56
        $length = self::getUInt2d($this->data, $this->pos + 2);
2715 56
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2716
2717
        // move stream pointer to next record
2718 56
        $this->pos += 4 + $length;
2719
2720
        // offset within record data
2721 56
        $offset = 0;
2722
2723
        // there are 4 types of records
2724 56
        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 56
        } 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 56
            $this->externalBooks[] = [
2753 56
                'type' => 'internal',
2754 56
            ];
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 57
    private function readExternSheet(): void
2809
    {
2810 57
        $length = self::getUInt2d($this->data, $this->pos + 2);
2811 57
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2812
2813
        // move stream pointer to next record
2814 57
        $this->pos += 4 + $length;
2815
2816
        // external sheet references provided for named cells
2817 57
        if ($this->version == self::XLS_BIFF8) {
2818
            // offset: 0; size: 2; number of following ref structures
2819 56
            $nm = self::getUInt2d($recordData, 0);
2820 56
            for ($i = 0; $i < $nm; ++$i) {
2821 54
                $this->ref[] = [
2822
                    // offset: 2 + 6 * $i; index to EXTERNALBOOK record
2823 54
                    'externalBookIndex' => self::getUInt2d($recordData, 2 + 6 * $i),
2824
                    // offset: 4 + 6 * $i; index to first sheet in EXTERNALBOOK record
2825 54
                    'firstSheetIndex' => self::getUInt2d($recordData, 4 + 6 * $i),
2826
                    // offset: 6 + 6 * $i; index to last sheet in EXTERNALBOOK record
2827 54
                    'lastSheetIndex' => self::getUInt2d($recordData, 6 + 6 * $i),
2828 54
                ];
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 14
    private function readDefinedName(): void
2845
    {
2846 14
        $length = self::getUInt2d($this->data, $this->pos + 2);
2847 14
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2848
2849
        // move stream pointer to next record
2850 14
        $this->pos += 4 + $length;
2851
2852 14
        if ($this->version == self::XLS_BIFF8) {
2853
            // retrieves named cells
2854
2855
            // offset: 0; size: 2; option flags
2856 13
            $opts = self::getUInt2d($recordData, 0);
2857
2858
            // bit: 5; mask: 0x0020; 0 = user-defined name, 1 = built-in-name
2859 13
            $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 13
            $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 13
            $flen = self::getUInt2d($recordData, 4);
2869
2870
            // offset: 8; size: 2; 0=Global name, otherwise index to sheet (1-based)
2871 13
            $scope = self::getUInt2d($recordData, 8);
2872
2873
            // offset: 14; size: var; Name (Unicode string without length field)
2874 13
            $string = self::readUnicodeString(substr($recordData, 14), $nlen);
2875
2876
            // offset: var; size: $flen; formula data
2877 13
            $offset = 14 + $string['size'];
2878 13
            $formulaStructure = pack('v', $flen) . substr($recordData, $offset);
2879
2880
            try {
2881 13
                $formula = $this->getFormulaFromStructure($formulaStructure);
2882 1
            } catch (PhpSpreadsheetException $e) {
2883 1
                $formula = '';
2884
            }
2885
2886 13
            $this->definedname[] = [
2887 13
                'isBuiltInName' => $isBuiltInName,
2888 13
                'name' => $string['value'],
2889 13
                'formula' => $formula,
2890 13
                'scope' => $scope,
2891 13
            ];
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 78
    private function readSst(): void
2921
    {
2922
        // offset within (spliced) record data
2923 78
        $pos = 0;
2924
2925
        // Limit global SST position, further control for bad SST Length in BIFF8 data
2926 78
        $limitposSST = 0;
2927
2928
        // get spliced record data
2929 78
        $splicedRecordData = $this->getSplicedRecordData();
2930
2931 78
        $recordData = $splicedRecordData['recordData'];
2932 78
        $spliceOffsets = $splicedRecordData['spliceOffsets'];
2933
2934
        // offset: 0; size: 4; total number of strings in the workbook
2935 78
        $pos += 4;
2936
2937
        // offset: 4; size: 4; number of following strings ($nm)
2938 78
        $nm = self::getInt4d($recordData, 4);
2939 78
        $pos += 4;
2940
2941
        // look up limit position
2942 78
        foreach ($spliceOffsets as $spliceOffset) {
2943
            // it can happen that the string is empty, therefore we need
2944
            // <= and not just <
2945 78
            if ($pos <= $spliceOffset) {
2946 78
                $limitposSST = $spliceOffset;
2947
            }
2948
        }
2949
2950
        // loop through the Unicode strings (16-bit length)
2951 78
        for ($i = 0; $i < $nm && $pos < $limitposSST; ++$i) {
2952
            // number of characters in the Unicode string
2953 56
            $numChars = self::getUInt2d($recordData, $pos);
2954 56
            $pos += 2;
2955
2956
            // option flags
2957 56
            $optionFlags = ord($recordData[$pos]);
2958 56
            ++$pos;
2959
2960
            // bit: 0; mask: 0x01; 0 = compressed; 1 = uncompressed
2961 56
            $isCompressed = (($optionFlags & 0x01) == 0);
2962
2963
            // bit: 2; mask: 0x02; 0 = ordinary; 1 = Asian phonetic
2964 56
            $hasAsian = (($optionFlags & 0x04) != 0);
2965
2966
            // bit: 3; mask: 0x03; 0 = ordinary; 1 = Rich-Text
2967 56
            $hasRichText = (($optionFlags & 0x08) != 0);
2968
2969 56
            $formattingRuns = 0;
2970 56
            if ($hasRichText) {
2971
                // number of Rich-Text formatting runs
2972 5
                $formattingRuns = self::getUInt2d($recordData, $pos);
2973 5
                $pos += 2;
2974
            }
2975
2976 56
            $extendedRunLength = 0;
2977 56
            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 56
            $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 56
            $limitpos = null;
2988 56
            foreach ($spliceOffsets as $spliceOffset) {
2989
                // it can happen that the string is empty, therefore we need
2990
                // <= and not just <
2991 56
                if ($pos <= $spliceOffset) {
2992 56
                    $limitpos = $spliceOffset;
2993
2994 56
                    break;
2995
                }
2996
            }
2997
2998 56
            if ($pos + $len <= $limitpos) {
2999
                // character array is not split between records
3000
3001 56
                $retstr = substr($recordData, $pos, $len);
3002 56
                $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 56
            $retstr = self::encodeUTF16($retstr, $isCompressed);
3077
3078
            // read additional Rich-Text information, if any
3079 56
            $fmtRuns = [];
3080 56
            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 56
            if ($hasAsian) {
3099
                // For Asian phonetic settings, we skip the extended string data
3100
                $pos += $extendedRunLength;
3101
            }
3102
3103
            // store the shared sting
3104 56
            $this->sst[] = [
3105 56
                'value' => $retstr,
3106 56
                'fmtRuns' => $fmtRuns,
3107 56
            ];
3108
        }
3109
3110
        // getSplicedRecordData() takes care of moving current position in data stream
3111
    }
3112
3113
    /**
3114
     * Read PRINTGRIDLINES record.
3115
     */
3116 79
    private function readPrintGridlines(): void
3117
    {
3118 79
        $length = self::getUInt2d($this->data, $this->pos + 2);
3119 79
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3120
3121
        // move stream pointer to next record
3122 79
        $this->pos += 4 + $length;
3123
3124 79
        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 77
            $printGridlines = (bool) self::getUInt2d($recordData, 0);
3127 77
            $this->phpSheet->setPrintGridlines($printGridlines);
3128
        }
3129
    }
3130
3131
    /**
3132
     * Read DEFAULTROWHEIGHT record.
3133
     */
3134 47
    private function readDefaultRowHeight(): void
3135
    {
3136 47
        $length = self::getUInt2d($this->data, $this->pos + 2);
3137 47
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3138
3139
        // move stream pointer to next record
3140 47
        $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 47
        $height = self::getUInt2d($recordData, 2);
3145 47
        $this->phpSheet->getDefaultRowDimension()->setRowHeight($height / 20);
3146
    }
3147
3148
    /**
3149
     * Read SHEETPR record.
3150
     */
3151 81
    private function readSheetPr(): void
3152
    {
3153 81
        $length = self::getUInt2d($this->data, $this->pos + 2);
3154 81
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3155
3156
        // move stream pointer to next record
3157 81
        $this->pos += 4 + $length;
3158
3159
        // offset: 0; size: 2
3160
3161
        // bit: 6; mask: 0x0040; 0 = outline buttons above outline group
3162 81
        $isSummaryBelow = (0x0040 & self::getUInt2d($recordData, 0)) >> 6;
3163 81
        $this->phpSheet->setShowSummaryBelow((bool) $isSummaryBelow);
3164
3165
        // bit: 7; mask: 0x0080; 0 = outline buttons left of outline group
3166 81
        $isSummaryRight = (0x0080 & self::getUInt2d($recordData, 0)) >> 7;
3167 81
        $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 81
        $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 79
    private function readHeader(): void
3232
    {
3233 79
        $length = self::getUInt2d($this->data, $this->pos + 2);
3234 79
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3235
3236
        // move stream pointer to next record
3237 79
        $this->pos += 4 + $length;
3238
3239 79
        if (!$this->readDataOnly) {
3240
            // offset: 0; size: var
3241
            // realized that $recordData can be empty even when record exists
3242 78
            if ($recordData) {
3243 46
                if ($this->version == self::XLS_BIFF8) {
3244 45
                    $string = self::readUnicodeStringLong($recordData);
3245
                } else {
3246 1
                    $string = $this->readByteStringShort($recordData);
3247
                }
3248
3249 46
                $this->phpSheet->getHeaderFooter()->setOddHeader($string['value']);
3250 46
                $this->phpSheet->getHeaderFooter()->setEvenHeader($string['value']);
3251
            }
3252
        }
3253
    }
3254
3255
    /**
3256
     * Read FOOTER record.
3257
     */
3258 79
    private function readFooter(): void
3259
    {
3260 79
        $length = self::getUInt2d($this->data, $this->pos + 2);
3261 79
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3262
3263
        // move stream pointer to next record
3264 79
        $this->pos += 4 + $length;
3265
3266 79
        if (!$this->readDataOnly) {
3267
            // offset: 0; size: var
3268
            // realized that $recordData can be empty even when record exists
3269 78
            if ($recordData) {
3270 48
                if ($this->version == self::XLS_BIFF8) {
3271 47
                    $string = self::readUnicodeStringLong($recordData);
3272
                } else {
3273 1
                    $string = $this->readByteStringShort($recordData);
3274
                }
3275 48
                $this->phpSheet->getHeaderFooter()->setOddFooter($string['value']);
3276 48
                $this->phpSheet->getHeaderFooter()->setEvenFooter($string['value']);
3277
            }
3278
        }
3279
    }
3280
3281
    /**
3282
     * Read HCENTER record.
3283
     */
3284 79
    private function readHcenter(): void
3285
    {
3286 79
        $length = self::getUInt2d($this->data, $this->pos + 2);
3287 79
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3288
3289
        // move stream pointer to next record
3290 79
        $this->pos += 4 + $length;
3291
3292 79
        if (!$this->readDataOnly) {
3293
            // offset: 0; size: 2; 0 = print sheet left aligned, 1 = print sheet centered horizontally
3294 78
            $isHorizontalCentered = (bool) self::getUInt2d($recordData, 0);
3295
3296 78
            $this->phpSheet->getPageSetup()->setHorizontalCentered($isHorizontalCentered);
3297
        }
3298
    }
3299
3300
    /**
3301
     * Read VCENTER record.
3302
     */
3303 79
    private function readVcenter(): void
3304
    {
3305 79
        $length = self::getUInt2d($this->data, $this->pos + 2);
3306 79
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3307
3308
        // move stream pointer to next record
3309 79
        $this->pos += 4 + $length;
3310
3311 79
        if (!$this->readDataOnly) {
3312
            // offset: 0; size: 2; 0 = print sheet aligned at top page border, 1 = print sheet vertically centered
3313 78
            $isVerticalCentered = (bool) self::getUInt2d($recordData, 0);
3314
3315 78
            $this->phpSheet->getPageSetup()->setVerticalCentered($isVerticalCentered);
3316
        }
3317
    }
3318
3319
    /**
3320
     * Read LEFTMARGIN record.
3321
     */
3322 64
    private function readLeftMargin(): void
3323
    {
3324 64
        $length = self::getUInt2d($this->data, $this->pos + 2);
3325 64
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3326
3327
        // move stream pointer to next record
3328 64
        $this->pos += 4 + $length;
3329
3330 64
        if (!$this->readDataOnly) {
3331
            // offset: 0; size: 8
3332 64
            $this->phpSheet->getPageMargins()->setLeft(self::extractNumber($recordData));
3333
        }
3334
    }
3335
3336
    /**
3337
     * Read RIGHTMARGIN record.
3338
     */
3339 64
    private function readRightMargin(): void
3340
    {
3341 64
        $length = self::getUInt2d($this->data, $this->pos + 2);
3342 64
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3343
3344
        // move stream pointer to next record
3345 64
        $this->pos += 4 + $length;
3346
3347 64
        if (!$this->readDataOnly) {
3348
            // offset: 0; size: 8
3349 64
            $this->phpSheet->getPageMargins()->setRight(self::extractNumber($recordData));
3350
        }
3351
    }
3352
3353
    /**
3354
     * Read TOPMARGIN record.
3355
     */
3356 64
    private function readTopMargin(): void
3357
    {
3358 64
        $length = self::getUInt2d($this->data, $this->pos + 2);
3359 64
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3360
3361
        // move stream pointer to next record
3362 64
        $this->pos += 4 + $length;
3363
3364 64
        if (!$this->readDataOnly) {
3365
            // offset: 0; size: 8
3366 64
            $this->phpSheet->getPageMargins()->setTop(self::extractNumber($recordData));
3367
        }
3368
    }
3369
3370
    /**
3371
     * Read BOTTOMMARGIN record.
3372
     */
3373 64
    private function readBottomMargin(): void
3374
    {
3375 64
        $length = self::getUInt2d($this->data, $this->pos + 2);
3376 64
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3377
3378
        // move stream pointer to next record
3379 64
        $this->pos += 4 + $length;
3380
3381 64
        if (!$this->readDataOnly) {
3382
            // offset: 0; size: 8
3383 64
            $this->phpSheet->getPageMargins()->setBottom(self::extractNumber($recordData));
3384
        }
3385
    }
3386
3387
    /**
3388
     * Read PAGESETUP record.
3389
     */
3390 81
    private function readPageSetup(): void
3391
    {
3392 81
        $length = self::getUInt2d($this->data, $this->pos + 2);
3393 81
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3394
3395
        // move stream pointer to next record
3396 81
        $this->pos += 4 + $length;
3397
3398 81
        if (!$this->readDataOnly) {
3399
            // offset: 0; size: 2; paper size
3400 80
            $paperSize = self::getUInt2d($recordData, 0);
3401
3402
            // offset: 2; size: 2; scaling factor
3403 80
            $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 80
            $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 80
            $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 80
            $isOverThenDown = (0x0001 & self::getUInt2d($recordData, 10));
3415
3416
            // bit: 1; mask: 0x0002; 0=landscape, 1=portrait
3417 80
            $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 80
            $isNotInit = (0x0004 & self::getUInt2d($recordData, 10)) >> 2;
3422
3423 80
            if (!$isNotInit) {
3424 76
                $this->phpSheet->getPageSetup()->setPaperSize($paperSize);
3425 76
                $this->phpSheet->getPageSetup()->setPageOrder(((bool) $isOverThenDown) ? PageSetup::PAGEORDER_OVER_THEN_DOWN : PageSetup::PAGEORDER_DOWN_THEN_OVER);
3426 76
                $this->phpSheet->getPageSetup()->setOrientation(((bool) $isPortrait) ? PageSetup::ORIENTATION_PORTRAIT : PageSetup::ORIENTATION_LANDSCAPE);
3427
3428 76
                $this->phpSheet->getPageSetup()->setScale($scale, false);
3429 76
                $this->phpSheet->getPageSetup()->setFitToPage((bool) $this->isFitToPages);
3430 76
                $this->phpSheet->getPageSetup()->setFitToWidth($fitToWidth, false);
3431 76
                $this->phpSheet->getPageSetup()->setFitToHeight($fitToHeight, false);
3432
            }
3433
3434
            // offset: 16; size: 8; header margin (IEEE 754 floating-point value)
3435 80
            $marginHeader = self::extractNumber(substr($recordData, 16, 8));
3436 80
            $this->phpSheet->getPageMargins()->setHeader($marginHeader);
3437
3438
            // offset: 24; size: 8; footer margin (IEEE 754 floating-point value)
3439 80
            $marginFooter = self::extractNumber(substr($recordData, 24, 8));
3440 80
            $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 80
    private function readDefColWidth(): void
3535
    {
3536 80
        $length = self::getUInt2d($this->data, $this->pos + 2);
3537 80
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3538
3539
        // move stream pointer to next record
3540 80
        $this->pos += 4 + $length;
3541
3542
        // offset: 0; size: 2; default column width
3543 80
        $width = self::getUInt2d($recordData, 0);
3544 80
        if ($width != 8) {
3545 11
            $this->phpSheet->getDefaultColumnDimension()->setWidth($width);
3546
        }
3547
    }
3548
3549
    /**
3550
     * Read COLINFO record.
3551
     */
3552 74
    private function readColInfo(): void
3553
    {
3554 74
        $length = self::getUInt2d($this->data, $this->pos + 2);
3555 74
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3556
3557
        // move stream pointer to next record
3558 74
        $this->pos += 4 + $length;
3559
3560 74
        if (!$this->readDataOnly) {
3561
            // offset: 0; size: 2; index to first column in range
3562 73
            $firstColumnIndex = self::getUInt2d($recordData, 0);
3563
3564
            // offset: 2; size: 2; index to last column in range
3565 73
            $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 73
            $width = self::getUInt2d($recordData, 4);
3569
3570
            // offset: 6; size: 2; index to XF record for default column formatting
3571 73
            $xfIndex = self::getUInt2d($recordData, 6);
3572
3573
            // offset: 8; size: 2; option flags
3574
            // bit: 0; mask: 0x0001; 1= columns are hidden
3575 73
            $isHidden = (0x0001 & self::getUInt2d($recordData, 8)) >> 0;
3576
3577
            // bit: 10-8; mask: 0x0700; outline level of the columns (0 = no outline)
3578 73
            $level = (0x0700 & self::getUInt2d($recordData, 8)) >> 8;
3579
3580
            // bit: 12; mask: 0x1000; 1 = collapsed
3581 73
            $isCollapsed = (bool) ((0x1000 & self::getUInt2d($recordData, 8)) >> 12);
3582
3583
            // offset: 10; size: 2; not used
3584
3585 73
            for ($i = $firstColumnIndex + 1; $i <= $lastColumnIndex + 1; ++$i) {
3586 73
                if ($lastColumnIndex == 255 || $lastColumnIndex == 256) {
3587 13
                    $this->phpSheet->getDefaultColumnDimension()->setWidth($width / 256);
3588
3589 13
                    break;
3590
                }
3591 65
                $this->phpSheet->getColumnDimensionByColumn($i)->setWidth($width / 256);
3592 65
                $this->phpSheet->getColumnDimensionByColumn($i)->setVisible(!$isHidden);
3593 65
                $this->phpSheet->getColumnDimensionByColumn($i)->setOutlineLevel($level);
3594 65
                $this->phpSheet->getColumnDimensionByColumn($i)->setCollapsed($isCollapsed);
3595 65
                if (isset($this->mapCellXfIndex[$xfIndex])) {
3596 63
                    $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 54
    private function readRow(): void
3613
    {
3614 54
        $length = self::getUInt2d($this->data, $this->pos + 2);
3615 54
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3616
3617
        // move stream pointer to next record
3618 54
        $this->pos += 4 + $length;
3619
3620 54
        if (!$this->readDataOnly) {
3621
            // offset: 0; size: 2; index of this row
3622 53
            $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 53
            $height = (0x7FFF & self::getUInt2d($recordData, 6)) >> 0;
3632
3633
            // bit: 15: mask: 0x8000; 0 = row has custom height; 1= row has default height
3634 53
            $useDefaultHeight = (0x8000 & self::getUInt2d($recordData, 6)) >> 15;
3635
3636 53
            if (!$useDefaultHeight) {
3637 51
                $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 53
            $level = (0x00000007 & self::getInt4d($recordData, 12)) >> 0;
3648 53
            $this->phpSheet->getRowDimension($r + 1)->setOutlineLevel($level);
3649
3650
            // bit: 4; mask: 0x00000010; 1 = outline group start or ends here... and is collapsed
3651 53
            $isCollapsed = (bool) ((0x00000010 & self::getInt4d($recordData, 12)) >> 4);
3652 53
            $this->phpSheet->getRowDimension($r + 1)->setCollapsed($isCollapsed);
3653
3654
            // bit: 5; mask: 0x00000020; 1 = row is hidden
3655 53
            $isHidden = (0x00000020 & self::getInt4d($recordData, 12)) >> 5;
3656 53
            $this->phpSheet->getRowDimension($r + 1)->setVisible(!$isHidden);
3657
3658
            // bit: 7; mask: 0x00000080; 1 = row has explicit format
3659 53
            $hasExplicitFormat = (0x00000080 & self::getInt4d($recordData, 12)) >> 7;
3660
3661
            // bit: 27-16; mask: 0x0FFF0000; only applies when hasExplicitFormat = 1; index to XF record
3662 53
            $xfIndex = (0x0FFF0000 & self::getInt4d($recordData, 12)) >> 16;
3663
3664 53
            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 55
    private function readLabelSst(): void
3726
    {
3727 55
        $length = self::getUInt2d($this->data, $this->pos + 2);
3728 55
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3729
3730
        // move stream pointer to next record
3731 55
        $this->pos += 4 + $length;
3732
3733
        // offset: 0; size: 2; index to row
3734 55
        $row = self::getUInt2d($recordData, 0);
3735
3736
        // offset: 2; size: 2; index to column
3737 55
        $column = self::getUInt2d($recordData, 2);
3738 55
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
3739
3740 55
        $emptyCell = true;
3741
        // Read cell?
3742 55
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
3743
            // offset: 4; size: 2; index to XF record
3744 55
            $xfIndex = self::getUInt2d($recordData, 4);
3745
3746
            // offset: 6; size: 4; index to SST record
3747 55
            $index = self::getInt4d($recordData, 6);
3748
3749
            // add cell
3750 55
            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 55
                if ($this->readEmptyCells || trim($this->sst[$index]['value']) !== '') {
3791 55
                    $cell = $this->phpSheet->getCell($columnString . ($row + 1));
3792 55
                    $cell->setValueExplicit($this->sst[$index]['value'], DataType::TYPE_STRING);
3793 55
                    $emptyCell = false;
3794
                }
3795
            }
3796
3797 55
            if (!$this->readDataOnly && !$emptyCell && isset($this->mapCellXfIndex[$xfIndex])) {
3798
                // add style information
3799 54
                $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 19
    private function readMulRk(): void
3813
    {
3814 19
        $length = self::getUInt2d($this->data, $this->pos + 2);
3815 19
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3816
3817
        // move stream pointer to next record
3818 19
        $this->pos += 4 + $length;
3819
3820
        // offset: 0; size: 2; index to row
3821 19
        $row = self::getUInt2d($recordData, 0);
3822
3823
        // offset: 2; size: 2; index to first column
3824 19
        $colFirst = self::getUInt2d($recordData, 2);
3825
3826
        // offset: var; size: 2; index to last column
3827 19
        $colLast = self::getUInt2d($recordData, $length - 2);
3828 19
        $columns = $colLast - $colFirst + 1;
3829
3830
        // offset within record data
3831 19
        $offset = 4;
3832
3833 19
        for ($i = 1; $i <= $columns; ++$i) {
3834 19
            $columnString = Coordinate::stringFromColumnIndex($colFirst + $i);
3835
3836
            // Read cell?
3837 19
            if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
3838
                // offset: var; size: 2; index to XF record
3839 19
                $xfIndex = self::getUInt2d($recordData, $offset);
3840
3841
                // offset: var; size: 4; RK value
3842 19
                $numValue = self::getIEEE754(self::getInt4d($recordData, $offset + 2));
3843 19
                $cell = $this->phpSheet->getCell($columnString . ($row + 1));
3844 19
                if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
3845
                    // add style
3846 18
                    $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
3847
                }
3848
3849
                // add cell value
3850 19
                $cell->setValueExplicit($numValue, DataType::TYPE_NUMERIC);
3851
            }
3852
3853 19
            $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 42
    private function readNumber(): void
3866
    {
3867 42
        $length = self::getUInt2d($this->data, $this->pos + 2);
3868 42
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3869
3870
        // move stream pointer to next record
3871 42
        $this->pos += 4 + $length;
3872
3873
        // offset: 0; size: 2; index to row
3874 42
        $row = self::getUInt2d($recordData, 0);
3875
3876
        // offset: 2; size 2; index to column
3877 42
        $column = self::getUInt2d($recordData, 2);
3878 42
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
3879
3880
        // Read cell?
3881 42
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
3882
            // offset 4; size: 2; index to XF record
3883 42
            $xfIndex = self::getUInt2d($recordData, 4);
3884
3885 42
            $numValue = self::extractNumber(substr($recordData, 6, 8));
3886
3887 42
            $cell = $this->phpSheet->getCell($columnString . ($row + 1));
3888 42
            if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
3889
                // add cell style
3890 41
                $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
3891
            }
3892
3893
            // add cell value
3894 42
            $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 24
    private function readFormula(): void
3907
    {
3908 24
        $length = self::getUInt2d($this->data, $this->pos + 2);
3909 24
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3910
3911
        // move stream pointer to next record
3912 24
        $this->pos += 4 + $length;
3913
3914
        // offset: 0; size: 2; row index
3915 24
        $row = self::getUInt2d($recordData, 0);
3916
3917
        // offset: 2; size: 2; col index
3918 24
        $column = self::getUInt2d($recordData, 2);
3919 24
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
3920
3921
        // offset: 20: size: variable; formula structure
3922 24
        $formulaStructure = substr($recordData, 20);
3923
3924
        // offset: 14: size: 2; option flags, recalculate always, recalculate on open etc.
3925 24
        $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 24
        $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 24
        $isPartOfSharedFormula = $isPartOfSharedFormula && ord($formulaStructure[2]) == 0x01;
3937
3938 24
        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 24
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
3948 24
            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 24
            $xfIndex = self::getUInt2d($recordData, 4);
3957
3958
            // offset: 6; size: 8; result of the formula
3959 24
            if ((ord($recordData[6]) == 0) && (ord($recordData[12]) == 255) && (ord($recordData[13]) == 255)) {
3960
                // String formula. Result follows in appended STRING record
3961 4
                $dataType = DataType::TYPE_STRING;
3962
3963
                // read possible SHAREDFMLA record
3964 4
                $code = self::getUInt2d($this->data, $this->pos);
3965 4
                if ($code == self::XLS_TYPE_SHAREDFMLA) {
3966
                    $this->readSharedFmla();
3967
                }
3968
3969
                // read STRING record
3970 4
                $value = $this->readString();
3971
            } elseif (
3972 22
                (ord($recordData[6]) == 1)
3973 22
                && (ord($recordData[12]) == 255)
3974 22
                && (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 22
                (ord($recordData[6]) == 2)
3981 22
                && (ord($recordData[12]) == 255)
3982 22
                && (ord($recordData[13]) == 255)
3983
            ) {
3984
                // Error formula. Error code is in +2
3985 9
                $dataType = DataType::TYPE_ERROR;
3986 9
                $value = Xls\ErrorCode::lookup(ord($recordData[8]));
3987
            } elseif (
3988 22
                (ord($recordData[6]) == 3)
3989 22
                && (ord($recordData[12]) == 255)
3990 22
                && (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 22
                $dataType = DataType::TYPE_NUMERIC;
3998 22
                $value = self::extractNumber(substr($recordData, 6, 8));
3999
            }
4000
4001 24
            $cell = $this->phpSheet->getCell($columnString . ($row + 1));
4002 24
            if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
4003
                // add cell style
4004 23
                $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
4005
            }
4006
4007
            // store the formula
4008 24
            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 24
                    if ($this->version != self::XLS_BIFF8) {
4013 1
                        throw new Exception('Not BIFF8. Can only read BIFF8 formulas');
4014
                    }
4015 23
                    $formula = $this->getFormulaFromStructure($formulaStructure); // get formula in human language
4016 23
                    $cell->setValueExplicit('=' . $formula, DataType::TYPE_FORMULA);
4017 1
                } catch (PhpSpreadsheetException $e) {
4018 24
                    $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 24
            $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 4
    private function readString()
4070
    {
4071 4
        $length = self::getUInt2d($this->data, $this->pos + 2);
4072 4
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4073
4074
        // move stream pointer to next record
4075 4
        $this->pos += 4 + $length;
4076
4077 4
        if ($this->version == self::XLS_BIFF8) {
4078 4
            $string = self::readUnicodeStringLong($recordData);
4079 4
            $value = $string['value'];
4080
        } else {
4081
            $string = $this->readByteStringLong($recordData);
4082
            $value = $string['value'];
4083
        }
4084
4085 4
        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 24
    private function readMulBlank(): void
4156
    {
4157 24
        $length = self::getUInt2d($this->data, $this->pos + 2);
4158 24
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4159
4160
        // move stream pointer to next record
4161 24
        $this->pos += 4 + $length;
4162
4163
        // offset: 0; size: 2; index to row
4164 24
        $row = self::getUInt2d($recordData, 0);
4165
4166
        // offset: 2; size: 2; index to first column
4167 24
        $fc = self::getUInt2d($recordData, 2);
4168
4169
        // offset: 4; size: 2 x nc; list of indexes to XF records
4170
        // add style information
4171 24
        if (!$this->readDataOnly && $this->readEmptyCells) {
4172 23
            for ($i = 0; $i < $length / 2 - 3; ++$i) {
4173 23
                $columnString = Coordinate::stringFromColumnIndex($fc + $i + 1);
4174
4175
                // Read cell?
4176 23
                if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
4177 23
                    $xfIndex = self::getUInt2d($recordData, 4 + 2 * $i);
4178 23
                    if (isset($this->mapCellXfIndex[$xfIndex])) {
4179 23
                        $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 21
    private function readBlank(): void
4243
    {
4244 21
        $length = self::getUInt2d($this->data, $this->pos + 2);
4245 21
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4246
4247
        // move stream pointer to next record
4248 21
        $this->pos += 4 + $length;
4249
4250
        // offset: 0; size: 2; row index
4251 21
        $row = self::getUInt2d($recordData, 0);
4252
4253
        // offset: 2; size: 2; col index
4254 21
        $col = self::getUInt2d($recordData, 2);
4255 21
        $columnString = Coordinate::stringFromColumnIndex($col + 1);
4256
4257
        // Read cell?
4258 21
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
4259
            // offset: 4; size: 2; XF index
4260 21
            $xfIndex = self::getUInt2d($recordData, 4);
4261
4262
            // add style information
4263 21
            if (!$this->readDataOnly && $this->readEmptyCells && isset($this->mapCellXfIndex[$xfIndex])) {
4264 21
                $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 82
    private function readWindow2(): void
4327
    {
4328 82
        $length = self::getUInt2d($this->data, $this->pos + 2);
4329 82
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4330
4331
        // move stream pointer to next record
4332 82
        $this->pos += 4 + $length;
4333
4334
        // offset: 0; size: 2; option flags
4335 82
        $options = self::getUInt2d($recordData, 0);
4336
4337
        // offset: 2; size: 2; index to first visible row
4338 82
        $firstVisibleRow = self::getUInt2d($recordData, 2);
4339
4340
        // offset: 4; size: 2; index to first visible colum
4341 82
        $firstVisibleColumn = self::getUInt2d($recordData, 4);
4342 82
        $zoomscaleInPageBreakPreview = 0;
4343 82
        $zoomscaleInNormalView = 0;
4344 82
        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 81
            if (!isset($recordData[10])) {
4350
                $zoomscaleInPageBreakPreview = 0;
4351
            } else {
4352 81
                $zoomscaleInPageBreakPreview = self::getUInt2d($recordData, 10);
4353
            }
4354
4355 81
            if ($zoomscaleInPageBreakPreview === 0) {
4356 78
                $zoomscaleInPageBreakPreview = 60;
4357
            }
4358
4359 81
            if (!isset($recordData[12])) {
4360
                $zoomscaleInNormalView = 0;
4361
            } else {
4362 81
                $zoomscaleInNormalView = self::getUInt2d($recordData, 12);
4363
            }
4364
4365 81
            if ($zoomscaleInNormalView === 0) {
4366 36
                $zoomscaleInNormalView = 100;
4367
            }
4368
        }
4369
4370
        // bit: 1; mask: 0x0002; 0 = do not show gridlines, 1 = show gridlines
4371 82
        $showGridlines = (bool) ((0x0002 & $options) >> 1);
4372 82
        $this->phpSheet->setShowGridlines($showGridlines);
4373
4374
        // bit: 2; mask: 0x0004; 0 = do not show headers, 1 = show headers
4375 82
        $showRowColHeaders = (bool) ((0x0004 & $options) >> 2);
4376 82
        $this->phpSheet->setShowRowColHeaders($showRowColHeaders);
4377
4378
        // bit: 3; mask: 0x0008; 0 = panes are not frozen, 1 = panes are frozen
4379 82
        $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 82
        $this->phpSheet->setRightToLeft((bool) ((0x0040 & $options) >> 6));
4383
4384
        // bit: 10; mask: 0x0400; 0 = sheet not active, 1 = sheet active
4385 82
        $isActive = (bool) ((0x0400 & $options) >> 10);
4386 82
        if ($isActive) {
4387 79
            $this->spreadsheet->setActiveSheetIndex($this->spreadsheet->getIndex($this->phpSheet));
4388
        }
4389
4390
        // bit: 11; mask: 0x0800; 0 = normal view, 1 = page break view
4391 82
        $isPageBreakPreview = (bool) ((0x0800 & $options) >> 11);
4392
4393
        //FIXME: set $firstVisibleRow and $firstVisibleColumn
4394
4395 82
        if ($this->phpSheet->getSheetView()->getView() !== SheetView::SHEETVIEW_PAGE_LAYOUT) {
4396
            //NOTE: this setting is inferior to page layout view(Excel2007-)
4397 82
            $view = $isPageBreakPreview ? SheetView::SHEETVIEW_PAGE_BREAK_PREVIEW : SheetView::SHEETVIEW_NORMAL;
4398 82
            $this->phpSheet->getSheetView()->setView($view);
4399 82
            if ($this->version === self::XLS_BIFF8) {
4400 81
                $zoomScale = $isPageBreakPreview ? $zoomscaleInPageBreakPreview : $zoomscaleInNormalView;
4401 81
                $this->phpSheet->getSheetView()->setZoomScale($zoomScale);
4402 81
                $this->phpSheet->getSheetView()->setZoomScaleNormal($zoomscaleInNormalView);
4403
            }
4404
        }
4405
    }
4406
4407
    /**
4408
     * Read PLV Record(Created by Excel2007 or upper).
4409
     */
4410 55
    private function readPageLayoutView(): void
4411
    {
4412 55
        $length = self::getUInt2d($this->data, $this->pos + 2);
4413 55
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4414
4415
        // move stream pointer to next record
4416 55
        $this->pos += 4 + $length;
4417
4418
        // offset: 0; size: 2; rt
4419
        //->ignore
4420 55
        $rt = self::getUInt2d($recordData, 0);
4421
        // offset: 2; size: 2; grbitfr
4422
        //->ignore
4423 55
        $grbitFrt = self::getUInt2d($recordData, 2);
4424
        // offset: 4; size: 8; reserved
4425
        //->ignore
4426
4427
        // offset: 12; size 2; zoom scale
4428 55
        $wScalePLV = self::getUInt2d($recordData, 12);
4429
        // offset: 14; size 2; grbit
4430 55
        $grbit = self::getUInt2d($recordData, 14);
4431
4432
        // decomprise grbit
4433 55
        $fPageLayoutView = $grbit & 0x01;
4434 55
        $fRulerVisible = ($grbit >> 1) & 0x01; //no support
4435 55
        $fWhitespaceHidden = ($grbit >> 3) & 0x01; //no support
4436
4437 55
        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 79
    private function readSelection(): void
4503
    {
4504 79
        $length = self::getUInt2d($this->data, $this->pos + 2);
4505 79
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4506
4507
        // move stream pointer to next record
4508 79
        $this->pos += 4 + $length;
4509
4510 79
        if (!$this->readDataOnly) {
4511
            // offset: 0; size: 1; pane identifier
4512 78
            $paneId = ord($recordData[0]);
4513
4514
            // offset: 1; size: 2; index to row of the active cell
4515 78
            $r = self::getUInt2d($recordData, 1);
4516
4517
            // offset: 3; size: 2; index to column of the active cell
4518 78
            $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 78
            $index = self::getUInt2d($recordData, 5);
4523
4524
            // offset: 7; size: var; cell range address list containing all selected cell ranges
4525 78
            $data = substr($recordData, 7);
4526 78
            $cellRangeAddressList = $this->readBIFF5CellRangeAddressList($data); // note: also BIFF8 uses BIFF5 syntax
4527
4528 78
            $selectedCells = $cellRangeAddressList['cellRangeAddresses'][0];
4529
4530
            // first row '1' + last row '16384' indicates that full column is selected (apparently also in BIFF8!)
4531 78
            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 78
            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 78
            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 78
            $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 65
    private function readSheetProtection(): void
4964
    {
4965 65
        $length = self::getUInt2d($this->data, $this->pos + 2);
4966 65
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4967
4968
        // move stream pointer to next record
4969 65
        $this->pos += 4 + $length;
4970
4971 65
        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 65
        $isf = self::getUInt2d($recordData, 12);
4983 65
        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 65
        $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 65
        $bool = (0x0001 & $options) >> 0;
4998 65
        $this->phpSheet->getProtection()->setObjects((bool) $bool);
4999
5000
        // bit: 1; mask 0x0002; edit scenarios
5001
        // Note - do not negate $bool
5002 65
        $bool = (0x0002 & $options) >> 1;
5003 65
        $this->phpSheet->getProtection()->setScenarios((bool) $bool);
5004
5005
        // bit: 2; mask 0x0004; format cells
5006 65
        $bool = (0x0004 & $options) >> 2;
5007 65
        $this->phpSheet->getProtection()->setFormatCells(!$bool);
5008
5009
        // bit: 3; mask 0x0008; format columns
5010 65
        $bool = (0x0008 & $options) >> 3;
5011 65
        $this->phpSheet->getProtection()->setFormatColumns(!$bool);
5012
5013
        // bit: 4; mask 0x0010; format rows
5014 65
        $bool = (0x0010 & $options) >> 4;
5015 65
        $this->phpSheet->getProtection()->setFormatRows(!$bool);
5016
5017
        // bit: 5; mask 0x0020; insert columns
5018 65
        $bool = (0x0020 & $options) >> 5;
5019 65
        $this->phpSheet->getProtection()->setInsertColumns(!$bool);
5020
5021
        // bit: 6; mask 0x0040; insert rows
5022 65
        $bool = (0x0040 & $options) >> 6;
5023 65
        $this->phpSheet->getProtection()->setInsertRows(!$bool);
5024
5025
        // bit: 7; mask 0x0080; insert hyperlinks
5026 65
        $bool = (0x0080 & $options) >> 7;
5027 65
        $this->phpSheet->getProtection()->setInsertHyperlinks(!$bool);
5028
5029
        // bit: 8; mask 0x0100; delete columns
5030 65
        $bool = (0x0100 & $options) >> 8;
5031 65
        $this->phpSheet->getProtection()->setDeleteColumns(!$bool);
5032
5033
        // bit: 9; mask 0x0200; delete rows
5034 65
        $bool = (0x0200 & $options) >> 9;
5035 65
        $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 65
        $bool = (0x0400 & $options) >> 10;
5040 65
        $this->phpSheet->getProtection()->setSelectLockedCells((bool) $bool);
5041
5042
        // bit: 11; mask 0x0800; sort cell range
5043 65
        $bool = (0x0800 & $options) >> 11;
5044 65
        $this->phpSheet->getProtection()->setSort(!$bool);
5045
5046
        // bit: 12; mask 0x1000; auto filter
5047 65
        $bool = (0x1000 & $options) >> 12;
5048 65
        $this->phpSheet->getProtection()->setAutoFilter(!$bool);
5049
5050
        // bit: 13; mask 0x2000; pivot tables
5051 65
        $bool = (0x2000 & $options) >> 13;
5052 65
        $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 65
        $bool = (0x4000 & $options) >> 14;
5057 65
        $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 80
    private function getSplicedRecordData()
5181
    {
5182 80
        $data = '';
5183 80
        $spliceOffsets = [];
5184
5185 80
        $i = 0;
5186 80
        $spliceOffsets[0] = 0;
5187
5188
        do {
5189 80
            ++$i;
5190
5191
            // offset: 0; size: 2; identifier
5192 80
            $identifier = self::getUInt2d($this->data, $this->pos);
5193
            // offset: 2; size: 2; length
5194 80
            $length = self::getUInt2d($this->data, $this->pos + 2);
5195 80
            $data .= $this->readRecordData($this->data, $this->pos + 4, $length);
5196
5197 80
            $spliceOffsets[$i] = $spliceOffsets[$i - 1] + $length;
5198
5199 80
            $this->pos += 4 + $length;
5200 80
            $nextIdentifier = self::getUInt2d($this->data, $this->pos);
5201 80
        } while ($nextIdentifier == self::XLS_TYPE_CONTINUE);
5202
5203 80
        return [
5204 80
            'recordData' => $data,
5205 80
            'spliceOffsets' => $spliceOffsets,
5206 80
        ];
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 40
    private function getFormulaFromStructure($formulaStructure, $baseCell = 'A1')
5218
    {
5219
        // offset: 0; size: 2; size of the following formula data
5220 40
        $sz = self::getUInt2d($formulaStructure, 0);
5221
5222
        // offset: 2; size: sz
5223 40
        $formulaData = substr($formulaStructure, 2, $sz);
5224
5225
        // offset: 2 + sz; size: variable (optional)
5226 40
        if (strlen($formulaStructure) > 2 + $sz) {
5227
            $additionalData = substr($formulaStructure, 2 + $sz);
5228
        } else {
5229 40
            $additionalData = '';
5230
        }
5231
5232 40
        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 40
    private function getFormulaFromData($formulaData, $additionalData = '', $baseCell = 'A1')
5245
    {
5246
        // start parsing the formula data
5247 40
        $tokens = [];
5248
5249 40
        while (strlen($formulaData) > 0 && $token = $this->getNextToken($formulaData, $baseCell)) {
5250 40
            $tokens[] = $token;
5251 40
            $formulaData = substr($formulaData, $token['size']);
5252
        }
5253
5254 40
        $formulaString = $this->createFormulaFromTokens($tokens, $additionalData);
5255
5256 40
        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 40
    private function createFormulaFromTokens($tokens, $additionalData)
5268
    {
5269
        // empty formula?
5270 40
        if (empty($tokens)) {
5271 2
            return '';
5272
        }
5273
5274 40
        $formulaStrings = [];
5275 40
        foreach ($tokens as $token) {
5276
            // initialize spaces
5277 40
            $space0 = $space0 ?? ''; // spaces before next token, not tParen
5278 40
            $space1 = $space1 ?? ''; // carriage returns before next token, not tParen
5279 40
            $space2 = $space2 ?? ''; // spaces before opening parenthesis
5280 40
            $space3 = $space3 ?? ''; // carriage returns before opening parenthesis
5281 40
            $space4 = $space4 ?? ''; // spaces before closing parenthesis
5282 40
            $space5 = $space5 ?? ''; // carriage returns before closing parenthesis
5283
5284 40
            switch ($token['name']) {
5285 40
                case 'tAdd': // addition
5286 40
                case 'tConcat': // addition
5287 40
                case 'tDiv': // division
5288 40
                case 'tEQ': // equality
5289 40
                case 'tGE': // greater than or equal
5290 40
                case 'tGT': // greater than
5291 40
                case 'tIsect': // intersection
5292 40
                case 'tLE': // less than or equal
5293 40
                case 'tList': // less than or equal
5294 40
                case 'tLT': // less than
5295 40
                case 'tMul': // multiplication
5296 40
                case 'tNE': // multiplication
5297 40
                case 'tPower': // power
5298 40
                case 'tRange': // range
5299 40
                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 40
                case 'tUplus': // unary plus
5307 40
                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 40
                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 40
                case 'tAttrVolatile': // indicates volatile function
5320 40
                case 'tAttrIf':
5321 40
                case 'tAttrSkip':
5322 40
                case 'tAttrChoose':
5323
                    // token is only important for Excel formula evaluator
5324
                    // do nothing
5325 2
                    break;
5326 40
                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 40
                case 'tAttrSum': // SUM function with one parameter
5357 11
                    $op = array_pop($formulaStrings);
5358 11
                    $formulaStrings[] = "{$space1}{$space0}SUM($op)";
5359 11
                    unset($space0, $space1);
5360
5361 11
                    break;
5362 40
                case 'tFunc': // function with fixed number of arguments
5363 40
                case 'tFuncV': // function with variable number of arguments
5364 25
                    if ($token['data']['function'] != '') {
5365
                        // normal function
5366 25
                        $ops = []; // array of operators
5367 25
                        for ($i = 0; $i < $token['data']['args']; ++$i) {
5368 17
                            $ops[] = array_pop($formulaStrings);
5369
                        }
5370 25
                        $ops = array_reverse($ops);
5371 25
                        $formulaStrings[] = "$space1$space0{$token['data']['function']}(" . implode(',', $ops) . ')';
5372 25
                        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 25
                    break;
5386 40
                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 40
                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 40
                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 40
                case 'tArea': // cell range address
5408 38
                case 'tBool': // boolean
5409 38
                case 'tErr': // error code
5410 37
                case 'tInt': // integer
5411 28
                case 'tMemErr':
5412 28
                case 'tMemFunc':
5413 28
                case 'tMissArg':
5414 28
                case 'tName':
5415 28
                case 'tNameX':
5416 28
                case 'tNum': // number
5417 28
                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 40
                    $formulaStrings[] = "$space1$space0{$token['data']}";
5424 40
                    unset($space0, $space1);
5425
5426 40
                    break;
5427
            }
5428
        }
5429 40
        $formulaString = $formulaStrings[0];
5430
5431 40
        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 40
    private function getNextToken($formulaData, $baseCell = 'A1')
5443
    {
5444
        // offset: 0; size: 1; token id
5445 40
        $id = ord($formulaData[0]); // token id
5446 40
        $name = false; // initialize token name
5447
5448
        switch ($id) {
5449 40
            case 0x03:
5450 7
                $name = 'tAdd';
5451 7
                $size = 1;
5452 7
                $data = '+';
5453
5454 7
                break;
5455 40
            case 0x04:
5456 8
                $name = 'tSub';
5457 8
                $size = 1;
5458 8
                $data = '-';
5459
5460 8
                break;
5461 40
            case 0x05:
5462 4
                $name = 'tMul';
5463 4
                $size = 1;
5464 4
                $data = '*';
5465
5466 4
                break;
5467 40
            case 0x06:
5468 12
                $name = 'tDiv';
5469 12
                $size = 1;
5470 12
                $data = '/';
5471
5472 12
                break;
5473 40
            case 0x07:
5474 1
                $name = 'tPower';
5475 1
                $size = 1;
5476 1
                $data = '^';
5477
5478 1
                break;
5479 40
            case 0x08:
5480 3
                $name = 'tConcat';
5481 3
                $size = 1;
5482 3
                $data = '&';
5483
5484 3
                break;
5485 40
            case 0x09:
5486 1
                $name = 'tLT';
5487 1
                $size = 1;
5488 1
                $data = '<';
5489
5490 1
                break;
5491 40
            case 0x0A:
5492 1
                $name = 'tLE';
5493 1
                $size = 1;
5494 1
                $data = '<=';
5495
5496 1
                break;
5497 40
            case 0x0B:
5498 3
                $name = 'tEQ';
5499 3
                $size = 1;
5500 3
                $data = '=';
5501
5502 3
                break;
5503 40
            case 0x0C:
5504 1
                $name = 'tGE';
5505 1
                $size = 1;
5506 1
                $data = '>=';
5507
5508 1
                break;
5509 40
            case 0x0D:
5510 1
                $name = 'tGT';
5511 1
                $size = 1;
5512 1
                $data = '>';
5513
5514 1
                break;
5515 40
            case 0x0E:
5516 2
                $name = 'tNE';
5517 2
                $size = 1;
5518 2
                $data = '<>';
5519
5520 2
                break;
5521 40
            case 0x0F:
5522
                $name = 'tIsect';
5523
                $size = 1;
5524
                $data = ' ';
5525
5526
                break;
5527 40
            case 0x10:
5528 1
                $name = 'tList';
5529 1
                $size = 1;
5530 1
                $data = ',';
5531
5532 1
                break;
5533 40
            case 0x11:
5534
                $name = 'tRange';
5535
                $size = 1;
5536
                $data = ':';
5537
5538
                break;
5539 40
            case 0x12:
5540 1
                $name = 'tUplus';
5541 1
                $size = 1;
5542 1
                $data = '+';
5543
5544 1
                break;
5545 40
            case 0x13:
5546 1
                $name = 'tUminus';
5547 1
                $size = 1;
5548 1
                $data = '-';
5549
5550 1
                break;
5551 40
            case 0x14:
5552 1
                $name = 'tPercent';
5553 1
                $size = 1;
5554 1
                $data = '%';
5555
5556 1
                break;
5557 40
            case 0x15:    //    parenthesis
5558 1
                $name = 'tParen';
5559 1
                $size = 1;
5560 1
                $data = null;
5561
5562 1
                break;
5563 40
            case 0x16:    //    missing argument
5564
                $name = 'tMissArg';
5565
                $size = 1;
5566
                $data = '';
5567
5568
                break;
5569 40
            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 40
            case 0x19:    //    Special attribute
5578
                // offset: 1; size: 1; attribute type flags:
5579 12
                switch (ord($formulaData[1])) {
5580 12
                    case 0x01:
5581 2
                        $name = 'tAttrVolatile';
5582 2
                        $size = 4;
5583 2
                        $data = null;
5584
5585 2
                        break;
5586 11
                    case 0x02:
5587 1
                        $name = 'tAttrIf';
5588 1
                        $size = 4;
5589 1
                        $data = null;
5590
5591 1
                        break;
5592 11
                    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 11
                    case 0x08:
5603 1
                        $name = 'tAttrSkip';
5604 1
                        $size = 4;
5605 1
                        $data = null;
5606
5607 1
                        break;
5608 11
                    case 0x10:
5609 11
                        $name = 'tAttrSum';
5610 11
                        $size = 4;
5611 11
                        $data = null;
5612
5613 11
                        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 12
                break;
5658 40
            case 0x1C:    //    error code
5659
                // offset: 1; size: 1; error code
5660 2
                $name = 'tErr';
5661 2
                $size = 2;
5662 2
                $data = Xls\ErrorCode::lookup(ord($formulaData[1]));
5663
5664 2
                break;
5665 39
            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 39
            case 0x1E:    //    integer
5673
                // offset: 1; size: 2; unsigned 16-bit integer
5674 21
                $name = 'tInt';
5675 21
                $size = 3;
5676 21
                $data = self::getUInt2d($formulaData, 1);
5677
5678 21
                break;
5679 37
            case 0x1F:    //    number
5680
                // offset: 1; size: 8;
5681 5
                $name = 'tNum';
5682 5
                $size = 9;
5683 5
                $data = self::extractNumber(substr($formulaData, 1));
5684 5
                $data = str_replace(',', '.', (string) $data); // in case non-English locale
5685
5686 5
                break;
5687 37
            case 0x20:    //    array constant
5688 37
            case 0x40:
5689 37
            case 0x60:
5690
                // offset: 1; size: 7; not used
5691
                $name = 'tArray';
5692
                $size = 8;
5693
                $data = null;
5694
5695
                break;
5696 37
            case 0x21:    //    function with fixed number of arguments
5697 37
            case 0x41:
5698 36
            case 0x61:
5699 13
                $name = 'tFunc';
5700 13
                $size = 3;
5701
                // offset: 1; size: 2; index to built-in sheet function
5702 13
                switch (self::getUInt2d($formulaData, 1)) {
5703 13
                    case 2:
5704 1
                        $function = 'ISNA';
5705 1
                        $args = 1;
5706
5707 1
                        break;
5708 13
                    case 3:
5709 1
                        $function = 'ISERROR';
5710 1
                        $args = 1;
5711
5712 1
                        break;
5713 13
                    case 10:
5714 9
                        $function = 'NA';
5715 9
                        $args = 0;
5716
5717 9
                        break;
5718 5
                    case 15:
5719 2
                        $function = 'SIN';
5720 2
                        $args = 1;
5721
5722 2
                        break;
5723 4
                    case 16:
5724 1
                        $function = 'COS';
5725 1
                        $args = 1;
5726
5727 1
                        break;
5728 4
                    case 17:
5729 1
                        $function = 'TAN';
5730 1
                        $args = 1;
5731
5732 1
                        break;
5733 4
                    case 18:
5734 1
                        $function = 'ATAN';
5735 1
                        $args = 1;
5736
5737 1
                        break;
5738 4
                    case 19:
5739 1
                        $function = 'PI';
5740 1
                        $args = 0;
5741
5742 1
                        break;
5743 4
                    case 20:
5744 1
                        $function = 'SQRT';
5745 1
                        $args = 1;
5746
5747 1
                        break;
5748 4
                    case 21:
5749 1
                        $function = 'EXP';
5750 1
                        $args = 1;
5751
5752 1
                        break;
5753 4
                    case 22:
5754 1
                        $function = 'LN';
5755 1
                        $args = 1;
5756
5757 1
                        break;
5758 4
                    case 23:
5759 1
                        $function = 'LOG10';
5760 1
                        $args = 1;
5761
5762 1
                        break;
5763 4
                    case 24:
5764 1
                        $function = 'ABS';
5765 1
                        $args = 1;
5766
5767 1
                        break;
5768 4
                    case 25:
5769 1
                        $function = 'INT';
5770 1
                        $args = 1;
5771
5772 1
                        break;
5773 4
                    case 26:
5774 1
                        $function = 'SIGN';
5775 1
                        $args = 1;
5776
5777 1
                        break;
5778 4
                    case 27:
5779 1
                        $function = 'ROUND';
5780 1
                        $args = 2;
5781
5782 1
                        break;
5783 4
                    case 30:
5784 2
                        $function = 'REPT';
5785 2
                        $args = 2;
5786
5787 2
                        break;
5788 4
                    case 31:
5789 1
                        $function = 'MID';
5790 1
                        $args = 3;
5791
5792 1
                        break;
5793 4
                    case 32:
5794 1
                        $function = 'LEN';
5795 1
                        $args = 1;
5796
5797 1
                        break;
5798 4
                    case 33:
5799 1
                        $function = 'VALUE';
5800 1
                        $args = 1;
5801
5802 1
                        break;
5803 4
                    case 34:
5804 1
                        $function = 'TRUE';
5805 1
                        $args = 0;
5806
5807 1
                        break;
5808 4
                    case 35:
5809 1
                        $function = 'FALSE';
5810 1
                        $args = 0;
5811
5812 1
                        break;
5813 4
                    case 38:
5814 1
                        $function = 'NOT';
5815 1
                        $args = 1;
5816
5817 1
                        break;
5818 4
                    case 39:
5819 1
                        $function = 'MOD';
5820 1
                        $args = 2;
5821
5822 1
                        break;
5823 4
                    case 40:
5824 1
                        $function = 'DCOUNT';
5825 1
                        $args = 3;
5826
5827 1
                        break;
5828 4
                    case 41:
5829 1
                        $function = 'DSUM';
5830 1
                        $args = 3;
5831
5832 1
                        break;
5833 4
                    case 42:
5834 1
                        $function = 'DAVERAGE';
5835 1
                        $args = 3;
5836
5837 1
                        break;
5838 4
                    case 43:
5839 1
                        $function = 'DMIN';
5840 1
                        $args = 3;
5841
5842 1
                        break;
5843 4
                    case 44:
5844 1
                        $function = 'DMAX';
5845 1
                        $args = 3;
5846
5847 1
                        break;
5848 4
                    case 45:
5849 1
                        $function = 'DSTDEV';
5850 1
                        $args = 3;
5851
5852 1
                        break;
5853 4
                    case 48:
5854 1
                        $function = 'TEXT';
5855 1
                        $args = 2;
5856
5857 1
                        break;
5858 4
                    case 61:
5859 1
                        $function = 'MIRR';
5860 1
                        $args = 3;
5861
5862 1
                        break;
5863 4
                    case 63:
5864 1
                        $function = 'RAND';
5865 1
                        $args = 0;
5866
5867 1
                        break;
5868 4
                    case 65:
5869 1
                        $function = 'DATE';
5870 1
                        $args = 3;
5871
5872 1
                        break;
5873 4
                    case 66:
5874 1
                        $function = 'TIME';
5875 1
                        $args = 3;
5876
5877 1
                        break;
5878 4
                    case 67:
5879 1
                        $function = 'DAY';
5880 1
                        $args = 1;
5881
5882 1
                        break;
5883 4
                    case 68:
5884 1
                        $function = 'MONTH';
5885 1
                        $args = 1;
5886
5887 1
                        break;
5888 4
                    case 69:
5889 1
                        $function = 'YEAR';
5890 1
                        $args = 1;
5891
5892 1
                        break;
5893 4
                    case 71:
5894 1
                        $function = 'HOUR';
5895 1
                        $args = 1;
5896
5897 1
                        break;
5898 4
                    case 72:
5899 1
                        $function = 'MINUTE';
5900 1
                        $args = 1;
5901
5902 1
                        break;
5903 4
                    case 73:
5904 1
                        $function = 'SECOND';
5905 1
                        $args = 1;
5906
5907 1
                        break;
5908 4
                    case 74:
5909 1
                        $function = 'NOW';
5910 1
                        $args = 0;
5911
5912 1
                        break;
5913 4
                    case 75:
5914 1
                        $function = 'AREAS';
5915 1
                        $args = 1;
5916
5917 1
                        break;
5918 4
                    case 76:
5919 1
                        $function = 'ROWS';
5920 1
                        $args = 1;
5921
5922 1
                        break;
5923 4
                    case 77:
5924 1
                        $function = 'COLUMNS';
5925 1
                        $args = 1;
5926
5927 1
                        break;
5928 4
                    case 83:
5929
                        $function = 'TRANSPOSE';
5930
                        $args = 1;
5931
5932
                        break;
5933 4
                    case 86:
5934 1
                        $function = 'TYPE';
5935 1
                        $args = 1;
5936
5937 1
                        break;
5938 4
                    case 97:
5939 1
                        $function = 'ATAN2';
5940 1
                        $args = 2;
5941
5942 1
                        break;
5943 4
                    case 98:
5944 1
                        $function = 'ASIN';
5945 1
                        $args = 1;
5946
5947 1
                        break;
5948 4
                    case 99:
5949 1
                        $function = 'ACOS';
5950 1
                        $args = 1;
5951
5952 1
                        break;
5953 4
                    case 105:
5954 1
                        $function = 'ISREF';
5955 1
                        $args = 1;
5956
5957 1
                        break;
5958 4
                    case 111:
5959 2
                        $function = 'CHAR';
5960 2
                        $args = 1;
5961
5962 2
                        break;
5963 3
                    case 112:
5964 1
                        $function = 'LOWER';
5965 1
                        $args = 1;
5966
5967 1
                        break;
5968 3
                    case 113:
5969 1
                        $function = 'UPPER';
5970 1
                        $args = 1;
5971
5972 1
                        break;
5973 3
                    case 114:
5974 1
                        $function = 'PROPER';
5975 1
                        $args = 1;
5976
5977 1
                        break;
5978 3
                    case 117:
5979 1
                        $function = 'EXACT';
5980 1
                        $args = 2;
5981
5982 1
                        break;
5983 3
                    case 118:
5984 1
                        $function = 'TRIM';
5985 1
                        $args = 1;
5986
5987 1
                        break;
5988 3
                    case 119:
5989 1
                        $function = 'REPLACE';
5990 1
                        $args = 4;
5991
5992 1
                        break;
5993 3
                    case 121:
5994 1
                        $function = 'CODE';
5995 1
                        $args = 1;
5996
5997 1
                        break;
5998 3
                    case 126:
5999 1
                        $function = 'ISERR';
6000 1
                        $args = 1;
6001
6002 1
                        break;
6003 3
                    case 127:
6004 1
                        $function = 'ISTEXT';
6005 1
                        $args = 1;
6006
6007 1
                        break;
6008 3
                    case 128:
6009 1
                        $function = 'ISNUMBER';
6010 1
                        $args = 1;
6011
6012 1
                        break;
6013 3
                    case 129:
6014 1
                        $function = 'ISBLANK';
6015 1
                        $args = 1;
6016
6017 1
                        break;
6018 3
                    case 130:
6019 1
                        $function = 'T';
6020 1
                        $args = 1;
6021
6022 1
                        break;
6023 3
                    case 131:
6024 1
                        $function = 'N';
6025 1
                        $args = 1;
6026
6027 1
                        break;
6028 3
                    case 140:
6029 1
                        $function = 'DATEVALUE';
6030 1
                        $args = 1;
6031
6032 1
                        break;
6033 3
                    case 141:
6034 1
                        $function = 'TIMEVALUE';
6035 1
                        $args = 1;
6036
6037 1
                        break;
6038 3
                    case 142:
6039 1
                        $function = 'SLN';
6040 1
                        $args = 3;
6041
6042 1
                        break;
6043 3
                    case 143:
6044 1
                        $function = 'SYD';
6045 1
                        $args = 4;
6046
6047 1
                        break;
6048 3
                    case 162:
6049 1
                        $function = 'CLEAN';
6050 1
                        $args = 1;
6051
6052 1
                        break;
6053 3
                    case 163:
6054 1
                        $function = 'MDETERM';
6055 1
                        $args = 1;
6056
6057 1
                        break;
6058 3
                    case 164:
6059
                        $function = 'MINVERSE';
6060
                        $args = 1;
6061
6062
                        break;
6063 3
                    case 165:
6064
                        $function = 'MMULT';
6065
                        $args = 2;
6066
6067
                        break;
6068 3
                    case 184:
6069 1
                        $function = 'FACT';
6070 1
                        $args = 1;
6071
6072 1
                        break;
6073 3
                    case 189:
6074 1
                        $function = 'DPRODUCT';
6075 1
                        $args = 3;
6076
6077 1
                        break;
6078 3
                    case 190:
6079 1
                        $function = 'ISNONTEXT';
6080 1
                        $args = 1;
6081
6082 1
                        break;
6083 3
                    case 195:
6084 1
                        $function = 'DSTDEVP';
6085 1
                        $args = 3;
6086
6087 1
                        break;
6088 3
                    case 196:
6089 1
                        $function = 'DVARP';
6090 1
                        $args = 3;
6091
6092 1
                        break;
6093 3
                    case 198:
6094 1
                        $function = 'ISLOGICAL';
6095 1
                        $args = 1;
6096
6097 1
                        break;
6098 3
                    case 199:
6099 1
                        $function = 'DCOUNTA';
6100 1
                        $args = 3;
6101
6102 1
                        break;
6103 3
                    case 207:
6104 1
                        $function = 'REPLACEB';
6105 1
                        $args = 4;
6106
6107 1
                        break;
6108 3
                    case 210:
6109 1
                        $function = 'MIDB';
6110 1
                        $args = 3;
6111
6112 1
                        break;
6113 3
                    case 211:
6114 1
                        $function = 'LENB';
6115 1
                        $args = 1;
6116
6117 1
                        break;
6118 3
                    case 212:
6119 1
                        $function = 'ROUNDUP';
6120 1
                        $args = 2;
6121
6122 1
                        break;
6123 3
                    case 213:
6124 1
                        $function = 'ROUNDDOWN';
6125 1
                        $args = 2;
6126
6127 1
                        break;
6128 3
                    case 214:
6129 1
                        $function = 'ASC';
6130 1
                        $args = 1;
6131
6132 1
                        break;
6133 3
                    case 215:
6134 1
                        $function = 'DBCS';
6135 1
                        $args = 1;
6136
6137 1
                        break;
6138 3
                    case 221:
6139 1
                        $function = 'TODAY';
6140 1
                        $args = 0;
6141
6142 1
                        break;
6143 3
                    case 229:
6144 1
                        $function = 'SINH';
6145 1
                        $args = 1;
6146
6147 1
                        break;
6148 3
                    case 230:
6149 1
                        $function = 'COSH';
6150 1
                        $args = 1;
6151
6152 1
                        break;
6153 3
                    case 231:
6154 1
                        $function = 'TANH';
6155 1
                        $args = 1;
6156
6157 1
                        break;
6158 3
                    case 232:
6159 1
                        $function = 'ASINH';
6160 1
                        $args = 1;
6161
6162 1
                        break;
6163 3
                    case 233:
6164 1
                        $function = 'ACOSH';
6165 1
                        $args = 1;
6166
6167 1
                        break;
6168 3
                    case 234:
6169 1
                        $function = 'ATANH';
6170 1
                        $args = 1;
6171
6172 1
                        break;
6173 3
                    case 235:
6174 1
                        $function = 'DGET';
6175 1
                        $args = 3;
6176
6177 1
                        break;
6178 2
                    case 244:
6179 1
                        $function = 'INFO';
6180 1
                        $args = 1;
6181
6182 1
                        break;
6183 2
                    case 252:
6184
                        $function = 'FREQUENCY';
6185
                        $args = 2;
6186
6187
                        break;
6188 2
                    case 261:
6189 1
                        $function = 'ERROR.TYPE';
6190 1
                        $args = 1;
6191
6192 1
                        break;
6193 2
                    case 271:
6194 1
                        $function = 'GAMMALN';
6195 1
                        $args = 1;
6196
6197 1
                        break;
6198 2
                    case 273:
6199
                        $function = 'BINOMDIST';
6200
                        $args = 4;
6201
6202
                        break;
6203 2
                    case 274:
6204 1
                        $function = 'CHIDIST';
6205 1
                        $args = 2;
6206
6207 1
                        break;
6208 2
                    case 275:
6209 1
                        $function = 'CHIINV';
6210 1
                        $args = 2;
6211
6212 1
                        break;
6213 2
                    case 276:
6214 1
                        $function = 'COMBIN';
6215 1
                        $args = 2;
6216
6217 1
                        break;
6218 2
                    case 277:
6219 1
                        $function = 'CONFIDENCE';
6220 1
                        $args = 3;
6221
6222 1
                        break;
6223 2
                    case 278:
6224 1
                        $function = 'CRITBINOM';
6225 1
                        $args = 3;
6226
6227 1
                        break;
6228 2
                    case 279:
6229 1
                        $function = 'EVEN';
6230 1
                        $args = 1;
6231
6232 1
                        break;
6233 2
                    case 280:
6234
                        $function = 'EXPONDIST';
6235
                        $args = 3;
6236
6237
                        break;
6238 2
                    case 281:
6239 1
                        $function = 'FDIST';
6240 1
                        $args = 3;
6241
6242 1
                        break;
6243 2
                    case 282:
6244 1
                        $function = 'FINV';
6245 1
                        $args = 3;
6246
6247 1
                        break;
6248 2
                    case 283:
6249 1
                        $function = 'FISHER';
6250 1
                        $args = 1;
6251
6252 1
                        break;
6253 2
                    case 284:
6254 1
                        $function = 'FISHERINV';
6255 1
                        $args = 1;
6256
6257 1
                        break;
6258 2
                    case 285:
6259 1
                        $function = 'FLOOR';
6260 1
                        $args = 2;
6261
6262 1
                        break;
6263 2
                    case 286:
6264
                        $function = 'GAMMADIST';
6265
                        $args = 4;
6266
6267
                        break;
6268 2
                    case 287:
6269 1
                        $function = 'GAMMAINV';
6270 1
                        $args = 3;
6271
6272 1
                        break;
6273 2
                    case 288:
6274 1
                        $function = 'CEILING';
6275 1
                        $args = 2;
6276
6277 1
                        break;
6278 2
                    case 289:
6279 1
                        $function = 'HYPGEOMDIST';
6280 1
                        $args = 4;
6281
6282 1
                        break;
6283 2
                    case 290:
6284 1
                        $function = 'LOGNORMDIST';
6285 1
                        $args = 3;
6286
6287 1
                        break;
6288 2
                    case 291:
6289 1
                        $function = 'LOGINV';
6290 1
                        $args = 3;
6291
6292 1
                        break;
6293 2
                    case 292:
6294 1
                        $function = 'NEGBINOMDIST';
6295 1
                        $args = 3;
6296
6297 1
                        break;
6298 2
                    case 293:
6299
                        $function = 'NORMDIST';
6300
                        $args = 4;
6301
6302
                        break;
6303 2
                    case 294:
6304 1
                        $function = 'NORMSDIST';
6305 1
                        $args = 1;
6306
6307 1
                        break;
6308 2
                    case 295:
6309 1
                        $function = 'NORMINV';
6310 1
                        $args = 3;
6311
6312 1
                        break;
6313 2
                    case 296:
6314 1
                        $function = 'NORMSINV';
6315 1
                        $args = 1;
6316
6317 1
                        break;
6318 2
                    case 297:
6319 1
                        $function = 'STANDARDIZE';
6320 1
                        $args = 3;
6321
6322 1
                        break;
6323 2
                    case 298:
6324 1
                        $function = 'ODD';
6325 1
                        $args = 1;
6326
6327 1
                        break;
6328 2
                    case 299:
6329 1
                        $function = 'PERMUT';
6330 1
                        $args = 2;
6331
6332 1
                        break;
6333 2
                    case 300:
6334
                        $function = 'POISSON';
6335
                        $args = 3;
6336
6337
                        break;
6338 2
                    case 301:
6339 1
                        $function = 'TDIST';
6340 1
                        $args = 3;
6341
6342 1
                        break;
6343 2
                    case 302:
6344
                        $function = 'WEIBULL';
6345
                        $args = 4;
6346
6347
                        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 13
                $data = ['function' => $function, 'args' => $args];
6507
6508 13
                break;
6509 36
            case 0x22:    //    function with variable number of arguments
6510 36
            case 0x42:
6511 36
            case 0x62:
6512 14
                $name = 'tFuncV';
6513 14
                $size = 4;
6514
                // offset: 1; size: 1; number of arguments
6515 14
                $args = ord($formulaData[1]);
6516
                // offset: 2: size: 2; index to built-in sheet function
6517 14
                $index = self::getUInt2d($formulaData, 2);
6518
                switch ($index) {
6519 14
                    case 0:
6520 3
                        $function = 'COUNT';
6521
6522 3
                        break;
6523 14
                    case 1:
6524 2
                        $function = 'IF';
6525
6526 2
                        break;
6527 14
                    case 4:
6528 6
                        $function = 'SUM';
6529
6530 6
                        break;
6531 9
                    case 5:
6532 6
                        $function = 'AVERAGE';
6533
6534 6
                        break;
6535 9
                    case 6:
6536 1
                        $function = 'MIN';
6537
6538 1
                        break;
6539 9
                    case 7:
6540 1
                        $function = 'MAX';
6541
6542 1
                        break;
6543 9
                    case 8:
6544 1
                        $function = 'ROW';
6545
6546 1
                        break;
6547 9
                    case 9:
6548 1
                        $function = 'COLUMN';
6549
6550 1
                        break;
6551 9
                    case 11:
6552 1
                        $function = 'NPV';
6553
6554 1
                        break;
6555 9
                    case 12:
6556 6
                        $function = 'STDEV';
6557
6558 6
                        break;
6559 4
                    case 13:
6560 1
                        $function = 'DOLLAR';
6561
6562 1
                        break;
6563 4
                    case 14:
6564
                        $function = 'FIXED';
6565
6566
                        break;
6567 4
                    case 28:
6568 1
                        $function = 'LOOKUP';
6569
6570 1
                        break;
6571 4
                    case 29:
6572 1
                        $function = 'INDEX';
6573
6574 1
                        break;
6575 4
                    case 36:
6576 3
                        $function = 'AND';
6577
6578 3
                        break;
6579 2
                    case 37:
6580 1
                        $function = 'OR';
6581
6582 1
                        break;
6583 2
                    case 46:
6584 1
                        $function = 'VAR';
6585
6586 1
                        break;
6587 2
                    case 49:
6588
                        $function = 'LINEST';
6589
6590
                        break;
6591 2
                    case 50:
6592
                        $function = 'TREND';
6593
6594
                        break;
6595 2
                    case 51:
6596
                        $function = 'LOGEST';
6597
6598
                        break;
6599 2
                    case 52:
6600
                        $function = 'GROWTH';
6601
6602
                        break;
6603 2
                    case 56:
6604 1
                        $function = 'PV';
6605
6606 1
                        break;
6607 2
                    case 57:
6608 1
                        $function = 'FV';
6609
6610 1
                        break;
6611 2
                    case 58:
6612 1
                        $function = 'NPER';
6613
6614 1
                        break;
6615 2
                    case 59:
6616 1
                        $function = 'PMT';
6617
6618 1
                        break;
6619 2
                    case 60:
6620 1
                        $function = 'RATE';
6621
6622 1
                        break;
6623 2
                    case 62:
6624 1
                        $function = 'IRR';
6625
6626 1
                        break;
6627 2
                    case 64:
6628 1
                        $function = 'MATCH';
6629
6630 1
                        break;
6631 2
                    case 70:
6632 1
                        $function = 'WEEKDAY';
6633
6634 1
                        break;
6635 2
                    case 78:
6636
                        $function = 'OFFSET';
6637
6638
                        break;
6639 2
                    case 82:
6640 1
                        $function = 'SEARCH';
6641
6642 1
                        break;
6643 2
                    case 100:
6644 1
                        $function = 'CHOOSE';
6645
6646 1
                        break;
6647 2
                    case 101:
6648 1
                        $function = 'HLOOKUP';
6649
6650 1
                        break;
6651 2
                    case 102:
6652 1
                        $function = 'VLOOKUP';
6653
6654 1
                        break;
6655 2
                    case 109:
6656 1
                        $function = 'LOG';
6657
6658 1
                        break;
6659 2
                    case 115:
6660 1
                        $function = 'LEFT';
6661
6662 1
                        break;
6663 2
                    case 116:
6664 1
                        $function = 'RIGHT';
6665
6666 1
                        break;
6667 2
                    case 120:
6668 1
                        $function = 'SUBSTITUTE';
6669
6670 1
                        break;
6671 2
                    case 124:
6672 1
                        $function = 'FIND';
6673
6674 1
                        break;
6675 2
                    case 125:
6676
                        $function = 'CELL';
6677
6678
                        break;
6679 2
                    case 144:
6680 1
                        $function = 'DDB';
6681
6682 1
                        break;
6683 2
                    case 148:
6684 1
                        $function = 'INDIRECT';
6685
6686 1
                        break;
6687 2
                    case 167:
6688 1
                        $function = 'IPMT';
6689
6690 1
                        break;
6691 2
                    case 168:
6692 1
                        $function = 'PPMT';
6693
6694 1
                        break;
6695 2
                    case 169:
6696 1
                        $function = 'COUNTA';
6697
6698 1
                        break;
6699 2
                    case 183:
6700 1
                        $function = 'PRODUCT';
6701
6702 1
                        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 14
                $data = ['function' => $function, 'args' => $args];
6875
6876 14
                break;
6877 36
            case 0x23:    //    index to defined name
6878 36
            case 0x43:
6879 36
            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 35
            case 0x24:    //    single cell reference e.g. A5
6889 35
            case 0x44:
6890 32
            case 0x64:
6891 14
                $name = 'tRef';
6892 14
                $size = 5;
6893 14
                $data = $this->readBIFF8CellAddress(substr($formulaData, 1, 4));
6894
6895 14
                break;
6896 30
            case 0x25:    //    cell range reference to cells in the same sheet (2d)
6897 13
            case 0x45:
6898 13
            case 0x65:
6899 22
                $name = 'tArea';
6900 22
                $size = 9;
6901 22
                $data = $this->readBIFF8CellRangeAddress(substr($formulaData, 1, 8));
6902
6903 22
                break;
6904 12
            case 0x26:    //    Constant reference sub-expression
6905 12
            case 0x46:
6906 12
            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 12
            case 0x27:    //    Deleted constant reference sub-expression
6916 12
            case 0x47:
6917 12
            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 12
            case 0x29:    //    Variable reference sub-expression
6927 12
            case 0x49:
6928 12
            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 12
            case 0x2C: // Relative 2d cell reference reference, used in shared formulas and some other places
6937 12
            case 0x4C:
6938 12
            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 10
            case 0x2D:    //    Relative 2d range reference
6945 10
            case 0x4D:
6946 10
            case 0x6D:
6947
                $name = 'tAreaN';
6948
                $size = 9;
6949
                $data = $this->readBIFF8CellRangeAddressB(substr($formulaData, 1, 8), $baseCell);
6950
6951
                break;
6952 10
            case 0x39:    //    External name
6953 10
            case 0x59:
6954 10
            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 10
            case 0x3A:    //    3d reference to cell
6965 8
            case 0x5A:
6966 8
            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 8
            case 0x3B:    //    3d reference to cell range
6984
            case 0x5B:
6985
            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
                throw new Exception('Unrecognized token ' . sprintf('%02X', $id) . ' in formula');
7005
        }
7006
7007 40
        return [
7008 40
            'id' => $id,
7009 40
            'name' => $name,
7010 40
            'size' => $size,
7011 40
            'data' => $data,
7012 40
        ];
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 17
    private function readBIFF8CellAddress($cellAddressStructure)
7024
    {
7025
        // offset: 0; size: 2; index to row (0... 65535) (or offset (-32768... 32767))
7026 17
        $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 17
        $column = Coordinate::stringFromColumnIndex((0x00FF & self::getUInt2d($cellAddressStructure, 2)) + 1);
7031
7032
        // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index)
7033 17
        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 17
        if (!(0x8000 & self::getUInt2d($cellAddressStructure, 2))) {
7038 10
            $row = '$' . $row;
7039
        }
7040
7041 17
        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 78
    private function readBIFF5CellRangeAddressFixed($subData)
7103
    {
7104
        // offset: 0; size: 2; index to first row
7105 78
        $fr = self::getUInt2d($subData, 0) + 1;
7106
7107
        // offset: 2; size: 2; index to last row
7108 78
        $lr = self::getUInt2d($subData, 2) + 1;
7109
7110
        // offset: 4; size: 1; index to first column
7111 78
        $fc = ord($subData[4]);
7112
7113
        // offset: 5; size: 1; index to last column
7114 78
        $lc = ord($subData[5]);
7115
7116
        // check values
7117 78
        if ($fr > $lr || $fc > $lc) {
7118
            throw new Exception('Not a cell range address');
7119
        }
7120
7121
        // column index to letter
7122 78
        $fc = Coordinate::stringFromColumnIndex($fc + 1);
7123 78
        $lc = Coordinate::stringFromColumnIndex($lc + 1);
7124
7125 78
        if ($fr == $lr && $fc == $lc) {
7126 67
            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 26
    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 26
        $fr = self::getUInt2d($subData, 0) + 1;
7187
7188
        // offset: 2; size: 2; index to last row (0... 65535) (or offset (-32768... 32767))
7189 26
        $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 26
        $fc = Coordinate::stringFromColumnIndex((0x00FF & self::getUInt2d($subData, 4)) + 1);
7195
7196
        // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index)
7197 26
        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 26
        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 26
        $lc = Coordinate::stringFromColumnIndex((0x00FF & self::getUInt2d($subData, 6)) + 1);
7210
7211
        // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index)
7212 26
        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 26
        if (!(0x8000 & self::getUInt2d($subData, 6))) {
7218 13
            $lr = '$' . $lr;
7219
        }
7220
7221 26
        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 78
    private function readBIFF5CellRangeAddressList($subData)
7348
    {
7349 78
        $cellRangeAddresses = [];
7350
7351
        // offset: 0; size: 2; number of the following cell range addresses
7352 78
        $nm = self::getUInt2d($subData, 0);
7353
7354 78
        $offset = 2;
7355
        // offset: 2; size: 6 * $nm; list of $nm (fixed) cell range addresses
7356 78
        for ($i = 0; $i < $nm; ++$i) {
7357 78
            $cellRangeAddresses[] = $this->readBIFF5CellRangeAddressFixed(substr($subData, $offset, 6));
7358 78
            $offset += 6;
7359
        }
7360
7361 78
        return [
7362 78
            'size' => 2 + 6 * $nm,
7363 78
            'cellRangeAddresses' => $cellRangeAddresses,
7364 78
        ];
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 55
    private static function readRGB($rgb)
7528
    {
7529
        // offset: 0; size 1; Red component
7530 55
        $r = ord($rgb[0]);
7531
7532
        // offset: 1; size: 1; Green component
7533 55
        $g = ord($rgb[1]);
7534
7535
        // offset: 2; size: 1; Blue component
7536 55
        $b = ord($rgb[2]);
7537
7538
        // HEX notation, e.g. 'FF00FC'
7539 55
        $rgb = sprintf('%02X%02X%02X', $r, $g, $b);
7540
7541 55
        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 84
    private static function readUnicodeStringShort($subData)
7599
    {
7600 84
        $value = '';
7601
7602
        // offset: 0: size: 1; length of the string (character count)
7603 84
        $characterCount = ord($subData[0]);
7604
7605 84
        $string = self::readUnicodeString(substr($subData, 1), $characterCount);
7606
7607
        // add 1 for the string length
7608 84
        ++$string['size'];
7609
7610 84
        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 80
    private static function readUnicodeStringLong($subData)
7623
    {
7624 80
        $value = '';
7625
7626
        // offset: 0: size: 2; length of the string (character count)
7627 80
        $characterCount = self::getUInt2d($subData, 0);
7628
7629 80
        $string = self::readUnicodeString(substr($subData, 2), $characterCount);
7630
7631
        // add 2 for the string length
7632 80
        $string['size'] += 2;
7633
7634 80
        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 84
    private static function readUnicodeString($subData, $characterCount)
7648
    {
7649 84
        $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 84
        $isCompressed = !((0x01 & ord($subData[0])) >> 0);
7654
7655
        // bit: 2; mask: 0x04; Asian phonetic settings
7656 84
        $hasAsian = (0x04) & ord($subData[0]) >> 2;
7657
7658
        // bit: 3; mask: 0x08; Rich-Text settings
7659 84
        $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 84
        $value = self::encodeUTF16(substr($subData, 1, $isCompressed ? $characterCount : 2 * $characterCount), $isCompressed);
7665
7666 84
        return [
7667 84
            'value' => $value,
7668 84
            'size' => $isCompressed ? 1 + $characterCount : 1 + 2 * $characterCount, // the size in bytes including the option flags
7669 84
        ];
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 82
    private static function extractNumber($data)
7693
    {
7694 82
        $rknumhigh = self::getInt4d($data, 4);
7695 82
        $rknumlow = self::getInt4d($data, 0);
7696 82
        $sign = ($rknumhigh & (int) 0x80000000) >> 31;
7697 82
        $exp = (($rknumhigh & 0x7ff00000) >> 20) - 1023;
7698 82
        $mantissa = (0x100000 | ($rknumhigh & 0x000fffff));
7699 82
        $mantissalow1 = ($rknumlow & (int) 0x80000000) >> 31;
7700 82
        $mantissalow2 = ($rknumlow & 0x7fffffff);
7701 82
        $value = $mantissa / 2 ** (20 - $exp);
7702
7703 82
        if ($mantissalow1 != 0) {
7704 24
            $value += 1 / 2 ** (21 - $exp);
7705
        }
7706
7707 82
        $value += $mantissalow2 / 2 ** (52 - $exp);
7708 82
        if ($sign) {
7709 17
            $value *= -1;
7710
        }
7711
7712 82
        return $value;
7713
    }
7714
7715
    /**
7716
     * @param int $rknum
7717
     *
7718
     * @return float
7719
     */
7720 31
    private static function getIEEE754($rknum)
7721
    {
7722 31
        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 26
            $sign = ($rknum & (int) 0x80000000) >> 31;
7731 26
            $exp = ($rknum & 0x7ff00000) >> 20;
7732 26
            $mantissa = (0x100000 | ($rknum & 0x000ffffc));
7733 26
            $value = $mantissa / 2 ** (20 - ($exp - 1023));
7734 26
            if ($sign) {
7735 11
                $value = -1 * $value;
7736
            }
7737
            //end of changes by mmp
7738
        }
7739 31
        if (($rknum & 0x01) != 0) {
7740 14
            $value /= 100;
7741
        }
7742
7743 31
        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 84
    private static function encodeUTF16($string, $compressed = false)
7755
    {
7756 84
        if ($compressed) {
7757 49
            $string = self::uncompressByteString($string);
7758
        }
7759
7760 84
        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 49
    private static function uncompressByteString($string)
7771
    {
7772 49
        $uncompressedString = '';
7773 49
        $strLen = strlen($string);
7774 49
        for ($i = 0; $i < $strLen; ++$i) {
7775 48
            $uncompressedString .= $string[$i] . "\0";
7776
        }
7777
7778 49
        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 85
    public static function getUInt2d($data, $pos)
7802
    {
7803 85
        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 85
    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 85
        $_or_24 = ord($data[$pos + 3]);
7833 85
        if ($_or_24 >= 128) {
7834
            // negative number
7835 32
            $_ord_24 = -abs((256 - $_or_24) << 24);
7836
        } else {
7837 85
            $_ord_24 = ($_or_24 & 127) << 24;
7838
        }
7839
7840 85
        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