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

Xls::getCFAlignmentStyle()   A

Complexity

Conditions 1

Size

Total Lines 2
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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